@@ -41,19 +41,27 @@ export const ChainloopTrace: Plugin = async ({ $, client }) => {
4141 }
4242 }
4343
44- // startSession fires the session-start hook and returns what it wrote to
45- // stdout: the instruction for the model, if Chainloop has one.
46- async function startSession ( sessionID : string ) : Promise < string > {
47- const json = JSON . stringify ( { session_id : sessionID , hook_event_name : "session.created" } )
44+ // instructionFrom fires a hook that can answer with an instruction for the
45+ // model, and returns what it wrote to stdout: that instruction, if
46+ // Chainloop has one.
47+ async function instructionFrom ( event : string , sessionID : string , hookEventName : string ) : Promise < string > {
48+ const json = JSON . stringify ( { session_id : sessionID , hook_event_name : hookEventName } )
4849 try {
49- const out = await $ `echo ${ json } | chainloop trace hook opencode session-start ` . text ( )
50+ const out = await $ `echo ${ json } | chainloop trace hook opencode ${ event } ` . text ( )
5051 return out . trim ( ) ? ( JSON . parse ( out ) . instruction ?? "" ) : ""
5152 } catch ( err ) {
52- console . error ( `chainloop-trace: session-start hook failed: ${ err } ` )
53+ console . error ( `chainloop-trace: ${ event } hook failed: ${ err } ` )
5354 return ""
5455 }
5556 }
5657
58+ // childSessions holds the sessions of subagents, whose parent session
59+ // already gets the instruction and the reminder.
60+ const childSessions = new Set < string > ( )
61+ // reminding holds the sessions that have a reminder being posted. The post
62+ // is a message too, and must not ask for another reminder.
63+ const reminding = new Set < string > ( )
64+
5765 // postInstruction adds the instruction to the session as a context-only
5866 // message: noReply stores it without asking the model for an answer. The
5967 // caller waits for it, so a first turn sent right away still finds it.
@@ -73,22 +81,44 @@ export const ChainloopTrace: Plugin = async ({ $, client }) => {
7381 if ( event . type === "session.created" ) {
7482 const info = event . properties ?. info
7583 const sessionID = info ?. id ?? ""
76- const instruction = await startSession ( sessionID )
7784 // A child session belongs to a subagent, whose parent already has
7885 // the instruction.
86+ if ( info ?. parentID ) childSessions . add ( sessionID )
87+ const instruction = await instructionFrom ( "session-start" , sessionID , "session.created" )
7988 if ( instruction && ! info ?. parentID ) await postInstruction ( sessionID , instruction )
8089 }
8190 if ( event . type === "session.deleted" ) {
8291 const sessionID = event . properties ?. info ?. id ?? ""
92+ childSessions . delete ( sessionID )
8393 await fire ( "session-end" , { session_id : sessionID , hook_event_name : "session.deleted" } )
8494 }
8595 } ,
96+ // At each user message, the user-prompt-submit hook can answer with a
97+ // short reminder to capture a new or changed spec. A message made only of
98+ // synthetic parts is one that the plugin posted itself.
99+ "chat.message" : async ( input , output ) => {
100+ const sessionID = input . sessionID
101+ if ( childSessions . has ( sessionID ) || reminding . has ( sessionID ) ) return
102+ const parts : any [ ] = output ?. parts ?? [ ]
103+ if ( parts . length > 0 && parts . every ( ( p ) => p ?. synthetic ) ) return
104+ const reminder = await instructionFrom ( "user-prompt-submit" , sessionID , "chat.message" )
105+ if ( ! reminder ) return
106+ reminding . add ( sessionID )
107+ try {
108+ await postInstruction ( sessionID , reminder )
109+ } finally {
110+ reminding . delete ( sessionID )
111+ }
112+ } ,
86113 "tool.execute.before" : async ( input , output ) => {
87114 if ( commandTools . includes ( input . tool ) ) {
115+ // callID pairs this hook with the tool.execute.after of the same
116+ // call, so overlapping commands keep their own snapshots.
88117 await fire ( "pre-tool-use" , {
89118 session_id : input . sessionID ,
90119 hook_event_name : "tool.execute.before" ,
91120 tool_name : input . tool ,
121+ tool_use_id : input . callID ,
92122 } )
93123 return
94124 }
@@ -108,6 +138,7 @@ export const ChainloopTrace: Plugin = async ({ $, client }) => {
108138 session_id : input . sessionID ,
109139 hook_event_name : "tool.execute.after" ,
110140 tool_name : input . tool ,
141+ tool_use_id : input . callID ,
111142 } )
112143 return
113144 }
0 commit comments