169 lines
9.0 KiB
Markdown
169 lines
9.0 KiB
Markdown
---
|
|
status: building
|
|
created: 2026-08-03
|
|
owner: 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](../platform/background-work-liveness.md) and
|
|
[ADR 0049](../../decisions/0049-fine-grained-foreground-idle-busy-signal.md)
|
|
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](../platform/background-work-liveness.md).
|
|
|
|
## 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`.
|