diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 24996c8d4b29..db7fd0cf2f23 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -385,6 +385,33 @@ NOTE: At any point in time through this workflow you should feel free to ask the return input.messages }) + const flush = Effect.fn("SessionPrompt.flush")(function* ( + inject: Array<{ role: "user" | "system"; text: string }> | undefined, + sessionID: SessionID, + user: { agent: string; model: MessageV2.User["model"] }, + ) { + if (!inject?.length) return + for (const entry of inject) { + const msg: MessageV2.User = { + id: MessageID.ascending(), + sessionID, + role: "user", + time: { created: Date.now() }, + agent: user.agent, + model: user.model, + } + yield* sessions.updateMessage(msg) + yield* sessions.updatePart({ + id: PartID.ascending(), + messageID: msg.id, + sessionID, + type: "text", + text: entry.role === "system" ? `\n${entry.text}\n` : entry.text, + synthetic: true, + } satisfies MessageV2.TextPart) + } + }) + const resolveTools = Effect.fn("SessionPrompt.resolveTools")(function* (input: { agent: Agent.Info model: Provider.Model @@ -466,6 +493,11 @@ NOTE: At any point in time through this workflow you should feel free to ask the { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args }, output, ) + yield* flush( + (output as Record).inject, + ctx.sessionID, + { agent: input.agent.name, model: { providerID: input.model.providerID, modelID: ModelID.make(input.model.api.id) } }, + ) return output }), ) @@ -498,6 +530,11 @@ NOTE: At any point in time through this workflow you should feel free to ask the { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId, args }, result, ) + yield* flush( + (result as Record).inject, + ctx.sessionID, + { agent: input.agent.name, model: { providerID: input.model.providerID, modelID: ModelID.make(input.model.api.id) } }, + ) const textParts: string[] = [] const attachments: Omit[] = [] @@ -683,6 +720,11 @@ NOTE: At any point in time through this workflow you should feel free to ask the { tool: "task", sessionID, callID: part.id, args: taskArgs }, result, ) + yield* flush( + (result as Record | undefined)?.inject, + sessionID, + { agent: lastUser.agent, model: lastUser.model }, + ) assistantMessage.finish = "tool-calls" assistantMessage.time.completed = Date.now() diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 473cac8a9bff..4b9ac7c2952d 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -231,12 +231,32 @@ export interface Hooks { input: { cwd: string; sessionID?: string; callID?: string }, output: { env: Record }, ) => Promise + /** + * Called after a tool finishes execution. Plugins can modify the tool + * result (title, output, metadata) before the AI sees it. + * + * Set `inject` to append messages the AI will see on the **next** loop + * iteration. This enables behavioral enforcement — e.g. reminding the + * AI to update planning files after every edit. + * + * ```ts + * "tool.execute.after": async (input, output) => { + * if (input.tool === "edit") { + * output.inject = [ + * { role: "user", text: "Remember to update progress.md." }, + * ] + * } + * } + * ``` + */ "tool.execute.after"?: ( input: { tool: string; sessionID: string; callID: string; args: any }, output: { title: string output: string metadata: any + /** Messages to inject into the conversation after this tool call. */ + inject?: Array<{ role: "user" | "system"; text: string }> }, ) => Promise "experimental.chat.messages.transform"?: (