diff --git a/packages/client-runtime/src/state/subagentRuntime.test.ts b/packages/client-runtime/src/state/subagentRuntime.test.ts index f87531f15808..4a8b4dcd595f 100644 --- a/packages/client-runtime/src/state/subagentRuntime.test.ts +++ b/packages/client-runtime/src/state/subagentRuntime.test.ts @@ -426,6 +426,62 @@ describe("deriveAgentPanelModel", () => { expect(model.liveCount).toBe(0); }); + it("counts a running workflow coordinator between phases", () => { + const betweenPhases = fold([ + activity("task.started", { + taskId: "wf-between-phases", + taskType: "local_workflow", + title: "review", + }), + activity("task.progress", { + taskId: "wf-between-phases:wf:0", + title: "review:a", + status: "completed", + parentAgentId: "wf-between-phases", + agentIndex: 0, + phaseIndex: 0, + }), + activity("task.completed", { + taskId: "wf-between-phases:wf:0", + status: "completed", + parentAgentId: "wf-between-phases", + }), + ]); + + const model = deriveAgentPanelModel({ agents: betweenPhases }); + + expect(model.runningCount).toBe(1); + expect(model.settledCount).toBe(1); + expect(model.liveCount).toBe(1); + expect(model.runningCount + model.waitingCount + model.idleCount + model.settledCount).toBe( + betweenPhases.length, + ); + }); + + it("does not count a workflow coordinator while a member is idle", () => { + const idleMember = fold([ + activity("task.started", { + taskId: "wf-idle-member", + taskType: "local_workflow", + title: "review", + }), + activity("task.progress", { + taskId: "wf-idle-member:wf:0", + title: "review:a", + status: "idle", + parentAgentId: "wf-idle-member", + agentIndex: 0, + phaseIndex: 0, + }), + ]); + + const model = deriveAgentPanelModel({ agents: idleMember }); + + expect(model.runningCount).toBe(0); + expect(model.idleCount).toBe(1); + expect(model.liveCount).toBe(0); + }); + it("keeps direct spawns in first-seen order as their activity changes", () => { const directRoster = fold([ activity("task.started", { taskId: "direct-a", title: "First" }, "2026-08-01T11:00:00.000Z"), diff --git a/packages/client-runtime/src/state/subagentRuntime.ts b/packages/client-runtime/src/state/subagentRuntime.ts index c1ea1cc2b15d..2d26ba10d0d0 100644 --- a/packages/client-runtime/src/state/subagentRuntime.ts +++ b/packages/client-runtime/src/state/subagentRuntime.ts @@ -826,16 +826,27 @@ export function deriveAgentPanelModel({ let settledCount = 0; let totalTokens = 0; for (const agent of source) { - // A workflow coordinator with members is a container for those members, not - // work of its own: it reports running for the whole run and aggregates their - // usage upstream in some providers. Counting it would report one more agent - // working than there are, and double count tokens. - if (agent.kind === "workflow" && (members.get(agent.id) ?? []).length > 0) continue; - if (agent.status === "running" || agent.status === "pending") runningCount += 1; - else if (agent.status === "waiting") waitingCount += 1; - else if (agent.status === "idle") idleCount += 1; - else settledCount += 1; - totalTokens += agent.usage?.totalTokens ?? 0; + const workflowMembers = members.get(agent.id) ?? []; + const standsInForMembers = + agent.kind === "workflow" && + workflowMembers.length > 0 && + !( + isActiveSubagentStatus(agent.status) && + workflowMembers.every((member) => isTerminalSubagentStatus(member.status)) + ); + + if (!standsInForMembers) { + if (agent.status === "running" || agent.status === "pending") runningCount += 1; + else if (agent.status === "waiting") waitingCount += 1; + else if (agent.status === "idle") idleCount += 1; + else settledCount += 1; + } + + // Workflow usage may aggregate member usage upstream, including while the + // coordinator is the only live unit between phases. + if (agent.kind !== "workflow" || workflowMembers.length === 0) { + totalTokens += agent.usage?.totalTokens ?? 0; + } } return {