/** * subagents-nested-print-mode-e2e.test.ts — remaining real-session coverage for * opt-in nested delegation after #164 landed. * * `test/nested-delegation-e2e.test.ts` already pins the happy path (tool * admission + two-hop foreground return + background poll/transcript). This * file covers the production-boundary cases that suite still leaves open: * default-off injection, depth-cap tool stripping, background parent holds * while a child nests, and cross-parent ownership denial against the published * root manager lifecycle. * * Extracted from codesoda/pi-subagents#2 (test-only follow-up to #164) and * reconciled with the merged #164 frontmatter contract (`allowed_subagents` * opt-in; no nested tools injected at the depth cap). */ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Context, ToolCall } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it, vi } from "vitest"; import { registerAgents } from "../src/agent-types.js"; import { loadCustomAgents } from "../src/custom-agents.js"; import { agentCall, type FauxResponder, type PrintModeRun, runPrintMode, } from "./helpers/print-mode-runner.js"; vi.setConfig({ testTimeout: 30_000 }); const NESTED_TOOLS = ["Agent", "get_subagent_result", "steer_subagent"]; function userPrompt(ctx: Context): string { for (const message of ctx.messages) { if (message.role !== "user") continue; const content = (message as { content?: unknown }).content; if (typeof content === "string") return content; if (Array.isArray(content)) { const text = content.find( (block: { type?: string; text?: string }) => block.type === "text", ) as { text?: string } | undefined; if (text?.text) return text.text; } } return ""; } function tools(ctx: Context): string[] { return (ctx.tools ?? []).map((tool) => tool.name); } function toolResults(ctx: Context, name: string): string[] { return ctx.messages.flatMap((message) => { if ( message.role !== "toolResult" || (message as { toolName?: string }).toolName !== name ) { return []; } const content = (message as { content?: unknown }).content; if (!Array.isArray(content)) return []; return [ content .map((block: { type?: string; text?: string }) => block.type === "text" ? (block.text ?? "") : "", ) .join(""), ]; }); } function nestedToolsIn(toolNames: string[] | undefined): string[] { return (toolNames ?? []).filter((name) => NESTED_TOOLS.includes(name)); } async function waitForChildReady( ready: Promise, timeoutMs: number, ): Promise { let timer: ReturnType | undefined; try { await Promise.race([ ready, new Promise((_, reject) => { timer = setTimeout( () => reject( new Error( `Nested child did not enter its deferred response within ${timeoutMs}ms`, ), ), timeoutMs, ); }), ]); } finally { if (timer) clearTimeout(timer); } } function lastToolResult(ctx: Context, name: string): string { const results = toolResults(ctx, name); return results[results.length - 1] ?? ""; } function toolCall( name: string, args: Record, id: string, ): ToolCall { return { type: "toolCall", id, name, arguments: args } as ToolCall; } function writeAgents(cwd: string, agents: Record): void { const dir = join(cwd, ".pi", "agents"); mkdirSync(dir, { recursive: true }); for (const [name, frontmatter] of Object.entries(agents)) { writeFileSync( join(dir, `${name}.md`), `---\ndescription: ${name}\n${frontmatter}---\n${name} agent\n`, ); } } async function runWithAgents( agents: Record, respond: FauxResponder, options: { prompt: string; maxModelCalls?: number; hold?: boolean } = { prompt: "root", }, ): Promise<{ run: PrintModeRun; cwd: string }> { const cwd = mkdtempSync(join(tmpdir(), "subagents-nested-e2e-")); writeAgents(cwd, agents); const run = await runPrintMode({ ...options, cwd, respond, // Pinned faux: every case here scripts exact tool calls, so the pre-publish // smoke's global `PI_E2E_LIVE=1` must not swap a real model in. live: false, beforeRun: () => registerAgents(loadCustomAgents(cwd)), }); return { run, cwd }; } describe("PR #164 nested agents through the real print-mode boundary", () => { let run: PrintModeRun | undefined; let cwd: string | undefined; afterEach(async () => { await run?.dispose(); run = undefined; if (cwd) rmSync(cwd, { recursive: true, force: true }); cwd = undefined; }); it("does not inject nested orchestration tools into a custom agent by default", async () => { const observed = new Map(); ({ run, cwd } = await runWithAgents( { plain: "" }, (ctx) => { const route = userPrompt(ctx); if (route === "plain-child") { observed.set(route, tools(ctx)); return "PLAIN_CHILD_RESULT"; } if (toolResults(ctx, "Agent").length === 0) { return agentCall({ subagent_type: "plain", description: "plain child", prompt: "plain-child", run_in_background: false, }); } return lastToolResult(ctx, "Agent"); }, { prompt: "root-default" }, )); expect(run.responseText).toContain("PLAIN_CHILD_RESULT"); expect(observed.get("plain-child")).toBeDefined(); expect(nestedToolsIn(observed.get("plain-child"))).toEqual([]); }); it("strips nested tools at the depth cap instead of injecting always-failing ones", async () => { // Default maxSubagentDepth is 2: main(0) → level_one(1) → level_two(2). // #164 injects nested tools only while depth < max, so the agent at the cap // never sees Agent/get/steer even when it opts in via allowed_subagents. const observed = new Map(); ({ run, cwd } = await runWithAgents( { level_one: "allowed_subagents: level_two\n", level_two: "allowed_subagents: level_three\n", level_three: "", }, (ctx) => { const route = userPrompt(ctx); observed.set(route, tools(ctx)); if (route === "level_three-child") return "UNEXPECTED_LEVEL_THREE"; if (route === "level_two-child") { const nested = nestedToolsIn(tools(ctx)); // Cap agents must complete directly — they have no nested tools. return `AT_CAP tools=${nested.length === 0 ? "none" : nested.join(",")}`; } if (route === "level_one-child") { if (toolResults(ctx, "Agent").length === 0) { return agentCall({ subagent_type: "level_two", description: "allowed level", prompt: "level_two-child", run_in_background: false, }); } return lastToolResult(ctx, "Agent"); } if (toolResults(ctx, "Agent").length === 0) { return agentCall({ subagent_type: "level_one", description: "recursive chain", prompt: "level_one-child", run_in_background: false, }); } return lastToolResult(ctx, "Agent"); }, { prompt: "root-depth", maxModelCalls: 24 }, )); expect(run.responseText).toContain("AT_CAP tools=none"); expect(observed.get("level_one-child")).toEqual( expect.arrayContaining(NESTED_TOOLS), ); expect(nestedToolsIn(observed.get("level_two-child"))).toEqual([]); expect(observed.has("level_three-child")).toBe(false); }); it("holds a background child while it performs real nested delegation", async () => { const calls = new Map(); ({ run, cwd } = await runWithAgents( { background_delegator: "allowed_subagents: background_grandchild\n", background_grandchild: "", }, async (ctx) => { const route = userPrompt(ctx); calls.set(route, (calls.get(route) ?? 0) + 1); if (route === "background-grandchild-child") { await new Promise((resolve) => setTimeout(resolve, 50)); return "BACKGROUND_NESTED_RESULT"; } if (route === "background-delegator-child") { if (toolResults(ctx, "Agent").length === 0) { return agentCall({ subagent_type: "background_grandchild", description: "nested foreground work", prompt: "background-grandchild-child", run_in_background: false, }); } return lastToolResult(ctx, "Agent"); } const agents = toolResults(ctx, "Agent"); if (agents.length === 0) { return agentCall({ subagent_type: "background_delegator", description: "background nested work", prompt: "background-delegator-child", run_in_background: true, }); } if (toolResults(ctx, "get_subagent_result").length === 0) { const id = agents[0].match(/Agent ID: ([^\s]+)/)?.[1]; if (!id) throw new Error(`No background agent ID in: ${agents[0]}`); return toolCall( "get_subagent_result", { agent_id: id, wait: true }, "get-background-result", ); } return lastToolResult(ctx, "get_subagent_result"); }, { prompt: "root-background", maxModelCalls: 24 }, )); expect(run.responseText).toContain("BACKGROUND_NESTED_RESULT"); expect(calls.get("background-delegator-child")).toBe(2); expect(calls.get("background-grandchild-child")).toBe(1); expect( run.parentSession.messages.some( (message) => message.role === "assistant" && message.content.some( (block) => block.type === "toolCall" && block.name === "get_subagent_result", ), ), ).toBe(true); }); it("keeps nested get and steer ownership-scoped to the calling parent", async () => { // #164 aborts nested children when their owner settles, so the owner must // stay running while a foreign peer probes the live nested record. Spawn // both owner and probe as top-level background agents (hold:false so the // root can continue); the nested tools themselves enforce ownership. const probeResults: { get?: string; steer?: string } = {}; let ownedChildBlocked = false; let releaseOwnedChild = () => {}; let releaseOwner = () => {}; let ownedChildEntered!: () => void; let ownerHolding!: () => void; let nestedId = ""; const ownedChildReady = new Promise((resolve) => { ownedChildEntered = resolve; }); const ownedChildRelease = new Promise((resolve) => { releaseOwnedChild = resolve; }); const ownerHold = new Promise((resolve) => { releaseOwner = resolve; }); const ownerHoldingReady = new Promise((resolve) => { ownerHolding = resolve; }); let waitForAll: Promise | undefined; try { ({ run, cwd } = await runWithAgents( { owner: "allowed_subagents: owned_child\n", // Unrestricted nested allowlist — still ownership-scoped at runtime. probe: "allowed_subagents: all\n", owned_child: "", }, async (ctx) => { const route = userPrompt(ctx); if (route === "owned-child") { ownedChildBlocked = true; ownedChildEntered(); await ownedChildRelease; return "OWNED_NESTED_RESULT"; } if (route === "owner-child") { const agents = toolResults(ctx, "Agent"); if (agents.length === 0) { return agentCall({ subagent_type: "owned_child", description: "owned nested child", prompt: "owned-child", run_in_background: true, }); } const id = agents[0].match(/Agent ID: ([^\s]+)/)?.[1]; if (!id) throw new Error(`No nested ID in: ${agents[0]}`); nestedId = id; ownerHolding(); // Stay running so #164 does not abortOwnedChildren(owned_child). await ownerHold; return `OWNER_NESTED_ID:${id}`; } if (route === "probe-child") { await ownedChildReady; // Wait until the owner has published the nested id. await waitForChildReady(ownerHoldingReady, 5_000); const id = nestedId; if (!id) throw new Error("Owner never published a nested agent id"); const gets = toolResults(ctx, "get_subagent_result"); const steers = toolResults(ctx, "steer_subagent"); if (gets.length === 0) { return toolCall( "get_subagent_result", { agent_id: id }, "foreign-get", ); } if (steers.length === 0) { return toolCall( "steer_subagent", { agent_id: id, message: "foreign guidance" }, "foreign-steer", ); } probeResults.get = gets.at(-1); probeResults.steer = steers.at(-1); return "OWNERSHIP_PROBE_DONE"; } const agents = toolResults(ctx, "Agent"); if (agents.length === 0) { return agentCall({ subagent_type: "owner", description: "nested record owner", prompt: "owner-child", run_in_background: true, }); } if (agents.length === 1) { // Fire probe without waiting for the background owner to finish. return agentCall({ subagent_type: "probe", description: "foreign ownership probe", prompt: "probe-child", run_in_background: true, }); } const results = toolResults(ctx, "get_subagent_result"); if (results.length === 0) { // Poll the probe (second background agent) for the denial outcome. const probeSpawn = agents[1]; const probeId = probeSpawn.match(/Agent ID: ([^\s]+)/)?.[1]; if (!probeId) throw new Error(`No probe ID in: ${probeSpawn}`); return toolCall( "get_subagent_result", { agent_id: probeId, wait: true }, "await-probe", ); } return lastToolResult(ctx, "get_subagent_result"); }, { prompt: "root-ownership", maxModelCalls: 32, hold: false }, )); await waitForChildReady(ownedChildReady, 5_000); expect(ownedChildBlocked).toBe(true); expect(run.manager).toBeDefined(); const manager = run.manager; if (!manager) throw new Error("Print-mode manager was not published"); // Owner (and its nested child) are still live — root finished without hold. expect(manager.hasRunning()).toBe(true); waitForAll = manager.waitForAll(); let waitTimer: ReturnType | undefined; const waitState = await Promise.race([ waitForAll.then(() => "settled" as const), new Promise<"pending">((resolve) => { waitTimer = setTimeout(() => resolve("pending"), 50); }), ]); if (waitTimer) clearTimeout(waitTimer); expect(waitState).toBe("pending"); expect(run.responseText).toContain("OWNERSHIP_PROBE_DONE"); expect(probeResults.get).toBeDefined(); expect(probeResults.steer).toBeDefined(); expect(probeResults.get).toMatch( /^Nested agent not found or not owned by this parent:/i, ); expect(probeResults.steer).toMatch( /^Running nested agent not found or not owned by this parent:/i, ); expect(probeResults.get).not.toContain("OWNED_NESTED_RESULT"); expect(probeResults.steer).not.toContain("Steering message sent"); releaseOwnedChild(); releaseOwner(); await expect(waitForAll).resolves.toBeUndefined(); expect(manager.hasRunning()).toBe(false); } finally { releaseOwnedChild(); releaseOwner(); if (waitForAll) { await Promise.race([ waitForAll, new Promise((resolve) => setTimeout(resolve, 5_000)), ]); } } }); });