import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { createChildHooks } from "../../src/runs/shared/child-hooks.ts"; import { buildInProcessChildLaunch } from "../../src/runs/shared/child-launch.ts"; import { childSupervisorMetadata, evaluateChildToolDiagnostic, type ChildRuntimeConfig } from "../../src/runs/shared/child-runtime-config.ts"; function baseConfig(overrides: Partial = {}): ChildRuntimeConfig { return { fanoutChild: false, depth: 1, waitTool: { enabled: true }, fast: false, ...overrides }; } interface FakePi { handlers: Map unknown>>; tools: Array<{ name: string }>; api: unknown; } function fakePi(available: string[]): FakePi { const handlers = new Map unknown>>(); const tools: Array<{ name: string }> = []; const api = { on(event: string, handler: (event?: unknown, ctx?: unknown) => unknown) { handlers.set(event, [...(handlers.get(event) ?? []), handler]); }, registerTool(tool: { name: string }) { tools.push(tool); }, getAllTools: () => [...available, ...tools.map((tool) => tool.name)].map((name) => ({ name })), events: { on() {}, emit() {} }, sendMessage() {}, getThinkingLevel: () => "off", }; return { handlers, tools, api }; } describe("child runtime config", () => { it("creates the prompt runtime hook always and the fast and fanout hooks on demand", () => { assert.deepEqual(createChildHooks(baseConfig()).map((hook) => hook.name), ["pi-subagents:prompt-runtime"]); assert.deepEqual(createChildHooks(baseConfig({ fast: true })).map((hook) => hook.name), ["pi-subagents:prompt-runtime", "pi-subagents:fast-mode"]); assert.deepEqual(createChildHooks(baseConfig({ fanoutChild: true })).map((hook) => hook.name), ["pi-subagents:prompt-runtime", "pi-subagents:fanout-child"]); }); it("provides the coordinator reply tool before agent_start without granting it to leaves", async () => { for (const tools of [["subagent", "contact_supervisor", "subagent_supervisor"], ["subagent", "contact_supervisor"], ["contact_supervisor"]]) { const launch = buildInProcessChildLaunch({ host: "parent", cwd: process.cwd(), childAgentName: "coordinator", childIndex: 0, sessionEnabled: false, tools, runId: "registration", parentSessionId: "root-A", inheritProjectContext: false, inheritGlobalContext: false, inheritSkills: false, }); const pi = fakePi([]); for (const hook of launch.session.hooks) hook.factory(pi.api as never); const ctx = { sessionManager: { getSessionId: () => "coordinator-B", getSessionFile: () => "/sessions/B.jsonl" } }; try { for (const handler of pi.handlers.get("session_start") ?? []) await handler({}, ctx); assert.equal(pi.tools.some(tool => tool.name === "subagent_supervisor"), tools.includes("subagent")); for (const handler of pi.handlers.get("agent_start") ?? []) await handler({}, ctx); const selected = pi.tools.map(tool => tool.name).filter(name => launch.session.tools?.includes(name)); assert.equal(selected.includes("subagent_supervisor"), tools.includes("subagent_supervisor")); assert.deepEqual(launch.session.tools, tools, "registration must not expand explicit tools"); } finally { for (const handler of pi.handlers.get("session_shutdown") ?? []) await handler({}, ctx); } } }); it("hooks read the config object", async () => { const diagnostics: unknown[] = []; const captured: unknown[] = []; const config = baseConfig({ agent: "config-agent", requiredTools: ["read", "fixture_search"], toolDiagnostic: (diagnostic) => diagnostics.push(diagnostic), structuredOutput: { schema: { type: "object" }, capture: (value) => captured.push(value) }, inheritProjectContext: true, inheritGlobalContext: true, inheritSkills: true, }); const pi = fakePi(["read"]); for (const hook of createChildHooks(config)) hook.factory(pi.api as never); assert.throws(() => pi.handlers.get("agent_start")?.[0]?.({}), /Agent 'config-agent' requested unavailable child tools: fixture_search/); assert.deepEqual(diagnostics, [{ agent: "config-agent", required: ["read", "fixture_search"], available: ["read", "bg_wait", "subagent_wait", "structured_output"], missing: ["fixture_search"] }]); const structured = pi.tools.find((tool) => tool.name === "structured_output") as { execute: (id: string, params: { value: unknown }) => Promise } | undefined; assert.ok(structured, "structured_output tool registered from config"); await structured.execute("call-1", { value: { done: true } }); assert.deepEqual(captured, [{ done: true }]); const rewritten = await pi.handlers.get("before_agent_start")?.[0]?.({ systemPrompt: "base prompt" }) as { systemPrompt: string } | undefined; assert.match(rewritten?.systemPrompt ?? "", /strict structured output contract/); }); it("evaluates the tool diagnostic against the available tools", () => { assert.equal(evaluateChildToolDiagnostic(baseConfig(), ["read"]), undefined); assert.equal(evaluateChildToolDiagnostic(baseConfig({ requiredTools: ["read"] }), ["read", "write"]), undefined); assert.deepEqual( evaluateChildToolDiagnostic(baseConfig({ agent: "worker", requiredTools: ["read", "mcp_search"], mcpDirectTools: ["mcp_search"] }), ["read"]), { agent: "worker", required: ["read", "mcp_search"], available: ["read"], missing: ["mcp_search"], missingMcpDirectTools: ["mcp_search"] }, ); }); it("derives supervisor metadata only when the channel, run, agent, index, and orchestrator session are all set", () => { assert.equal(childSupervisorMetadata(baseConfig({ supervisorChannelDir: "/tmp/channel", runId: "run-1", agent: "worker" })), undefined); assert.deepEqual( childSupervisorMetadata(baseConfig({ supervisorChannelDir: "/tmp/channel", runId: "run-1", agent: "worker", childIndex: 2, orchestratorSessionId: "session-1", orchestratorTarget: "orchestrator", intercomSessionName: "child-target", })), { channelDir: "/tmp/channel", runId: "run-1", agent: "worker", childIndex: 2, orchestratorTarget: "orchestrator", orchestratorSessionId: "session-1", childTarget: "child-target" }, ); }); });