diff --git a/src/subagent/agent-fleet.test.ts b/src/subagent/agent-fleet.test.ts index be2dc4d55..43a3c75e6 100644 --- a/src/subagent/agent-fleet.test.ts +++ b/src/subagent/agent-fleet.test.ts @@ -135,6 +135,13 @@ async function callToolRaw( }; } +function parseFleetJson(content: string): Record { + expect(content).toContain("\n"); + const parsed = JSON.parse(content) as Record; + expect(JSON.stringify(parsed, null, 2)).toBe(content); + return parsed; +} + async function callTool( tool: | ReturnType @@ -142,7 +149,7 @@ async function callTool( args: Record, ): Promise> { const { content } = await callToolRaw(tool, args); - return JSON.parse(content); + return parseFleetJson(content); } describe("spawn_agent", () => { @@ -196,6 +203,7 @@ describe("spawn_agent", () => { }); expect(result.isError).toBe(true); + expect(result.content.startsWith("Error:")).toBe(true); expect(result.content).toContain("profile orchestrators are not supported"); expect(runCalled).toBe(false); expect(deps.sessions.list()).toEqual([]); @@ -291,6 +299,17 @@ describe("spawn_agent + wait_agents", () => { defined(gates[2]).resolve({ report: "third" }); }); + test("wait_agents with no uncollected agents returns empty pretty-printed results", async () => { + const deps = makeDeps(async () => ({ report: "unused" })); + const wait = createWaitAgentsTool({ + sessions: deps.sessions, + fleetRecords: deps.fleetRecords, + }); + const { content } = await callToolRaw(wait, { timeout_ms: 50 }); + const parsed = parseFleetJson(content); + expect(parsed).toEqual({ results: [], timed_out: false }); + }); + test("wait_agents times out on a still-running agent without cancelling it, and can be called again", async () => { const gate = deferred(); const deps = makeDeps(async () => gate.promise); @@ -1975,7 +1994,7 @@ describe("list_agents", () => { typeof raw.content === "string" ? raw.content : JSON.stringify(raw.content); - const parsed = JSON.parse(content) as { + const parsed = parseFleetJson(content) as { agents: { agent_id: string; status: string; @@ -2893,7 +2912,15 @@ describe("admission queue", () => { }, new AbortController().signal, ); - expect(raw.content).toContain('"status":"interrupted"'); + const interrupted = parseFleetJson( + typeof raw.content === "string" + ? raw.content + : JSON.stringify(raw.content), + ); + expect(interrupted).toEqual({ + agent_id: result.agent_id, + status: "interrupted", + }); expect(deps.sessions.get(result.agent_id as string)?.lifecycleStatus).toBe( "interrupted", ); diff --git a/src/subagent/agent-fleet.ts b/src/subagent/agent-fleet.ts index c401912d5..5ca457fe2 100644 --- a/src/subagent/agent-fleet.ts +++ b/src/subagent/agent-fleet.ts @@ -619,6 +619,10 @@ function fleetResult(callId: string, content: string): ToolResult { return { callId, content, ...(isError ? { isError: true } : {}) }; } +function fleetJson(value: unknown): string { + return JSON.stringify(value, null, 2); +} + /** Resolve agent=/intent= to a closed director. */ export function resolveDirectorDispatch( agentId: string | undefined, @@ -1450,10 +1454,7 @@ export function createSpawnAgentTool(deps: AgentFleetDeps): AgentTool { start, }); if (status === "queued") deps.fleetRecords.markQueued(session.id); - return fleetResult( - call.id, - JSON.stringify({ agent_id: session.id, status }), - ); + return fleetResult(call.id, fleetJson({ agent_id: session.id, status })); }, }); } @@ -1551,7 +1552,7 @@ export function createWaitAgentsTool(deps: WaitAgentsDeps): AgentTool { if (targets.length === 0) { return fleetResult( call.id, - JSON.stringify({ results: [], timed_out: false }), + fleetJson({ results: [], timed_out: false }), ); } @@ -1627,10 +1628,7 @@ export function createWaitAgentsTool(deps: WaitAgentsDeps): AgentTool { }; }); - return fleetResult( - call.id, - JSON.stringify({ results, timed_out: timedOut }), - ); + return fleetResult(call.id, fleetJson({ results, timed_out: timedOut })); }, }); } @@ -1678,7 +1676,7 @@ export function createListAgentsTool(deps: WaitAgentsDeps): AgentTool { : {}), }; }); - return fleetResult(call.id, JSON.stringify({ agents })); + return fleetResult(call.id, fleetJson({ agents })); }, }); } diff --git a/src/subagent/lifecycle-tools.test.ts b/src/subagent/lifecycle-tools.test.ts index 9fc60234f..d70b38ad1 100644 --- a/src/subagent/lifecycle-tools.test.ts +++ b/src/subagent/lifecycle-tools.test.ts @@ -15,6 +15,13 @@ import { import { createAdmissionQueue } from "./admission.js"; import { defined } from "../../tests/helpers/defined.js"; +function parseFleetJson(content: string): Record { + expect(content).toContain("\n"); + const parsed = JSON.parse(content) as Record; + expect(JSON.stringify(parsed, null, 2)).toBe(content); + return parsed; +} + async function callTool( tool: | ReturnType @@ -38,7 +45,7 @@ async function callTool( typeof result.content === "string" ? result.content : JSON.stringify(result.content); - return JSON.parse(content); + return parseFleetJson(content); } describe("close_agent", () => { @@ -659,6 +666,7 @@ describe("resume_agent", () => { new AbortController().signal, ); expect(empty.isError).toBe(true); + expect(String(empty.content).startsWith("Error:")).toBe(true); expect(String(empty.content)).toContain("non-empty message"); const oversize = await resumeAgent.handler( diff --git a/src/subagent/lifecycle-tools.ts b/src/subagent/lifecycle-tools.ts index c2efb957c..6d38dd3eb 100644 --- a/src/subagent/lifecycle-tools.ts +++ b/src/subagent/lifecycle-tools.ts @@ -32,6 +32,10 @@ function lifecycleResult(callId: string, content: string): ToolResult { return { callId, content, ...(isError ? { isError: true } : {}) }; } +function fleetJson(value: unknown): string { + return JSON.stringify(value, null, 2); +} + const CloseAgentArgs = type({ target: "string", }); @@ -185,7 +189,7 @@ export function createCloseAgentTool(deps: CloseAgentToolDeps): AgentTool { if (deps.sessions.get(target) === undefined) { return lifecycleResult( call.id, - JSON.stringify({ + fleetJson({ agent_id: target, status: "not_found" satisfies AgentLifecycleStatus, }), @@ -228,7 +232,7 @@ export function createCloseAgentTool(deps: CloseAgentToolDeps): AgentTool { const own = closed.find((c) => c.agent_id === target); return lifecycleResult( call.id, - JSON.stringify({ + fleetJson({ agent_id: target, status: own?.status ?? "shutdown", closed, @@ -299,7 +303,7 @@ export function createResumeAgentTool(deps: ResumeAgentToolDeps): AgentTool { } return lifecycleResult( call.id, - JSON.stringify({ agent_id: target, status: outcome.status }), + fleetJson({ agent_id: target, status: outcome.status }), ); }, }); @@ -361,7 +365,7 @@ export function createInterruptAgentTool( deps.fleetRecords.interrupt(target); return lifecycleResult( call.id, - JSON.stringify({ + fleetJson({ agent_id: target, status: "interrupted" satisfies AgentLifecycleStatus, }), @@ -474,7 +478,7 @@ export function createSendInputTool(deps: LifecycleToolDeps): AgentTool { } return lifecycleResult( call.id, - JSON.stringify({ agent_id: target, status: outcome.status }), + fleetJson({ agent_id: target, status: outcome.status }), ); }, });