Skip to content

Commit 24eb459

Browse files
committed
feat(plugin): include parent agent context in hook inputs
1 parent 378b8ca commit 24eb459

8 files changed

Lines changed: 174 additions & 13 deletions

File tree

packages/opencode/src/session/llm.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ export namespace LLM {
171171
{
172172
sessionID: input.sessionID,
173173
agent: input.agent.name,
174+
parentAgent: input.user.parentAgent,
174175
model: input.model,
175176
provider,
176177
message: input.user,
@@ -191,6 +192,7 @@ export namespace LLM {
191192
{
192193
sessionID: input.sessionID,
193194
agent: input.agent.name,
195+
parentAgent: input.user.parentAgent,
194196
model: input.model,
195197
provider,
196198
message: input.user,

packages/opencode/src/session/message-v2.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ export namespace MessageV2 {
368368
})
369369
.optional(),
370370
agent: z.string(),
371+
parentAgent: z.string().optional(),
371372
model: z.object({
372373
providerID: ProviderID.zod,
373374
modelID: ModelID.zod,

packages/opencode/src/session/prompt.ts

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
347347
processor: Pick<SessionProcessor.Handle, "message" | "updateToolCall" | "completeToolCall">
348348
bypassAgentCheck: boolean
349349
messages: MessageV2.WithParts[]
350+
parentAgent?: string
350351
}) {
351352
using _ = log.time("resolveTools")
352353
const tools: Record<string, AITool> = {}
@@ -402,7 +403,14 @@ NOTE: At any point in time through this workflow you should feel free to ask the
402403
const ctx = context(args, options)
403404
yield* plugin.trigger(
404405
"tool.execute.before",
405-
{ tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID },
406+
{
407+
tool: item.id,
408+
sessionID: ctx.sessionID,
409+
messageID: input.processor.message.id,
410+
callID: ctx.callID,
411+
agent: ctx.agent,
412+
parentAgent: input.parentAgent,
413+
},
406414
{ args },
407415
)
408416
const result = yield* Effect.promise(() => item.execute(args, ctx))
@@ -417,7 +425,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
417425
}
418426
yield* plugin.trigger(
419427
"tool.execute.after",
420-
{ tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args },
428+
{
429+
tool: item.id,
430+
sessionID: ctx.sessionID,
431+
messageID: input.processor.message.id,
432+
callID: ctx.callID,
433+
args,
434+
agent: ctx.agent,
435+
parentAgent: input.parentAgent,
436+
},
421437
output,
422438
)
423439
if (options.abortSignal?.aborted) {
@@ -443,7 +459,14 @@ NOTE: At any point in time through this workflow you should feel free to ask the
443459
const ctx = context(args, opts)
444460
yield* plugin.trigger(
445461
"tool.execute.before",
446-
{ tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId },
462+
{
463+
tool: key,
464+
sessionID: ctx.sessionID,
465+
messageID: input.processor.message.id,
466+
callID: opts.toolCallId,
467+
agent: ctx.agent,
468+
parentAgent: input.parentAgent,
469+
},
447470
{ args },
448471
)
449472
yield* Effect.promise(() => ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] }))
@@ -452,7 +475,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
452475
)
453476
yield* plugin.trigger(
454477
"tool.execute.after",
455-
{ tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId, args },
478+
{
479+
tool: key,
480+
sessionID: ctx.sessionID,
481+
messageID: input.processor.message.id,
482+
callID: opts.toolCallId,
483+
args,
484+
agent: ctx.agent,
485+
parentAgent: input.parentAgent,
486+
},
456487
result,
457488
)
458489

@@ -564,7 +595,14 @@ NOTE: At any point in time through this workflow you should feel free to ask the
564595
}
565596
yield* plugin.trigger(
566597
"tool.execute.before",
567-
{ tool: TaskTool.id, sessionID, callID: part.id },
598+
{
599+
tool: TaskTool.id,
600+
sessionID,
601+
messageID: assistantMessage.id,
602+
callID: part.id,
603+
agent: lastUser.agent,
604+
parentAgent: lastUser.parentAgent,
605+
},
568606
{ args: taskArgs },
569607
)
570608

