From 5af7ede8de9b715cf315b8046dc610f03331ea9d Mon Sep 17 00:00:00 2001 From: ArmirKS Date: Fri, 27 Feb 2026 23:16:43 +0100 Subject: [PATCH] feat(plugin): surface agent and parentAgent in plugin hook input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds agent and parentAgent to tool.execute.before, tool.execute.after, and shell.env hook input. When agent A spawns agent B via the task tool, hooks receive { agent: 'B', parentAgent: 'A' }. Top-level agents have parentAgent undefined. Both fields are optional — no existing plugin breaks. Fixes #15403 --- packages/opencode/src/session/message-v2.ts | 1 + packages/opencode/src/session/prompt.ts | 19 +++++- packages/opencode/src/tool/task.ts | 1 + .../opencode/test/plugin/parent-agent.test.ts | 59 +++++++++++++++++++ packages/plugin/src/index.ts | 6 +- 5 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 packages/opencode/test/plugin/parent-agent.test.ts diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 178751a2227..471a324fae8 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -356,6 +356,7 @@ export namespace MessageV2 { }) .optional(), agent: z.string(), + parentAgent: z.string().optional(), model: z.object({ providerID: z.string(), modelID: z.string(), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 75bd3c9dfac..436fa177b6a 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -98,6 +98,7 @@ export namespace SessionPrompt { }) .optional(), agent: z.string().optional(), + parentAgent: z.string().optional(), noReply: z.boolean().optional(), tools: z .record(z.string(), z.boolean()) @@ -409,6 +410,8 @@ export namespace SessionPrompt { tool: "task", sessionID, callID: part.id, + agent: lastUser.agent, + parentAgent: lastUser.parentAgent, }, { args: taskArgs }, ) @@ -458,6 +461,8 @@ export namespace SessionPrompt { sessionID, callID: part.id, args: taskArgs, + agent: lastUser.agent, + parentAgent: lastUser.parentAgent, }, result, ) @@ -607,6 +612,7 @@ export namespace SessionPrompt { processor, bypassAgentCheck, messages: msgs, + parentAgent: lastUser.parentAgent, }) // Inject StructuredOutput tool if JSON schema mode enabled @@ -739,6 +745,7 @@ export namespace SessionPrompt { processor: SessionProcessor.Info bypassAgentCheck: boolean messages: MessageV2.WithParts[] + parentAgent?: string }) { using _ = log.time("resolveTools") const tools: Record = {} @@ -795,6 +802,8 @@ export namespace SessionPrompt { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, + agent: ctx.agent, + parentAgent: input.parentAgent, }, { args, @@ -817,6 +826,8 @@ export namespace SessionPrompt { sessionID: ctx.sessionID, callID: ctx.callID, args, + agent: ctx.agent, + parentAgent: input.parentAgent, }, output, ) @@ -841,6 +852,8 @@ export namespace SessionPrompt { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId, + agent: ctx.agent, + parentAgent: input.parentAgent, }, { args, @@ -863,6 +876,8 @@ export namespace SessionPrompt { sessionID: ctx.sessionID, callID: opts.toolCallId, args, + agent: ctx.agent, + parentAgent: input.parentAgent, }, result, ) @@ -970,6 +985,7 @@ export namespace SessionPrompt { }, tools: input.tools, agent: agent.name, + parentAgent: input.parentAgent, model, system: input.system, format: input.format, @@ -1461,6 +1477,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the export const ShellInput = z.object({ sessionID: Identifier.schema("session"), agent: z.string(), + parentAgent: z.string().optional(), model: z .object({ providerID: z.string(), @@ -1620,7 +1637,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the const cwd = Instance.directory const shellEnv = await Plugin.trigger( "shell.env", - { cwd, sessionID: input.sessionID, callID: part.callID }, + { cwd, sessionID: input.sessionID, callID: part.callID, agent: input.agent, parentAgent: input.parentAgent }, { env: {} }, ) const proc = spawn(shell, args, { diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 8c8cf827aba..087eebdea0f 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -133,6 +133,7 @@ export const TaskTool = Tool.define("task", async (ctx) => { providerID: model.providerID, }, agent: agent.name, + parentAgent: ctx.agent, tools: { todowrite: false, todoread: false, diff --git a/packages/opencode/test/plugin/parent-agent.test.ts b/packages/opencode/test/plugin/parent-agent.test.ts new file mode 100644 index 00000000000..b0c11a51f74 --- /dev/null +++ b/packages/opencode/test/plugin/parent-agent.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "bun:test" +import { SessionPrompt } from "../../src/session/prompt" +import { MessageV2 } from "../../src/session/message-v2" + +describe("parentAgent hook input", () => { + test("PromptInput accepts parentAgent", () => { + const input = SessionPrompt.PromptInput.parse({ + sessionID: "ses_test", + agent: "scout", + parentAgent: "coder", + parts: [{ type: "text", text: "test" }], + }) + expect(input.parentAgent).toBe("coder") + }) + + test("PromptInput parentAgent is optional", () => { + const input = SessionPrompt.PromptInput.parse({ + sessionID: "ses_test", + agent: "scout", + parts: [{ type: "text", text: "test" }], + }) + expect(input.parentAgent).toBeUndefined() + }) + + test("ShellInput accepts parentAgent", () => { + const input = SessionPrompt.ShellInput.parse({ + sessionID: "ses_test", + agent: "coder", + parentAgent: "orchestrator", + command: "ls", + }) + expect(input.parentAgent).toBe("orchestrator") + }) + + test("UserMessage stores parentAgent", () => { + const msg = MessageV2.User.parse({ + id: "msg_test", + sessionID: "ses_test", + role: "user", + time: { created: Date.now() }, + agent: "scout", + parentAgent: "coder", + model: { providerID: "test", modelID: "test" }, + }) + expect(msg.parentAgent).toBe("coder") + }) + + test("UserMessage parentAgent is optional (top-level agent)", () => { + const msg = MessageV2.User.parse({ + id: "msg_test", + sessionID: "ses_test", + role: "user", + time: { created: Date.now() }, + agent: "coder", + model: { providerID: "test", modelID: "test" }, + }) + expect(msg.parentAgent).toBeUndefined() + }) +}) diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 76370d1d5a7..8c54d1e5154 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -182,15 +182,15 @@ export interface Hooks { output: { parts: Part[] }, ) => Promise "tool.execute.before"?: ( - input: { tool: string; sessionID: string; callID: string }, + input: { tool: string; sessionID: string; callID: string; agent?: string; parentAgent?: string }, output: { args: any }, ) => Promise "shell.env"?: ( - input: { cwd: string; sessionID?: string; callID?: string }, + input: { cwd: string; sessionID?: string; callID?: string; agent?: string; parentAgent?: string }, output: { env: Record }, ) => Promise "tool.execute.after"?: ( - input: { tool: string; sessionID: string; callID: string; args: any }, + input: { tool: string; sessionID: string; callID: string; args: any; agent?: string; parentAgent?: string }, output: { title: string output: string