| author | |
| committer | |
| log | 00f8b7c28056188e3c5630160806a0afe51c9010 |
| tree | 32dd9a8efefcfaa6a74a69ee06445ec5b96b7826 |
| parent | f1e90e388b86fe4b007a55c0e685a1fa878115e6 |
| signature | Signed by PGP key B5690EEEBB952194 |
11 files changed, 200 insertions(+), 52 deletions(-)
apps/mobile/src/lib/threadActivity.test.ts+15-11| ... | ... | @@ -2256,9 +2256,13 @@ describe("quiet timeline: nested agents", () => { |
| 2256 | 2256 | }, |
| 2257 | 2257 | ); |
| 2258 | 2258 | |
| 2259 | it.each(["cancelled", "failed", "interrupted"] as const)( | |
| 2260 | "replaces Antigravity progress with %s without a timeline bypass flag", | |
| 2259 | it.each(["cancelled", "failed", "interrupted", "idle"] as const)( | |
| 2260 | "replaces Antigravity batch progress with %s", | |
| 2261 | 2261 | (status) => { |
| 2262 | const detail = | |
| 2263 | status === "idle" | |
| 2264 | ? "Turn ended. Individual agent status is unavailable." | |
| 2265 | : "Antigravity process stopped."; | |
| 2262 | 2266 | const thread = makeThread({ |
| 2263 | 2267 | id: ThreadId.make("antigravity-agents"), |
| 2264 | 2268 | projectId: ProjectId.make("project-1"), |
| ... | ... | @@ -2268,14 +2272,14 @@ describe("quiet timeline: nested agents", () => { |
| 2268 | 2272 | makeActivity({ |
| 2269 | 2273 | id: EventId.make(`progress-${index}`), |
| 2270 | 2274 | kind: "task.progress", |
| 2271 | summary: "Antigravity subagent", | |
| 2275 | summary: "Antigravity subagent batch", | |
| 2272 | 2276 | createdAt: `2026-04-01T00:00:0${index + 1}.000Z`, |
| 2273 | 2277 | payload: { |
| 2274 | 2278 | taskId, |
| 2275 | taskType: "subagent", | |
| 2279 | taskType: "subagent_batch", | |
| 2276 | 2280 | agentKind: "agent", |
| 2277 | title: "Antigravity subagent", | |
| 2278 | detail: "Antigravity subagent", | |
| 2281 | title: "Antigravity subagent batch", | |
| 2282 | detail: "Antigravity subagent batch", | |
| 2279 | 2283 | status: "running", |
| 2280 | 2284 | }, |
| 2281 | 2285 | }), |
| ... | ... | @@ -2287,11 +2291,11 @@ describe("quiet timeline: nested agents", () => { |
| 2287 | 2291 | createdAt: "2026-04-01T00:00:03.000Z", |
| 2288 | 2292 | payload: { |
| 2289 | 2293 | taskId: "trajectory:4", |
| 2290 | taskType: "subagent", | |
| 2294 | taskType: "subagent_batch", | |
| 2291 | 2295 | agentKind: "agent", |
| 2292 | title: "Antigravity subagent", | |
| 2296 | title: "Antigravity subagent batch", | |
| 2293 | 2297 | status, |
| 2294 | error: "Antigravity process stopped.", | |
| 2298 | ...(status === "idle" ? { detail, timelineBypass: true } : { error: detail }), | |
| 2295 | 2299 | }, |
| 2296 | 2300 | }), |
| 2297 | 2301 | ], |
| ... | ... | @@ -2302,8 +2306,8 @@ describe("quiet timeline: nested agents", () => { |
| 2302 | 2306 | expect(rows).toHaveLength(2); |
| 2303 | 2307 | expect(rows[0]).toMatchObject({ |
| 2304 | 2308 | lifecycleStatus: status === "failed" ? "failed" : "stopped", |
| 2305 | detail: "Antigravity process stopped.", | |
| 2306 | workEntry: { taskId: "trajectory:4", toolTitle: "Antigravity subagent" }, | |
| 2309 | detail, | |
| 2310 | workEntry: { taskId: "trajectory:4", toolTitle: "Antigravity subagent batch" }, | |
| 2307 | 2311 | }); |
| 2308 | 2312 | expect(rows[1]).toMatchObject({ |
| 2309 | 2313 | lifecycleStatus: "inProgress", |
apps/mobile/src/lib/threadActivity.ts+5-1| ... | ... | @@ -449,6 +449,7 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo |
| 449 | 449 | const taskDetailAsLabel = |
| 450 | 450 | isTaskActivity && |
| 451 | 451 | !taskSummary && |
| 452 | !title && | |
| 452 | 453 | typeof payload?.detail === "string" && |
| 453 | 454 | payload.detail.length > 0 |
| 454 | 455 | ? payload.detail |
| ... | ... | @@ -499,7 +500,7 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo |
| 499 | 500 | toolName: data?.toolName, |
| 500 | 501 | data, |
| 501 | 502 | }); |
| 502 | if (detail && !repeatsCommand) entry.detail = detail; | |
| 503 | if (detail && detail !== title && !repeatsCommand) entry.detail = detail; | |
| 503 | 504 | } |
| 504 | 505 | if (isTaskActivity && typeof payload?.error === "string" && payload.error.trim()) { |
| 505 | 506 | entry.detail = payload.error; |
| ... | ... | @@ -1110,6 +1111,9 @@ function extractWorkLogToolLifecycleStatus( |
| 1110 | 1111 | payload: Record<string, unknown> | null, |
| 1111 | 1112 | ): WorkLogToolLifecycleStatus | undefined { |
| 1112 | 1113 | const status = payload?.status; |
| 1114 | // The parent turn ended, so batch tracking is inactive. The detail explains | |
| 1115 | // that child status is unavailable; do not retain the earlier running marker. | |
| 1116 | if (status === "idle" && payload?.taskType === "subagent_batch") return "stopped"; | |
| 1113 | 1117 | if (status === "pending" || status === "running" || status === "waiting") return "inProgress"; |
| 1114 | 1118 | if (status === "cancelled" || status === "interrupted") return "stopped"; |
| 1115 | 1119 | if ( |
apps/server/src/provider/Layers/AntigravityAdapter.test.ts+3-2| ... | ... | @@ -796,6 +796,7 @@ it.layer(layer)("AntigravityAdapter", (it) => { |
| 796 | 796 | expect(launched.payload).toMatchObject({ |
| 797 | 797 | taskId: started.toolCall.toolCallId, |
| 798 | 798 | title: "Antigravity subagent batch", |
| 799 | taskType: "subagent_batch", | |
| 799 | 800 | description: "Launch subagents", |
| 800 | 801 | status: "running", |
| 801 | 802 | }); |
| ... | ... | @@ -857,7 +858,7 @@ it.layer(layer)("AntigravityAdapter", (it) => { |
| 857 | 858 | const completed = yield* h.waitForEvent((event) => event.type === "task.completed"); |
| 858 | 859 | expect(completed.payload).toEqual({ |
| 859 | 860 | taskId: "replayed:4", |
| 860 | taskType: "subagent", | |
| 861 | taskType: "subagent_batch", | |
| 861 | 862 | toolUseId: "replayed:4", |
| 862 | 863 | title: "Antigravity subagent batch", |
| 863 | 864 | status: "failed", |
| ... | ... | @@ -1085,7 +1086,7 @@ it.layer(layer)("AntigravityAdapter", (it) => { |
| 1085 | 1086 | expect(settled.payload).toMatchObject({ |
| 1086 | 1087 | taskId: "trajectory:4", |
| 1087 | 1088 | title: "Antigravity subagent batch", |
| 1088 | taskType: "subagent", | |
| 1089 | taskType: "subagent_batch", | |
| 1089 | 1090 | status: |
| 1090 | 1091 | stop === "disconnect" |
| 1091 | 1092 | ? "failed" |
apps/server/src/provider/Layers/AntigravityAdapter.ts+1-1| ... | ... | @@ -176,7 +176,7 @@ interface OpenSubagent { |
| 176 | 176 | function subagentLinkage(toolCallId: string) { |
| 177 | 177 | return { |
| 178 | 178 | taskId: RuntimeTaskId.make(toolCallId), |
| 179 | taskType: "subagent", | |
| 179 | taskType: "subagent_batch", | |
| 180 | 180 | toolUseId: toolCallId, |
| 181 | 181 | title: "Antigravity subagent batch", |
| 182 | 182 | }; |
apps/web/src/components/AgentsPanel.tsx+4-2| ... | ... | @@ -140,6 +140,8 @@ function agentActivityText(agent: RuntimeSubagent): string | null { |
| 140 | 140 | /** Flat, non-interactive agent status line. No unfold. */ |
| 141 | 141 | function AgentRow({ agent }: { agent: RuntimeSubagent }) { |
| 142 | 142 | const visuals = STATUS_VISUALS[agent.status]; |
| 143 | const statusLabel = | |
| 144 | agent.kind === "subagent_batch" && agent.status === "idle" ? "Idle" : visuals.label; | |
| 143 | 145 | const activity = agentActivityText(agent); |
| 144 | 146 | const modelLabel = formatSubagentModelLabel(agent.model, agent.effort); |
| 145 | 147 | const role = |
| ... | ... | @@ -180,12 +182,12 @@ function AgentRow({ agent }: { agent: RuntimeSubagent }) { |
| 180 | 182 | agent.status === "failed" ? "text-destructive-foreground" : "text-muted-foreground", |
| 181 | 183 | )} |
| 182 | 184 | > |
| 183 | {activity ?? visuals.label} | |
| 185 | {activity ?? statusLabel} | |
| 184 | 186 | </span> |
| 185 | 187 | <span className="col-start-2 col-end-4 row-start-3 truncate font-mono text-[.7rem] tabular-nums text-muted-foreground/70"> |
| 186 | 188 | {metadata.join(" · ")} |
| 187 | 189 | </span> |
| 188 | <span className="sr-only">{visuals.label}</span> | |
| 190 | <span className="sr-only">{statusLabel}</span> | |
| 189 | 191 | </div> |
| 190 | 192 | ); |
| 191 | 193 | } |
apps/web/src/components/chat/MessagesTimeline.tsx+15-32| ... | ... | @@ -167,6 +167,7 @@ import { |
| 167 | 167 | formatInlineTerminalContextLabel, |
| 168 | 168 | textContainsInlineTerminalContextLabels, |
| 169 | 169 | } from "./userMessageTerminalContexts"; |
| 170 | import { deriveAgentSpawnSummary } from "./agentSpawnSummary"; | |
| 170 | 171 | import { SkillInlineText } from "./SkillInlineText"; |
| 171 | 172 | import { formatWorkspaceRelativePath } from "../../filePathDisplay"; |
| 172 | 173 | import { |
| ... | ... | @@ -2993,22 +2994,12 @@ const AgentSpawnCtaRow = memo(function AgentSpawnCtaRow(props: { workEntry: Time |
| 2993 | 2994 | Math.max(memberIds.size - (spawn.workflowId ? 1 : 0), 0), |
| 2994 | 2995 | ); |
| 2995 | 2996 | |
| 2996 | const running = agents.filter( | |
| 2997 | (agent) => agent.status === "running" || agent.status === "pending", | |
| 2998 | ).length; | |
| 2999 | const waiting = agents.filter((agent) => agent.status === "waiting").length; | |
| 3000 | const failed = agents.filter((agent) => agent.status === "failed").length; | |
| 3001 | // The coordinator's own status is authoritative for workflows: dynamic | |
| 3002 | // spawns mean the member list can be momentarily all-settled while the | |
| 3003 | // run is still mid-flight (the "completed" lie from live testing). A | |
| 3004 | // workflow is live until the coordinator itself reaches a terminal state. | |
| 3005 | const coordinatorStatus = workflowGroup?.workflow.status; | |
| 3006 | const coordinatorSettled = | |
| 3007 | coordinatorStatus === "completed" || | |
| 3008 | coordinatorStatus === "failed" || | |
| 3009 | coordinatorStatus === "cancelled" || | |
| 3010 | coordinatorStatus === "interrupted"; | |
| 3011 | const live = workflowGroup !== undefined ? !coordinatorSettled : running + waiting > 0; | |
| 2997 | const summary = deriveAgentSpawnSummary({ | |
| 2998 | agents, | |
| 2999 | agentCount, | |
| 3000 | coordinatorStatus: workflowGroup?.workflow.status, | |
| 3001 | }); | |
| 3002 | const { live, lead } = summary; | |
| 3012 | 3003 | // Same rule as the panel footer: providers may aggregate member usage into |
| 3013 | 3004 | // the coordinator, so count the coordinator only when no members exist. |
| 3014 | 3005 | const totalTokens = agents.reduce( |
| ... | ... | @@ -3020,22 +3011,14 @@ const AgentSpawnCtaRow = memo(function AgentSpawnCtaRow(props: { workEntry: Time |
| 3020 | 3011 | const workflowName = |
| 3021 | 3012 | workflowGroup?.workflow.workflowName ?? workflowGroup?.workflow.title ?? null; |
| 3022 | 3013 | |
| 3023 | // One steady in-flight presentation (monitoring-pill rule): waiting and | |
| 3024 | // stalled agents read as working; only settled states differentiate. | |
| 3025 | const working = running + waiting; | |
| 3026 | const dotClass = live ? "bg-info" : failed > 0 ? "bg-destructive" : "bg-success"; | |
| 3027 | const lead = live | |
| 3028 | ? `Kicked off ${agentCount} subagent${agentCount === 1 ? "" : "s"}` | |
| 3029 | : `Ran ${agentCount} subagent${agentCount === 1 ? "" : "s"}`; | |
| 3030 | const status = live | |
| 3031 | ? livePhase | |
| 3032 | ? `${livePhase.title} · ${livePhase.activeCount} working` | |
| 3033 | : working > 0 | |
| 3034 | ? `${working} working` | |
| 3035 | : "working" | |
| 3036 | : failed > 0 | |
| 3037 | ? `${failed} failed` | |
| 3038 | : "✓ completed"; | |
| 3014 | const dotClass = { | |
| 3015 | working: "bg-info", | |
| 3016 | failed: "bg-destructive", | |
| 3017 | completed: "bg-success", | |
| 3018 | inactive: "bg-muted-foreground/50", | |
| 3019 | }[summary.tone]; | |
| 3020 | const status = | |
| 3021 | live && livePhase ? `${livePhase.title} · ${livePhase.activeCount} working` : summary.status; | |
| 3039 | 3022 | |
| 3040 | 3023 | return ( |
| 3041 | 3024 | <button |
apps/web/src/components/chat/agentSpawnSummary.test.ts created+73| ... | ... | @@ -0,0 +1,73 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | |
| 2 | import type { RuntimeSubagent } from "@t3tools/client-runtime/state/subagentRuntime"; | |
| 3 | import { deriveAgentSpawnSummary } from "./agentSpawnSummary"; | |
| 4 | ||
| 5 | const batch = (status: RuntimeSubagent["status"]) => ({ kind: "subagent_batch" as const, status }); | |
| 6 | const agent = (status: RuntimeSubagent["status"]) => ({ kind: "subagent" as const, status }); | |
| 7 | ||
| 8 | describe("deriveAgentSpawnSummary", () => { | |
| 9 | it("counts a native batch without claiming the number of children", () => { | |
| 10 | expect(deriveAgentSpawnSummary({ agents: [batch("running")], agentCount: 1 })).toEqual({ | |
| 11 | live: true, | |
| 12 | lead: "Launched 1 subagent batch", | |
| 13 | status: "1 working", | |
| 14 | tone: "working", | |
| 15 | }); | |
| 16 | expect(deriveAgentSpawnSummary({ agents: [batch("idle")], agentCount: 1 })).toEqual({ | |
| 17 | live: false, | |
| 18 | lead: "Launched 1 subagent batch", | |
| 19 | status: "1 idle", | |
| 20 | tone: "inactive", | |
| 21 | }); | |
| 22 | }); | |
| 23 | ||
| 24 | it("keeps individual agents and batches separate in a mixed group", () => { | |
| 25 | expect( | |
| 26 | deriveAgentSpawnSummary({ | |
| 27 | agents: [agent("running"), batch("running"), batch("idle")], | |
| 28 | agentCount: 3, | |
| 29 | }).lead, | |
| 30 | ).toBe("Launched 1 subagent and 2 batches"); | |
| 31 | }); | |
| 32 | ||
| 33 | it.each([ | |
| 34 | ["idle", "1 idle", "inactive"], | |
| 35 | ["cancelled", "1 stopped", "inactive"], | |
| 36 | ["interrupted", "1 stopped", "inactive"], | |
| 37 | ["failed", "1 failed", "failed"], | |
| 38 | ["completed", "✓ completed", "completed"], | |
| 39 | ] as const)("reports %s accurately alongside a completed agent", (state, status, tone) => { | |
| 40 | expect( | |
| 41 | deriveAgentSpawnSummary({ agents: [agent("completed"), agent(state)], agentCount: 2 }), | |
| 42 | ).toMatchObject({ live: false, status, tone }); | |
| 43 | }); | |
| 44 | ||
| 45 | it("does not claim completion when the roster is missing a member", () => { | |
| 46 | expect(deriveAgentSpawnSummary({ agents: [agent("completed")], agentCount: 2 })).toMatchObject({ | |
| 47 | status: "Status unavailable", | |
| 48 | tone: "inactive", | |
| 49 | }); | |
| 50 | }); | |
| 51 | ||
| 52 | it("keeps a workflow active between child launches", () => { | |
| 53 | expect( | |
| 54 | deriveAgentSpawnSummary({ | |
| 55 | agents: [agent("completed")], | |
| 56 | agentCount: 1, | |
| 57 | coordinatorStatus: "running", | |
| 58 | }), | |
| 59 | ).toMatchObject({ live: true, status: "working", tone: "working" }); | |
| 60 | }); | |
| 61 | ||
| 62 | it.each([ | |
| 63 | ["failed", "Workflow failed", "failed"], | |
| 64 | ["cancelled", "Workflow stopped", "inactive"], | |
| 65 | ] as const)( | |
| 66 | "preserves a %s workflow outcome when its children completed", | |
| 67 | (coordinatorStatus, status, tone) => { | |
| 68 | expect( | |
| 69 | deriveAgentSpawnSummary({ agents: [agent("completed")], agentCount: 1, coordinatorStatus }), | |
| 70 | ).toMatchObject({ live: false, status, tone }); | |
| 71 | }, | |
| 72 | ); | |
| 73 | }); |
apps/web/src/components/chat/agentSpawnSummary.ts created+64| ... | ... | @@ -0,0 +1,64 @@ |
| 1 | import { | |
| 2 | isActiveSubagentStatus, | |
| 3 | isTerminalSubagentStatus, | |
| 4 | type RuntimeSubagent, | |
| 5 | } from "@t3tools/client-runtime/state/subagentRuntime"; | |
| 6 | ||
| 7 | /** Summarize observed states without treating idle or missing agents as completed. */ | |
| 8 | export function deriveAgentSpawnSummary({ | |
| 9 | agents, | |
| 10 | agentCount, | |
| 11 | coordinatorStatus, | |
| 12 | }: { | |
| 13 | agents: ReadonlyArray<Pick<RuntimeSubagent, "kind" | "status">>; | |
| 14 | agentCount: number; | |
| 15 | coordinatorStatus?: RuntimeSubagent["status"] | undefined; | |
| 16 | }) { | |
| 17 | const working = agents.filter((agent) => isActiveSubagentStatus(agent.status)).length; | |
| 18 | const failed = agents.filter((agent) => agent.status === "failed").length; | |
| 19 | const idle = agents.filter((agent) => agent.status === "idle").length; | |
| 20 | const stopped = agents.filter( | |
| 21 | (agent) => agent.status === "cancelled" || agent.status === "interrupted", | |
| 22 | ).length; | |
| 23 | const batches = agents.filter((agent) => agent.kind === "subagent_batch").length; | |
| 24 | const individuals = agentCount - batches; | |
| 25 | // Workflow coordinators can keep running between dynamic member launches. | |
| 26 | const live = | |
| 27 | coordinatorStatus !== undefined ? !isTerminalSubagentStatus(coordinatorStatus) : working > 0; | |
| 28 | const subjects = [ | |
| 29 | individuals > 0 ? `${individuals} subagent${individuals === 1 ? "" : "s"}` : null, | |
| 30 | batches > 0 | |
| 31 | ? `${batches} ${individuals > 0 ? "" : "subagent "}batch${batches === 1 ? "" : "es"}` | |
| 32 | : null, | |
| 33 | ] | |
| 34 | .filter(Boolean) | |
| 35 | .join(" and "); | |
| 36 | const lead = `${batches > 0 ? "Launched" : live ? "Kicked off" : "Ran"} ${subjects || "subagents"}`; | |
| 37 | ||
| 38 | const status = live | |
| 39 | ? working > 0 | |
| 40 | ? `${working} working` | |
| 41 | : "working" | |
| 42 | : coordinatorStatus === "failed" | |
| 43 | ? "Workflow failed" | |
| 44 | : coordinatorStatus === "cancelled" || coordinatorStatus === "interrupted" | |
| 45 | ? "Workflow stopped" | |
| 46 | : failed > 0 | |
| 47 | ? `${failed} failed` | |
| 48 | : stopped > 0 | |
| 49 | ? `${stopped} stopped` | |
| 50 | : idle > 0 | |
| 51 | ? `${idle} idle` | |
| 52 | : coordinatorStatus !== "completed" && | |
| 53 | (agents.length === 0 || agents.length < agentCount) | |
| 54 | ? "Status unavailable" | |
| 55 | : "✓ completed"; | |
| 56 | const tone = live | |
| 57 | ? "working" | |
| 58 | : failed > 0 || coordinatorStatus === "failed" | |
| 59 | ? "failed" | |
| 60 | : status === "✓ completed" | |
| 61 | ? "completed" | |
| 62 | : "inactive"; | |
| 63 | return { live, lead, status, tone }; | |
| 64 | } |
docs/user/providers-antigravity.md+2| ... | ... | @@ -145,6 +145,8 @@ Subagent launches appear as **Antigravity subagent batch** in **Agents** on web |
| 145 | 145 | and in the work log on mobile. One launch can start several agents. The batch stays active |
| 146 | 146 | after launch while the parent turn runs. When that turn ends, the entry becomes idle and |
| 147 | 147 | states that individual agent status is unavailable. Launch errors remain visible. |
| 148 | The conversation summary counts launches as batches and shows idle or stopped batches | |
| 149 | without a completion checkmark. | |
| 148 | 150 | |
| 149 | 151 | The official ACP agent does not send individual child status, names, models, token usage, |
| 150 | 152 | or reply ownership. T3 Code cannot show separate child entries or separate child replies |
packages/client-runtime/src/state/subagentRuntime.test.ts+13-2| ... | ... | @@ -69,7 +69,7 @@ describe("foldSubagentActivities", () => { |
| 69 | 69 | it("shows the batch status limit after its parent turn ends without claiming a result", () => { |
| 70 | 70 | const running = activity("task.progress", { |
| 71 | 71 | taskId: "batch-1", |
| 72 | taskType: "subagent", | |
| 72 | taskType: "subagent_batch", | |
| 73 | 73 | title: "Antigravity subagent batch", |
| 74 | 74 | status: "running", |
| 75 | 75 | summary: "Launch readers", |
| ... | ... | @@ -78,7 +78,7 @@ describe("foldSubagentActivities", () => { |
| 78 | 78 | running, |
| 79 | 79 | activity("task.updated", { |
| 80 | 80 | taskId: "batch-1", |
| 81 | taskType: "subagent", | |
| 81 | taskType: "subagent_batch", | |
| 82 | 82 | status: "idle", |
| 83 | 83 | detail: "Turn ended. Individual agent status is unavailable.", |
| 84 | 84 | timelineBypass: true, |
| ... | ... | @@ -87,6 +87,7 @@ describe("foldSubagentActivities", () => { |
| 87 | 87 | expect(agents).toHaveLength(1); |
| 88 | 88 | expect(agents[0]).toMatchObject({ |
| 89 | 89 | title: "Antigravity subagent batch", |
| 90 | kind: "subagent_batch", | |
| 90 | 91 | status: "idle", |
| 91 | 92 | progress: "Turn ended. Individual agent status is unavailable.", |
| 92 | 93 | result: null, |
| ... | ... | @@ -94,6 +95,16 @@ describe("foldSubagentActivities", () => { |
| 94 | 95 | }); |
| 95 | 96 | }); |
| 96 | 97 | |
| 98 | it("learns batch identity from a later update and retains it on sparse updates", () => { | |
| 99 | const agents = fold([ | |
| 100 | activity("task.progress", { taskId: "batch-1", taskType: "subagent", status: "running" }), | |
| 101 | activity("task.updated", { taskId: "batch-1", taskType: "subagent_batch", status: "idle" }), | |
| 102 | activity("task.updated", { taskId: "batch-1", status: "idle" }), | |
| 103 | ]); | |
| 104 | expect(agents).toHaveLength(1); | |
| 105 | expect(agents[0]).toMatchObject({ kind: "subagent_batch", status: "idle" }); | |
| 106 | }); | |
| 107 | ||
| 97 | 108 | it("builds an agent from start → progress → completion", () => { |
| 98 | 109 | const agents = fold([ |
| 99 | 110 | activity("task.started", { |
packages/client-runtime/src/state/subagentRuntime.ts+5-1| ... | ... | @@ -58,7 +58,7 @@ export interface SubagentRunHandles { |
| 58 | 58 | |
| 59 | 59 | export interface RuntimeSubagent { |
| 60 | 60 | readonly id: string; |
| 61 | readonly kind: "subagent" | "workflow" | "workflow_agent"; | |
| 61 | readonly kind: "subagent" | "subagent_batch" | "workflow" | "workflow_agent"; | |
| 62 | 62 | readonly title: string; |
| 63 | 63 | readonly role: string | null; |
| 64 | 64 | readonly model: string | null; |
| ... | ... | @@ -259,6 +259,9 @@ function kindFromPayload( |
| 259 | 259 | payload: Record<string, unknown>, |
| 260 | 260 | agentId: string, |
| 261 | 261 | ): RuntimeSubagent["kind"] { |
| 262 | if (payload.taskType === "subagent_batch") { | |
| 263 | return "subagent_batch"; | |
| 264 | } | |
| 262 | 265 | if (asString(payload.taskType) === "local_workflow") { |
| 263 | 266 | return "workflow"; |
| 264 | 267 | } |
| ... | ... | @@ -314,6 +317,7 @@ function getOrCreate( |
| 314 | 317 | |
| 315 | 318 | /** Metadata fill from any payload: never downgrades known values to null. */ |
| 316 | 319 | function fillMetadata(agent: MutableAgent, payload: Record<string, unknown>): void { |
| 320 | if (payload.taskType === "subagent_batch") agent.kind = "subagent_batch"; | |
| 317 | 321 | const title = asString(payload.title); |
| 318 | 322 | if (title) agent.title = title; |
| 319 | 323 | const role = asString(payload.role); |