diff --git a/agents/dispatch/src/toolset.ts b/agents/dispatch/src/toolset.ts index f273a330e..7ec80a64a 100644 --- a/agents/dispatch/src/toolset.ts +++ b/agents/dispatch/src/toolset.ts @@ -21,6 +21,7 @@ export const tools = [ "resume_agent", "interrupt_agent", "send_input", + "approve_worker_call", "read_agent_trace", "search_agents", ] as const; diff --git a/src/agent/directors/tool-sets.test.ts b/src/agent/directors/tool-sets.test.ts index 156704451..f8010b542 100644 --- a/src/agent/directors/tool-sets.test.ts +++ b/src/agent/directors/tool-sets.test.ts @@ -92,6 +92,15 @@ describe("DISPATCH_TOOLS / ORCHESTRATOR_TOOLS", () => { ); }); + test("approve_worker_call sits beside send_input on both dispatch surfaces", () => { + for (const surface of [DISPATCH_TOOLS, ORCHESTRATOR_TOOLS] as const) { + const names = surface as readonly string[]; + const send = names.indexOf("send_input"); + expect(send).toBeGreaterThanOrEqual(0); + expect(names[send + 1]).toBe("approve_worker_call"); + } + }); + test("skill_search + use_skill mount on every worker surface, never ask_operator", () => { expect([...SKILL_TOOLS]).toEqual(["skill_search", "use_skill"]); for (const surface of [ diff --git a/src/agent/directors/tool-sets.ts b/src/agent/directors/tool-sets.ts index abe9fc31d..0189c8457 100644 --- a/src/agent/directors/tool-sets.ts +++ b/src/agent/directors/tool-sets.ts @@ -65,6 +65,7 @@ export const ORCHESTRATOR_TOOLS = [ "resume_agent", "interrupt_agent", "send_input", + "approve_worker_call", "read_agent_trace", ] as const; diff --git a/src/agent/prompt-sizes.test.ts b/src/agent/prompt-sizes.test.ts index 75c82386f..97d92c114 100644 --- a/src/agent/prompt-sizes.test.ts +++ b/src/agent/prompt-sizes.test.ts @@ -233,7 +233,11 @@ describe("director prompt size budget", () => { } } if (DIRECTOR_REGISTRY[directorId].spawn.maySpawn) { - for (const verb of ["spawn_agent", "send_input"]) { + for (const verb of [ + "spawn_agent", + "send_input", + "approve_worker_call", + ]) { expect( names.filter((n) => n === verb).length, `${directorId} [${family}] mounts ${verb} once`, diff --git a/src/agent/prompt-sizes.ts b/src/agent/prompt-sizes.ts index fe505b83a..a762d1849 100644 --- a/src/agent/prompt-sizes.ts +++ b/src/agent/prompt-sizes.ts @@ -126,6 +126,7 @@ export function canonicalToolNamesForDirector( "resume_agent", "interrupt_agent", "send_input", + "approve_worker_call", ); } const dupe = names.find((name, index) => names.indexOf(name) !== index); diff --git a/src/agent/tool-search.ts b/src/agent/tool-search.ts index c9486db94..8b0d19295 100644 --- a/src/agent/tool-search.ts +++ b/src/agent/tool-search.ts @@ -51,6 +51,7 @@ export const CORE_TOOL_NAMES: readonly string[] = [ "resume_agent", "interrupt_agent", "send_input", + "approve_worker_call", ]; const ORCHESTRATOR_ONLY_TOOL_NAMES: readonly string[] = [ @@ -62,6 +63,7 @@ const ORCHESTRATOR_ONLY_TOOL_NAMES: readonly string[] = [ "resume_agent", "interrupt_agent", "send_input", + "approve_worker_call", ]; // Session-start facts gating a core tool's advertisement, fixed for the diff --git a/src/exec/runner.test.ts b/src/exec/runner.test.ts index de85c49d1..23701272a 100644 --- a/src/exec/runner.test.ts +++ b/src/exec/runner.test.ts @@ -750,6 +750,68 @@ describe("exec advertised tools vs TUI", () => { }); expect(isAdvertised("ask_operator")).toBe(true); }); + + test("parent prefix and dispatch call gate list approve_worker_call after send_input", () => { + const stub = (name: string) => ({ + name, + description: name, + inputSchema: { type: "object" as const, properties: {} }, + }); + const expectBesideSendInput = (names: readonly string[]) => { + const send = names.indexOf("send_input"); + expect(send).toBeGreaterThanOrEqual(0); + expect(names[send + 1]).toBe("approve_worker_call"); + }; + const registry = [ + stub("read_file"), + stub("approve_worker_call"), + stub("send_input"), + ]; + for (const toolAvailability of [ + { + languageServerAvailable: false, + operatorAvailable: true, + waitAgentsMounted: false, + }, + { + languageServerAvailable: false, + operatorAvailable: false, + waitAgentsMounted: true, + }, + ] as const) { + expectBesideSendInput( + advertisedToolNamesForSessionMode(sessionMode, toolAvailability), + ); + const tui = createAdvertisedToolset({ + sessionMode, + toolAvailability, + getProvider: () => ({ providerName: "test", model: "test" }), + }); + expectBesideSendInput( + tui.computeAdvertised(registry).map((def) => def.name), + ); + expect( + createExecToolCallGate(tui.isAdvertised)("approve_worker_call"), + ).toBe(true); + } + const overlay = resolveExecDirectorOverlay("dispatch"); + const exec = createAdvertisedToolset({ + sessionMode, + toolAvailability: { + languageServerAvailable: false, + operatorAvailable: false, + waitAgentsMounted: true, + }, + getProvider: () => ({ providerName: "test", model: "test" }), + builtInPrefix: overlay.advertisedAllow, + }); + expectBesideSendInput( + exec.computeAdvertised(registry).map((def) => def.name), + ); + expect( + createExecToolCallGate(exec.isAdvertised)("approve_worker_call"), + ).toBe(true); + }); }); describe("exec tool call gate and promoter", () => {