diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 8e4babd61924..2245f8340147 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -362,6 +362,7 @@ export namespace MessageV2 { }) .optional(), agent: z.string(), + parentAgent: z.string().optional(), model: z.object({ providerID: ProviderID.zod, modelID: ModelID.zod, diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 743537f59871..2866d53b6fa3 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -101,6 +101,7 @@ export namespace SessionPrompt { }) .optional(), agent: z.string().optional(), + parentAgent: z.string().optional(), noReply: z.boolean().optional(), tools: z .record(z.string(), z.boolean()) @@ -411,7 +412,10 @@ export namespace SessionPrompt { { tool: "task", sessionID, + messageID: assistantMessage.id, callID: part.id, + agent: lastUser.agent, + parentAgent: lastUser.parentAgent, }, { args: taskArgs }, ) @@ -459,8 +463,11 @@ export namespace SessionPrompt { { tool: "task", sessionID, + messageID: assistantMessage.id, callID: part.id, args: taskArgs, + agent: lastUser.agent, + parentAgent: lastUser.parentAgent, }, result, ) @@ -611,6 +618,7 @@ export namespace SessionPrompt { processor, bypassAgentCheck, messages: msgs, + parentAgent: lastUser.parentAgent, }) // Inject StructuredOutput tool if JSON schema mode enabled @@ -749,6 +757,7 @@ export namespace SessionPrompt { processor: SessionProcessor.Info bypassAgentCheck: boolean messages: MessageV2.WithParts[] + parentAgent?: string }) { using _ = log.time("resolveTools") const tools: Record = {} @@ -804,7 +813,10 @@ export namespace SessionPrompt { { tool: item.id, sessionID: ctx.sessionID, + messageID: input.processor.message.id, callID: ctx.callID, + agent: ctx.agent, + parentAgent: input.parentAgent, }, { args, @@ -825,8 +837,11 @@ export namespace SessionPrompt { { tool: item.id, sessionID: ctx.sessionID, + messageID: input.processor.message.id, callID: ctx.callID, args, + agent: ctx.agent, + parentAgent: input.parentAgent, }, output, ) @@ -850,7 +865,10 @@ export namespace SessionPrompt { { tool: key, sessionID: ctx.sessionID, + messageID: input.processor.message.id, callID: opts.toolCallId, + agent: ctx.agent, + parentAgent: input.parentAgent, }, { args, @@ -871,8 +889,11 @@ export namespace SessionPrompt { { tool: key, sessionID: ctx.sessionID, + messageID: input.processor.message.id, callID: opts.toolCallId, args, + agent: ctx.agent, + parentAgent: input.parentAgent, }, result, ) @@ -980,6 +1001,7 @@ export namespace SessionPrompt { }, tools: input.tools, agent: agent.name, + parentAgent: input.parentAgent, model, system: input.system, format: input.format, @@ -1471,6 +1493,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the export const ShellInput = z.object({ sessionID: SessionID.zod, agent: z.string(), + parentAgent: z.string().optional(), model: z .object({ providerID: ProviderID.zod, @@ -1630,7 +1653,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 68e44eb97e48..2c63c4889ff8 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -134,6 +134,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 000000000000..b0c11a51f744 --- /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 b78bcae177d4..62312aaa5614 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -182,15 +182,23 @@ export interface Hooks { output: { parts: Part[] }, ) => Promise "tool.execute.before"?: ( - input: { tool: string; sessionID: string; callID: string }, + input: { tool: string; sessionID: string; messageID: 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 + messageID: string + callID: string + args: any + agent?: string + parentAgent?: string + }, output: { title: string output: string