Skip to content

feat(desktop): attribute a sub-agent's work to it, and show what it did (R5) - #574

Merged
physercoe merged 3 commits into
mainfrom
r5-subagent-panel
Aug 17, 2026
Merged

feat(desktop): attribute a sub-agent's work to it, and show what it did (R5)#574
physercoe merged 3 commits into
mainfrom
r5-subagent-panel

Conversation

@physercoe

Copy link
Copy Markdown
Owner

Closes the R5 wedge of docs/plans/desktop-companion-vision-parity.md. W4 is complete with R6 (#573).

What the director gets

The Sub-agents chip listed delegated agents but could not say what any of them did. Clicking one now opens what it was asked to do and everything it has done — its tool calls, its notes, and what it is running right now. The drill-down lives in the dock's existing panel with a back button, so no new region.

The premise was true of one engine, and not one we drive

The plan said "events already carry subagent marking". The only producer of that marking is the kimi M4 wire-tail (kimi_code/mapper.go:235). Built as written, the panel would have been permanently empty for claude and codex.

No session log on the dev box had a sidechain in it, and the field appears in no vendor doc we hold — so I made the observation: a real claude --print --output-format stream-json run making one Agent call. claude-code 2.1.220 carries more than the plan assumed:

  • parent_tool_use_id on every assistant/user framenull for the main agent, the spawning Agent call's tool_use_id for a sub-agent's work. This is the correlation the panel needs.
  • subagent_type + task_description alongside it.
  • Four system subtypes: task_started (full prompt), task_progress (last_tool_name + running usage), task_updated, task_notification (summary).
  • The tool is named Agent, not Task — the dock's name set already matches it.

The bug that fell out

Our translator dropped all of it. A sub-agent's tool calls and prose landed in the transcript indistinguishable from the main agent's own work — read as the agent you're talking to having run them. And #374's guard, which keeps a sub-agent's token usage out of the session's turn counts, checks subagent:true and therefore skipped nothing on claude, quietly inflating them.

So R5 ships the producer half first. The frame profile stamps parent_tool_use_id plus a derived subagent boolean — the same one kimi already sets, so one vocabulary answers for both engines and the two existing consumers (digest_fold.go, transcriptStats.ts) start working with no change. Deriving it at the producer rather than in each consumer is deliberate: a predicate re-derived per consumer is the duplication this repo has been bitten by. task_progress regains tool_use_id + subagent_type, which #374 dropped along with the noise it meant to drop.

Honesty for the engines that report nothing

codex and gemini publish no per-sub-agent correlation. The panel still opens for them — the header comes off the spawning call, which every engine has — and then says "this engine does not report which events are a sub-agent's". That is a different sentence from "this sub-agent has not done anything yet", which is what an empty stream means when the engine does report. The panel picks between them by asking the data, not the engine name, so an engine that starts reporting tomorrow needs no change here.

Verification

  • 8 new tests, three mutation-checked. The separating input is two sub-agents running at once — a filter keyed on the subagent boolean alone puts both agents' work under each row and passes every single-agent test. Mutations caught: subagent-bool-only join (2 fail), toolId = event id (7 fail), reported dropping the provenance probe (1 fail).
  • Both translators agree: the legacy translate() mirrors the profile field-for-field (the parity test compares key sets, so fields are set unconditionally, nil included).
  • The 15-frame recording is now in the parity corpus — its first sidechain coverage — scrubbed of the recording box's scratch paths and session id. The desktop's TS interpreter is pinned to the same regenerated fixture, so the local Companion gets the same events; the electron suite verifies it.
  • go test ./internal/hostrunner ./internal/agentfamilies ./internal/server ✅ · desktop node --test 883 pass · electron 715 pass, 6 skipped · tsc --noEmit ✅ · full vite build ✅ · token ratchet clean, no phantom tokens · CI's whole lint set ✅ including openapi.

🤖 Generated with Claude Code

Ubuntu and others added 3 commits August 16, 2026 17:09
…id (R5)

The Sub-agents chip listed delegated agents but could not say what any of
them did. Clicking one now opens what it was asked to do and everything
it has done — its tool calls, its notes, and what it is running now.

Building it measured the premise. The plan said "events already carry
`subagent` marking"; the only producer of that marking was the kimi M4
wire-tail (kimi_code/mapper.go:235), so the panel would have been
permanently empty for claude and codex — the two engines the Companion
is built around.

The probe that settled it: a real `claude --print --output-format
stream-json` run making one Agent call, because no session log on this
box had a sidechain in it and the field appears in no doc we hold.
claude-code 2.1.220 carries MORE than the plan assumed:

  - parent_tool_use_id on every assistant/user frame — null for the main
    agent, the spawning Agent call's tool_use_id for a sub-agent's work
  - subagent_type + task_description alongside it
  - four system subtypes: task_started (full prompt), task_progress
    (last_tool_name + running usage), task_updated, task_notification
  - the tool is named `Agent`, not `Task`

Our translator dropped all of it. So a sub-agent's tool calls and prose
landed in the transcript INDISTINGUISHABLE from the main agent's own
work — read as the agent doing it itself — and #374's guard, which keeps
a sub-agent's usage out of the session's turn counts, skipped nothing on
claude and quietly inflated them.

So R5 ships the producer first: the frame profile stamps
parent_tool_use_id plus a DERIVED `subagent` boolean — the same one kimi
already sets, so one vocabulary answers for both engines and the two
existing consumers (digest_fold.go, transcriptStats.ts) start working
with no change. Deriving it at the producer rather than in each consumer
is deliberate: a predicate re-derived per consumer is the duplication
this repo has been bitten by. task_progress regains tool_use_id +
subagent_type, which #374 dropped along with the noise it meant to drop.

The legacy translator mirrors it field-for-field (the parity test
compares key sets, so the fields are set unconditionally, nil included),
and the 15-frame recording is now in the parity corpus — its first
sidechain coverage, scrubbed of the recording box's paths and session
id. The desktop's TS interpreter is pinned to the same regenerated
fixture, so the local Companion gets it too.

Honesty: codex and gemini publish no per-sub-agent correlation. The
panel still opens for them — the header comes off the spawning call,
which every engine has — and says "this engine does not report which
events are a sub-agent's". That is a different sentence from "this
sub-agent has not done anything yet", which is what an empty stream
means when the engine DOES report, and the panel picks between them by
asking the data, not the engine name.

Tests: 8 new, three mutation-checked. The separating input is two
sub-agents running at once — a filter keyed on the `subagent` boolean
alone puts both agents' work under each row, and passes every
single-agent test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… kind #374's guard reads

R5 stamped text / thought / tool_call / tool_result, which fixes the
transcript's attribution — but the token-inflation half of the bug it
describes lives on `usage`: digest_fold's InTokens/OutTokens fold and
transcriptStats' turn accounting both skip `subagent:true` USAGE frames
and read nothing else. A sub-agent's assistant frames carry
`message.usage` like any other (corpus frame 25, the recorded sidechain
frame, does), so the unstamped usage emit kept the delegated agent's
tokens counted inside the main turn — the exact inflation the change
set out to remove, surviving its own fix. The pinned fixture showed it:
frame 25's usage event had no `subagent` key at all.

Both translators now stamp the same three fields on usage (`$.` scope,
not the sub_rules' `$$.` — this rule has no for_each, so the frame IS
the inner scope). Fixture + families artifact regenerated; frame 25's
usage row now reads `subagent: true`.

New TestProfile_ClaudeCode_SubagentUsageIsFlagged pins the requirement
semantically for BOTH translators — deliberately independent of the
parity fixture, which is regenerated on drift and would simply re-pin a
future regression. Mutation-verified both ways: dropping the legacy
stamp fails parity (keyset diff on every usage frame), dropping the
YAML field fails this test on both paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…atus docs

Both PRs rewrote the plan's W4 status paragraph and appended under the
changelog's Unreleased headings — the collision called out at review time.

- plan: united to "W4 complete (2026-08-17)" naming all of F4 / L4a–L4c /
  R5 / R6, remaining = L3c only (deferrable).
- changelog: union — both Added entries kept (R5 above R6), both Fixed
  entries under one heading.

Everything else auto-merged; verified on the merged tree: agentfamilies +
hostrunner drift/parity tests (the auto-merged generated artifact matches
a regeneration of the merged YAML), electron 720/0, frontend 908/0, docs
lint 323.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@physercoe
physercoe merged commit 70be8fe into main Aug 17, 2026
8 checks passed
@physercoe
physercoe deleted the r5-subagent-panel branch August 17, 2026 01:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants