Files
kandev/docs/specs/ui/subagent-observability.md
T

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's result_text. Non-text blocks (image, audio) are skipped. Absent, empty, or all-non-text content leaves result_text empty.
    • resolvedModel — a string, into the payload's model.
  • result_text is 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_text as 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 full result_text is shown in the expanded body, above any nested child messages.
  • A subagent with no captured result_text shows 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-supervisor suppresses the prefix of Test-supervisor review of new invariant tests, leaving review of new invariant tests).
  • The tools metadata chip is suppressed when the reported tool_use_count equals 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_count is 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_count already delivered by task.updated and 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.content carrying text blocks, WHEN the result is parsed, THEN the payload's result_text is the concatenated text and its model is resolvedModel.
  • GIVEN a toolResponse whose content is absent, empty, or contains only non-text blocks, WHEN the result is parsed, THEN result_text is empty and no placeholder text is substituted.
  • GIVEN a payload whose result_text was 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_text spanning 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-supervisor described as Test-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_count of 10 with 10 rendered child messages, WHEN its metadata row renders, THEN the tools chip is omitted; GIVEN a reported count that differs from the child count, THEN both are shown.
  • GIVEN a task whose active_subagent_count is 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_count derivation, liveness accounting, or the busy-signal contract, all owned by background-work-liveness.

Notes

  • Wire evidence: _meta.claudeCode.toolResponse on a Claude Task completion carries agentId, agentType, status, content, prompt, resolvedModel, toolStats, totalDurationMs, totalTokens, totalToolUseCount, usage. The adapter reads six of these in claudeSubagentResponse (apps/backend/internal/agentctl/server/adapter/transport/acp/subagent.go).
  • The same text also arrives on the terminal frame's content[] and rawOutput, but that path is unreachable: rawOutput is a JSON array there and extractSubagentResult type-asserts it to a map. The toolResponse path is the one this spec uses; the array case is left alone.
  • Frontend seams: filterVisibleMessages / turn grouping in apps/web/hooks/use-processed-messages.ts, the card in apps/web/components/task/chat/messages/tool-subagent-message.tsx, chips in subagent-meta.ts, the group label in turn-group-message.tsx, and the board chip in apps/web/components/kanban-card-content.tsx reading the activeSubagentCount already mapped in apps/web/lib/kanban/map-task.ts.