9.0 KiB
status, created, owner
| status | created | owner |
|---|---|---|
| building | 2026-08-03 | nova28 |
Subagent Observability
Why
An agent can dispatch subagents — a review wave, a research fan-out, a parallel
migration — and each one is a substantial unit of work: minutes of wall time,
tens of thousands of tokens, its own tool transcript. Kandev already ingests all
of that. It normalizes the ACP subagent frames, stores per-subagent identity and
metrics, nests each subagent's child tool calls under it, and derives a live
active_subagent_count per session and per task.
None of it is reachable in practice.
A real session (task 93ff1109, two review waves, four subagents) renders its
reviewers only after the user scrolls back ~50 messages and expands a row
labelled 4 tool calls, 2 subagents — the subagent cards are collapsed inside a
turn group that treats a 243-second reviewer as equivalent to
cat > /tmp/prompt.txt. The kanban board shows nothing at all:
active_subagent_count reaches the frontend store and no component reads it,
because rendering it was an explicit non-goal of
background-work-liveness and
ADR 0049
deliberately preserved subagent identities "for a future subagent list without
committing the current UI to one". This spec is that follow-up.
The sharpest gap is that the card records what a subagent did but never what
it returned. For a review wave that is the whole point: an orchestrator's claim
to have run N reviewers cannot be checked against anything, because the verdicts
exist only in the orchestrator's own prose. A wire capture (acpdbg,
claude-agent-acp 0.49.0) confirms the returned text is already on the frame
kandev parses — _meta.claudeCode.toolResponse.content sits beside the
totalTokens and totalDurationMs the adapter already reads, and is stepped
over.
What
Capture
- The Claude subagent result parser reads two additional fields off
_meta.claudeCode.toolResponse, the same map it already reads six fields from:content— an array of ACP content blocks. Text blocks are concatenated in order (newline-separated) into the subagent payload'sresult_text. Non-text blocks (image, audio) are skipped. Absent, empty, or all-non-textcontentleavesresult_textempty.resolvedModel— a string, into the payload'smodel.
result_textis a general subagent-payload field, not a Claude-specific one: its existing Auggie producer is unchanged, and a value already set by an earlier frame is not overwritten by a later empty one.- The captured text is stored verbatim on the message payload. Truncation is a rendering concern, not a persistence one.
Transcript
- A subagent is never collapsed into a turn group. When a turn group would contain subagent messages, those messages render at the group's own level, in chronological position, and the group collapses only the remaining tool calls. A group left with no non-subagent tool calls renders no collapsed row.
- The collapsed face of a subagent card shows, when the data is present: the
subagent type, a description, the child tool-call count, duration, token count,
model, and the first line of
result_textas a single-line result summary. - The result summary is the first non-empty line of
result_text, truncated with an ellipsis to fit one line. The fullresult_textis shown in the expanded body, above any nested child messages. - A subagent with no captured
result_textshows no result summary line and no placeholder. Absence is silent — a subagent that returned nothing and an agent that does not report results must not be made to look like a failure. - The description is not rendered when it merely restates the subagent type:
the existing exact-match suppression is extended to also suppress a
description whose leading words are the subagent type (case- and
separator-insensitive, so
test-supervisorsuppresses the prefix ofTest-supervisor review of new invariant tests, leavingreview of new invariant tests). - The
toolsmetadata chip is suppressed when the reportedtool_use_countequals the rendered child-message count, since the header already states it. A divergence between the two keeps both.
Turn-group label
- A collapsed turn group's label states the counts it actually describes. The leading numeral is the number of collapsed tool calls, and pluralization agrees with it.
- Because subagents are no longer collapsed into groups, the label never mentions subagents.
Board
- A kanban card shows a subagent indicator whenever the task's
active_subagent_countis greater than zero: a small chip carrying the count, next to the existing status affordances. - The chip is live — it appears, updates, and disappears from the same
active_subagent_countalready delivered bytask.updatedand the boot payload, with no new API surface. - At zero, or when the field is absent, no chip renders.
Scenarios
- GIVEN a Claude subagent completes with
toolResponse.contentcarrying text blocks, WHEN the result is parsed, THEN the payload'sresult_textis the concatenated text and itsmodelisresolvedModel. - GIVEN a
toolResponsewhosecontentis absent, empty, or contains only non-text blocks, WHEN the result is parsed, THENresult_textis empty and no placeholder text is substituted. - GIVEN a payload whose
result_textwas already set by an earlier frame, WHEN a later frame reports empty content, THEN the existing value is retained. - GIVEN a turn containing two subagents and four tool calls, WHEN the
transcript renders, THEN both subagent cards appear at top level and the
collapsed group reads
4 tool calls. - GIVEN a turn containing one tool call, WHEN the transcript renders,
THEN the collapsed group reads
1 tool call. - GIVEN a turn whose only tool activity is subagents, WHEN the transcript renders, THEN the subagent cards appear and no collapsed group row renders.
- GIVEN a completed subagent with
result_textspanning several lines, WHEN its card is collapsed, THEN a single-line summary shows the first non-empty line, and WHEN expanded, THEN the full text is shown above any child messages. - GIVEN a subagent with no
result_text, WHEN its card renders, THEN no result summary line appears. - GIVEN a subagent of type
test-supervisordescribed asTest-supervisor review of new invariant tests, WHEN its header renders, THEN the description shows without the redundant leading type. - GIVEN a subagent reporting
tool_use_countof 10 with 10 rendered child messages, WHEN its metadata row renders, THEN thetoolschip is omitted; GIVEN a reported count that differs from the child count, THEN both are shown. - GIVEN a task whose
active_subagent_countis two, WHEN its kanban card renders, THEN a chip shows the count; WHEN the count drops to zero, THEN the chip disappears without a reload.
Out of scope
- Modelling a subagent as a kandev task or task session. Subagents stay inside the parent session; this spec surfaces them, it does not promote them.
toolResponse.toolStats(per-kind tool breakdown and line counts). Available on the same map and worth a later pass; not captured here.- Parsing a verdict taxonomy (APPROVE / REQUEST_CHANGES) out of
result_text. The text is surfaced verbatim; kandev does not interpret it. - Result capture for non-Claude subagent dialects beyond the existing Auggie path. OpenCode, Cursor, and Amp continue to report only what they report.
- A dedicated subagent list, drill-down panel, or per-subagent filtering on the board. The board gets a count chip only.
- Any change to
active_subagent_countderivation, liveness accounting, or the busy-signal contract, all owned by background-work-liveness.
Notes
- Wire evidence:
_meta.claudeCode.toolResponseon a Claude Task completion carriesagentId,agentType,status,content,prompt,resolvedModel,toolStats,totalDurationMs,totalTokens,totalToolUseCount,usage. The adapter reads six of these inclaudeSubagentResponse(apps/backend/internal/agentctl/server/adapter/transport/acp/subagent.go). - The same text also arrives on the terminal frame's
content[]andrawOutput, but that path is unreachable:rawOutputis a JSON array there andextractSubagentResulttype-asserts it to a map. ThetoolResponsepath is the one this spec uses; the array case is left alone. - Frontend seams:
filterVisibleMessages/ turn grouping inapps/web/hooks/use-processed-messages.ts, the card inapps/web/components/task/chat/messages/tool-subagent-message.tsx, chips insubagent-meta.ts, the group label inturn-group-message.tsx, and the board chip inapps/web/components/kanban-card-content.tsxreading theactiveSubagentCountalready mapped inapps/web/lib/kanban/map-task.ts.