@@ -582,6 +620,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
582620
taskTool
583621
.execute(taskArgs, {
584622
agent: task.agent,
623+
parentAgent: lastUser.agent,
585624
messageID: assistantMessage.id,
586625
sessionID,
587626
abort: signal,
@@ -645,7 +684,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
645684

646685
yield* plugin.trigger(
647686
"tool.execute.after",
648-
{ tool: TaskTool.id, sessionID, callID: part.id, args: taskArgs },
687+
{
688+
tool: TaskTool.id,
689+
sessionID,
690+
messageID: assistantMessage.id,
691+
callID: part.id,
692+
args: taskArgs,
693+
agent: lastUser.agent,
694+
parentAgent: lastUser.parentAgent,
695+
},
649696
result,
650697
)
651698

@@ -726,6 +773,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
726773
time: { created: Date.now() },
727774
role: "user",
728775
agent: input.agent,
776+
parentAgent: input.parentAgent,
729777
model: { providerID: model.providerID, modelID: model.modelID },
730778
}
731779
yield* sessions.updateMessage(userMsg)
@@ -812,7 +860,14 @@ NOTE: At any point in time through this workflow you should feel free to ask the
812860
const cwd = ctx.directory
813861
const shellEnv = yield* plugin.trigger(
814862
"shell.env",
815-
{ cwd, sessionID: input.sessionID, callID: part.callID },
863+
{
864+
cwd,
865+
sessionID: input.sessionID,
866+
messageID: part.messageID,
867+
callID: part.callID,
868+
agent: input.agent,
869+
parentAgent: input.parentAgent,
870+
},
816871
{ env: {} },
817872
)
818873

@@ -1702,6 +1757,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
17021757
})
17031758
.optional(),
17041759
agent: z.string().optional(),
1760+
parentAgent: z.string().optional(),
17051761
noReply: z.boolean().optional(),
17061762
tools: z
17071763
.record(z.string(), z.boolean())
@@ -1783,6 +1839,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
17831839
sessionID: SessionID.zod,
17841840
messageID: MessageID.zod.optional(),
17851841
agent: z.string(),
1842+
parentAgent: z.string().optional(),
17861843
model: z
17871844
.object({
17881845
providerID: ProviderID.zod,

packages/opencode/src/tool/bash.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,14 @@ export const BashTool = Tool.defineEffect(
364364
const shellEnv = Effect.fn("BashTool.shellEnv")(function* (ctx: Tool.Context, cwd: string) {
365365
const extra = yield* plugin.trigger(
366366
"shell.env",
367-
{ cwd, sessionID: ctx.sessionID, callID: ctx.callID },
367+
{
368+
cwd,
369+
sessionID: ctx.sessionID,
370+
messageID: ctx.messageID,
371+
callID: ctx.callID,
372+
agent: ctx.agent,
373+
parentAgent: ctx.parentAgent,
374+
},
368375
{ env: {} },
369376
)
370377
return {

packages/opencode/src/tool/task.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export const TaskTool = Tool.defineEffect(
135135
providerID: model.providerID,
136136
},
137137
agent: next.name,
138+
parentAgent: ctx.agent,
138139
tools: {
139140
...(canTodo ? {} : { todowrite: false }),
140141
...(canTask ? {} : { task: false }),

packages/opencode/src/tool/tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export namespace Tool {
1818
sessionID: SessionID
1919
messageID: MessageID
2020
agent: string
21+
parentAgent?: string
2122
abort: AbortSignal
2223
callID?: string
2324
extra?: { [key: string]: any }
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { SessionPrompt } from "../../src/session/prompt"
3+
import { MessageV2 } from "../../src/session/message-v2"
4+
import { Identifier } from "../../src/id/id"
5+
6+
const SESSION_ID = Identifier.descending("session")
7+
const MESSAGE_ID = Identifier.ascending("message")
8+
9+
describe("parentAgent hook input", () => {
10+
test("PromptInput accepts parentAgent", () => {
11+
const input = SessionPrompt.PromptInput.parse({
12+
sessionID: SESSION_ID,
13+
agent: "scout",
14+
parentAgent: "coder",
15+
parts: [{ type: "text", text: "test" }],
16+
})
17+
expect(input.parentAgent).toBe("coder")
18+
})
19+
20+
test("PromptInput parentAgent is optional", () => {
21+
const input = SessionPrompt.PromptInput.parse({
22+
sessionID: SESSION_ID,
23+
agent: "scout",
24+
parts: [{ type: "text", text: "test" }],
25+
})
26+
expect(input.parentAgent).toBeUndefined()
27+
})
28+
29+
test("ShellInput accepts parentAgent", () => {
30+
const input = SessionPrompt.ShellInput.parse({
31+
sessionID: SESSION_ID,
32+
agent: "coder",
33+
parentAgent: "orchestrator",
34+
command: "ls",
35+
})
36+
expect(input.parentAgent).toBe("orchestrator")
37+
})
38+
39+
test("UserMessage stores parentAgent", () => {
40+
const msg = MessageV2.User.parse({
41+
id: MESSAGE_ID,
42+
sessionID: SESSION_ID,
43+
role: "user",
44+
time: { created: Date.now() },
45+
agent: "scout",
46+
parentAgent: "coder",
47+
model: { providerID: "anthropic", modelID: "claude-sonnet-4-6" },
48+
})
49+
expect(msg.parentAgent).toBe("coder")
50+
})
51+
52+
test("UserMessage parentAgent is optional", () => {
53+
const msg = MessageV2.User.parse({
54+
id: MESSAGE_ID,
55+
sessionID: SESSION_ID,
56+
role: "user",
57+
time: { created: Date.now() },
58+
agent: "coder",
59+
model: { providerID: "openai", modelID: "gpt-5.4" },
60+
})
61+
expect(msg.parentAgent).toBeUndefined()
62+
})
63+
})

packages/plugin/src/index.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,14 @@ export interface Hooks {
211211
* Modify parameters sent to LLM
212212
*/
213213
"chat.params"?: (
214-
input: { sessionID: string; agent: string; model: Model; provider: ProviderContext; message: UserMessage },
214+
input: {
215+
sessionID: string
216+
agent: string
217+
parentAgent?: string
218+
model: Model
219+
provider: ProviderContext
220+
message: UserMessage
221+
},
215222
output: {
216223
temperature: number
217224
topP: number
@@ -221,7 +228,14 @@ export interface Hooks {
221228
},
222229
) => Promise<void>
223230
"chat.headers"?: (
224-
input: { sessionID: string; agent: string; model: Model; provider: ProviderContext; message: UserMessage },
231+
input: {
232+
sessionID: string
233+
agent: string
234+
parentAgent?: string
235+
model: Model
236+
provider: ProviderContext
237+
message: UserMessage
238+
},
225239
output: { headers: Record<string, string> },
226240
) => Promise<void>
227241
"permission.ask"?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void>
@@ -230,15 +244,30 @@ export interface Hooks {
230244
output: { parts: Part[] },
231245
) => Promise<void>
232246
"tool.execute.before"?: (
233-
input: { tool: string; sessionID: string; callID: string },
247+
input: { tool: string; sessionID: string; messageID: string; callID: string; agent?: string; parentAgent?: string },
234248
output: { args: any },
235249
) => Promise<void>
236250
"shell.env"?: (
237-
input: { cwd: string; sessionID?: string; callID?: string },
251+
input: {
252+
cwd: string
253+
sessionID?: string
254+
messageID?: string
255+
callID?: string
256+
agent?: string
257+
parentAgent?: string
258+
},
238259
output: { env: Record<string, string> },
239260
) => Promise<void>
240261
"tool.execute.after"?: (
241-
input: { tool: string; sessionID: string; callID: string; args: any },
262+
input: {
263+
tool: string
264+
sessionID: string
265+
messageID: string
266+
callID: string
267+
args: any
268+
agent?: string
269+
parentAgent?: string
270+
},
242271
output: {
243272
title: string
244273
output: string

0 commit comments

Comments
 (0)