Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions src/subagent/agent-fleet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,21 @@ async function callToolRaw(
};
}

function parseFleetJson(content: string): Record<string, unknown> {
expect(content).toContain("\n");
const parsed = JSON.parse(content) as Record<string, unknown>;
expect(JSON.stringify(parsed, null, 2)).toBe(content);
return parsed;
}

async function callTool(
tool:
| ReturnType<typeof createSpawnAgentTool>
| ReturnType<typeof createWaitAgentsTool>,
args: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const { content } = await callToolRaw(tool, args);
return JSON.parse(content);
return parseFleetJson(content);
}

describe("spawn_agent", () => {
Expand Down Expand Up @@ -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([]);
Expand Down Expand Up @@ -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<RunSubAgentResult>();
const deps = makeDeps(async () => gate.promise);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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",
);
Expand Down
18 changes: 8 additions & 10 deletions src/subagent/agent-fleet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 }));
},
});
}
Expand Down Expand Up @@ -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 }),
);
}

Expand Down Expand Up @@ -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 }));
},
});
}
Expand Down Expand Up @@ -1678,7 +1676,7 @@ export function createListAgentsTool(deps: WaitAgentsDeps): AgentTool {
: {}),
};
});
return fleetResult(call.id, JSON.stringify({ agents }));
return fleetResult(call.id, fleetJson({ agents }));
},
});
}
10 changes: 9 additions & 1 deletion src/subagent/lifecycle-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ import {
import { createAdmissionQueue } from "./admission.js";
import { defined } from "../../tests/helpers/defined.js";

function parseFleetJson(content: string): Record<string, unknown> {
expect(content).toContain("\n");
const parsed = JSON.parse(content) as Record<string, unknown>;
expect(JSON.stringify(parsed, null, 2)).toBe(content);
return parsed;
}

async function callTool(
tool:
| ReturnType<typeof createCloseAgentTool>
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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(
Expand Down
14 changes: 9 additions & 5 deletions src/subagent/lifecycle-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
});
Expand Down Expand Up @@ -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,
}),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 }),
);
},
});
Expand Down Expand Up @@ -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,
}),
Expand Down Expand Up @@ -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 }),
);
},
});
Expand Down
Loading