Skip to content

An agent's subagents are invisible: the processes section lists OS processes only - #417

Merged
paulyhedral merged 1 commit into
developfrom
411-agent-subagent-visibility
Sep 23, 2026
Merged

paulyhedral merged 1 commit into
developfrom
411-agent-subagent-visibility

Conversation

@paulyhedral

@paulyhedral paulyhedral commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Implements the agent-subagent-visibility OpenSpec change — showing an agent's subagents in the agent pane's processes section, told apart from the OS child processes already listed there.

Proposal PR: #409 (merged). All 48 tasks across 11 groups complete.

The gap

Knot had no runtime representation of a subagent anywhere in crates/. SessionUpdate had no such variant, ToolCallCard kept kind as an opaque passthrough, and agent_type::ALL modelled no parent/child relation.

Nor could ps supply it: Claude Code runs a subagent inside the parent's own process. An agent quiet for four minutes read as node +2 more whether it was stuck, compiling, or waiting on three delegations.

What landed

  • New crate knot-subagents — the model (Subagent, SubagentState, SubagentKind, SubagentEvent), the registry, and one recognizer per agent type. Runtime-agnostic and UI-free; every recognizer is a pure function from JSON to an optional event.
  • knot-acp carries rawInput and _meta through ToolCallStart/ToolCallUpdate, which it previously discarded.
  • Two feeds, one registry — the ACP drain on a session task, the hook route on an axum worker, both writing the same Arc reached through a GPUI global.
  • SubagentReporting on the agent_type roster — None | ToolCalls | Hooks | Either, resolved against view mode.
  • Two labelled groups in the section, subagents first, with the header counting each separately.
  • render/processes_pane.rs split by concern before the second row kind landed.

Four things worth a reviewer's attention

A quiet bug the adapter source caught. claudeCodeMetaFromToolUse stamps subagent: true, but the completion path never calls it — it builds { toolName, ...nonExecution } inline. Keying recognition on the flag alone would have matched every dispatch and no completion, leaving every subagent on screen as permanently running. Recognition keys on the flag or toolName ∈ {Task, Agent}; the fold treats an absent _meta as "no change" so a completion cannot erase the marker. Both have tests.

Claude ships as ToolCalls, not Either. The hook emitter is a plugin outside this repo, so a Terminal-mode Claude agent reads as subagent reporting unavailable rather than confidently claiming it dispatched nothing. Flipping the column is a one-line follow-up once that plugin ships, and the roster test names that so the value is not read as an oversight.

The dedicated ACP channel was declined, not missed. The same adapter offers first-class subagent_spawned / subagent_state_update updates with four states and a cancel capability — gated on a capability Knot does not advertise. The file calls itself a temporary surface for an unshipped ACP draft (agentclientprotocol/agent-client-protocol#1992), and advertising the capability rescopes a subagent's nested tool calls onto a child session acp-panel-ui does not model. Two requirements here — no terminate action, and a two-valued Outcome — are correct only for the channel built; design.md says so rather than leaving them looking universal. Tracked as pilgrimagesoftware/Knot-App#15.

PanelMessage::ToolCall is now boxed. The two new fields pushed ToolCallCard past clippy's large_enum_variant threshold, but the imbalance predates them: an unboxed 264-byte card made every plain-text entry in a conversation cost the card's width.

Two corrections to this change's own artifacts

Both made in-branch rather than worked around:

  • design.md's recognition decision claimed rawInput's tool name was the only available signal. Reading the adapter showed it stamps a metadata marker for exactly this purpose. Corrected, with the measurement for the rejected alternative (kind is "think", shared with ordinary reasoning calls).
  • A spec-delta scenario asked for three subagents of one kind to produce a remainder of two, contradicting the same requirement's rule that the remainder counts what the listed names do not cover — and the existing eight-node scenario had already established the opposite. The code was right; the scenario was not.

Checks

make passes: fmt-check, size-check (462 files, none over 700 lines), declared-check (394 modules), clippy with -D warnings, 1653 tests.

openspec validate agent-subagent-visibility --strict passes.

Not in scope

Terminating or interrupting a subagent; attributing an OS process to one; nesting subagents; parsing the agent's transcript; shipping the hook emitter. Each is named in the proposal's non-goals with its reason.

Unrelated flakiness surfaced while running the fuller suite is filed separately as pilgrimagesoftware/Knot-App#19 — a pre-existing wall-clock race in panel_session, untouched here.

Closes pilgrimagesoftware/Knot-App#129

🤖 Generated with Claude Code

The crate a subagent is modelled in: what one is, how its run ends, and
the single event shape both feeds reduce to. No recognizers yet.

SubagentState is closed with no default, deliberately unlike the settings
vocabularies. Those degrade an unrecognized value to a default so one bad
field cannot take a whole document down; here the opposite is required -
defaulting a failure to Finished would report a subagent succeeded when
the agent said it did not. An unknown word is an error, and the caller's
job on one is to record nothing.

SubagentKind is open, like agent_type: the set of kinds is the user's own
configuration. The one thing the code decides is whether a kind was stated
at all, which is why it is not a String - a report naming no kind and a
report naming the kind "unstated" are different facts, and an empty string
makes them the same one. Display writes nothing for the absent case rather
than inventing English the catalogue owns.

Subagent carries an `ended` instant the change's task sketch did not name.
The spec requires a finished record's elapsed time to stop at completion,
and a start instant plus a state word cannot say when that was. It sits
beside the state rather than inside Finished so the vocabulary stays one:
two failures with different end instants are still one state. Completing
twice keeps the first instant, so a duplicate report cannot stretch a
finished subagent's runtime.

elapsed() takes `now` rather than reading the clock, so one frame renders
a whole list against a single consistent reading and the arithmetic tests
without sleeping.

Refs #411

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@paulyhedral paulyhedral added rust Pull requests that update rust code enhancement New feature or request labels Sep 23, 2026
@paulyhedral paulyhedral self-assigned this Sep 23, 2026
@paulyhedral
paulyhedral merged commit a9b0169 into develop Sep 23, 2026
2 checks passed
paulyhedral added a commit that referenced this pull request Sep 25, 2026
PR #417 merged after its first commit only, so groups 2-11 of the
agent-subagent-visibility change never reached develop. Bring the branch
up to date so they can land.

Conflicts resolved by keeping both sides: the subagent reporting column
beside the MCP columns in the agent_type roster, the boxed ToolCall
variant beside the new Shell entry, the subagent repaint flag beside the
MCP probe tick, and the subagent clear beside the MCP section forget. The
accordion layout develop added to processes_pane.rs is ported into the
split processes_pane/section.rs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request rust Pull requests that update rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

An agent's subagents are invisible: the processes section lists OS processes only

1 participant