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"?: (