11import type { Plugin , PluginInput } from "@opencode-ai/plugin" ;
22import type { Message , Part } from "@opencode-ai/sdk" ;
3+ import { z } from "zod" ;
34import { buildHandoffPrompt } from "./prompt.ts" ;
45import { createAutoUpdateHook } from "./auto-update.ts" ;
56
@@ -210,6 +211,15 @@ async function executeHandoff(
210211 return `✓ Session "${ newTitle } " created (${ context . agent || "default" } · ${ modelDisplay } ). Select it from the picker.` ;
211212}
212213
214+ const handoffArgsSchema = {
215+ summary : z . string ( ) . describe ( "1-3 sentence summary of current state (required)" ) ,
216+ goal : z . string ( ) . optional ( ) . describe ( "Goal for the next session if user specified one" ) ,
217+ next_steps : z . array ( z . string ( ) ) . optional ( ) . describe ( "Array of remaining tasks" ) ,
218+ blocked : z . string ( ) . optional ( ) . describe ( "Current blocker if any" ) ,
219+ key_decisions : z . array ( z . string ( ) ) . optional ( ) . describe ( "Important decisions made" ) ,
220+ files_modified : z . array ( z . string ( ) ) . optional ( ) . describe ( "Key files that were changed" ) ,
221+ } ;
222+
213223function createHandoffTool ( pluginCtx : PluginContext ) {
214224 return {
215225 description : `Generate a compact continuation prompt and start a new session with it.
@@ -222,16 +232,8 @@ When called, this tool:
222232
223233IMPORTANT: You MUST provide a concise summary. Do not dump the entire conversation - distill it to essential context only.
224234
225- Arguments (pass as JSON object):
226- - summary (required): 1-3 sentence summary of current state
227- - goal (optional): If the user said "handoff <something>" or "session_handoff <something>", extract what comes after as the goal for the next session
228- - next_steps (optional): Array of remaining tasks
229- - blocked (optional): Current blocker if any
230- - key_decisions (optional): Array of important decisions made
231- - files_modified (optional): Array of key files changed
232-
233235The new session will have access to \`read_session\` tool if more context is needed later.` ,
234- args : { } ,
236+ args : handoffArgsSchema ,
235237 async execute ( args : Record < string , unknown > , ctx : { sessionID : string } ) {
236238 return executeHandoff ( pluginCtx , args as unknown as HandoffToolArgs , ctx . sessionID ) ;
237239 } ,
@@ -290,6 +292,30 @@ This tool fetches the last 20 messages which uses significant tokens. The handof
290292 } ;
291293}
292294
295+ export function isHandoffTrigger ( text : string ) : boolean {
296+ const trimmed = text . trim ( ) . toLowerCase ( ) ;
297+ return (
298+ trimmed === "handoff" ||
299+ trimmed === "/handoff" ||
300+ trimmed === "session handoff" ||
301+ trimmed . startsWith ( "handoff " ) ||
302+ trimmed . startsWith ( "/handoff " )
303+ ) ;
304+ }
305+
306+ export function extractGoalFromHandoff ( text : string ) : string | null {
307+ const trimmed = text . trim ( ) ;
308+ const lower = trimmed . toLowerCase ( ) ;
309+
310+ if ( lower . startsWith ( "/handoff " ) ) {
311+ return trimmed . slice ( 9 ) . trim ( ) || null ;
312+ }
313+ if ( lower . startsWith ( "handoff " ) ) {
314+ return trimmed . slice ( 8 ) . trim ( ) || null ;
315+ }
316+ return null ;
317+ }
318+
293319const HandoffPlugin : Plugin = async ( ctx ) => {
294320 const autoUpdateHook = createAutoUpdateHook ( {
295321 directory : ctx . directory ,
@@ -309,6 +335,46 @@ const HandoffPlugin: Plugin = async (ctx) => {
309335 client : ctx . client ,
310336 } ) ,
311337 } ,
338+ "chat.message" : async (
339+ _input : {
340+ sessionID : string ;
341+ agent ?: string ;
342+ model ?: { providerID : string ; modelID : string } ;
343+ messageID ?: string ;
344+ variant ?: string ;
345+ } ,
346+ output : { message : unknown ; parts : Part [ ] } ,
347+ ) => {
348+ const textParts = output . parts . filter (
349+ ( p ) : p is Part & { type : "text" ; text : string } =>
350+ p . type === "text" && typeof ( p as { text ?: string } ) . text === "string" ,
351+ ) ;
352+
353+ for ( const part of textParts ) {
354+ if ( isHandoffTrigger ( part . text ) ) {
355+ const goal = extractGoalFromHandoff ( part . text ) ;
356+
357+ part . text = [
358+ '<system-instruction priority="critical">' ,
359+ "The user has triggered a session handoff. You MUST invoke the `session_handoff` tool immediately." ,
360+ "" ,
361+ "DO NOT interpret this as a regular task request." ,
362+ "DO NOT continue working on any previous tasks." ,
363+ "DO NOT ask clarifying questions." ,
364+ "" ,
365+ "IMMEDIATELY call the session_handoff tool with:" ,
366+ "- summary: A brief summary of what was accomplished in this session" ,
367+ goal ? `- goal: "${ goal } "` : "- goal: (not specified)" ,
368+ "- next_steps: Any remaining tasks from the todo list" ,
369+ "- key_decisions: Important decisions made during the session" ,
370+ "- files_modified: Key files that were changed" ,
371+ "</system-instruction>" ,
372+ ] . join ( "\n" ) ;
373+
374+ break ;
375+ }
376+ }
377+ } ,
312378 } ;
313379} ;
314380
0 commit comments