Files
ai-app/SUBAGENTS.md
T
irisandClaude Fable 5.1 9fa09b0af1 Show a session's subagents as subcards, each with a read-only transcript
A subagent is a second transcript owned by a session, in the same event
model, with no process and no controls. The claude translator routes lines
carrying parent_tool_use_id to a per-subagent translator and transcript
under <session>/subagents/<tool_use_id>; three routes expose the list, a
transcript page and the SSE stream. Echo grows /subagent [n] as the rig.

On the phone a card with subagents ends in a chevron expander, collapsed by
default, opening to outlined subcards styled like dev-updater's components;
a subcard opens SessionScreen in read-only form, addressed through
TranscriptAddress so paging, cache and stream are shared.

Design in SUBAGENTS.md; choices awaiting review in DECISIONS.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 13:41:15 -04:00

6.2 KiB

Subagents

A session's subagents -- the helpers a Claude Code session starts through its Task tool -- each get a transcript of their own, listed under the session's card and readable in the same transcript view the session has. Designed 2026-09-05; the decisions Bryan has not yet reviewed are in DECISIONS.md.

What a subagent is here

A subagent is a second transcript owned by a session, in the same event model, with no process and no controls. It is not a session: it cannot be messaged, stopped or started, and it has no setup, model or usage of its own. Everything it shares with a session -- the transcript file format, the paging routes, the SSE stream, the phone's cache and rendering -- is reused by addressing, not by copying.

The CLI reports a subagent's messages on the parent's own stream-json output, each carrying parent_tool_use_id = the id of the Task tool_use that started it. Before this the translator dropped those lines (subagent_events_are_not_duplicated_into_the_transcript); now it routes them to that subagent's own translator and transcript. The parent's transcript still shows only the Task call itself.

Storage

Under the session directory:

<session>/subagents/<tool_use_id>/meta.json        {title, created}
<session>/subagents/<tool_use_id>/transcript.jsonl same SeqEvent lines as the session's

The id is the Task tool_use id (toolu_…), which is unique, stable across a backend restart, and already the key everything on the parent side uses. Only ids matching [A-Za-z0-9_-]+ are ever created or looked up, since the id becomes a path.

The transcript's sequence numbers are its own, starting at 1. Transcript, read_window, catch_up and read_after work on it unchanged.

Its path out: deleting the session deletes its directory, subagents included. There is no separate delete.

Lifecycle, as events in the subagent's transcript

  1. Created on the first child line for an unseen parent id (or, when the parent Task call was seen, at that call). First lines written: Status Running, then UserMessage { text: <the Task's prompt> } when the prompt is known -- it genuinely is the subagent's first user turn.
  2. Every child line is translated by that subagent's own Translator (one per subagent: tool ids are unique but streaming deltas are by content-block index, and parallel subagents interleave).
  3. When the parent's tool_result for the Task id arrives, the parent gets its ToolEnd as before, and the subagent gets Status Exited.
  4. When the parent session's process exits (Status Exited on the session), every subagent still Running gets Status Exited too: its process was the parent's.

A subagent that was mid-flight when the backend restarted keeps working: the registry reopens the existing transcript on the next child line, and the file continues its sequence. If its Task call finished while the backend was down nothing ever closes it -- its last status stays Running, which the list reports as unknown rather than as running (see the wire shape).

Title: the Task call's description input, then (<subagent_type>) when one is given; falling back to the tool's name when the child arrives before (or without) the parent call being seen.

Server layout

  • session/subagent.rs -- the registry: Subagents (per session, in Shared), Subagent (its Transcript behind a mutex plus a broadcast::Sender<SeqEvent>), record(id, event), start(id, title, prompt), finish(id), finish_all(), list() from disk. Drivers get an Arc<Subagents> beside their EventSink; llama ignores it.
  • session/claude/translate.rs -- routes child lines by parent id, holds one child Translator per subagent, remembers pending Task calls' description/prompt/subagent_type.
  • session/echo.rs -- /subagent [n]: the test rig. Starts n (default 1) subagents at once, each named "helper k". Each writes the prompt as its user message, streams a few words of text, runs one Bash tool call, then finishes about three seconds after starting, and the parent's Task calls end when their subagent does. Three seconds so the running state can be seen on the phone.
  • routes.rs -- three routes, in the doc table.

Wire shape

GET /sessions/{id}                       SessionInfo gains `subagents: N` (count, 0 when none)
GET /sessions                            same field on each row
GET /sessions/{id}/subagents             [{id, title, status, created, lastActivity}], oldest first
GET /sessions/{id}/subagents/{sub}/transcript   exactly the session transcript's query and answer
GET /sessions/{id}/subagents/{sub}/events?after=N  exactly the session events stream

status is the transcript's last Status event, serialised like a session's (running, exited), except that a subagent whose session is not itself running cannot be running: the list answers unknown for that one. The phone words these as running, finished and unknown on the subcard.

The count on SessionInfo is a directory listing, so the list stays cheap. The per-subagent status is only read when the list route is asked for.

Phone

  • SessionSummary.subagents: Int. A card with a non-zero count ends in an expander row -- a full-width Chevron(Pointing.Down) row that flips to Pointing.Up -- collapsed by default. Expanding fetches /sessions/{id}/subagents and draws one OutlinedCard per subagent, indented inside the session card, the way dev-updater draws a project's components: title, then the status word and a relative time. The expansion state is per session id and survives a refresh of the list.
  • Tapping a subcard opens Screen.Subagent, which is SessionScreen in read-only form: the same transcript, paging, cache, selection, images and status row, with the composer, the process button, the model picker, the files button, the settings cog and the usage bar left out. The header shows the subagent's title with the session's title beneath it. Back returns to the list.
  • Addressing: fetchTranscript, EventStream, TranscriptSource and the cache take a transcript address rather than a session id -- sessions/{id} or sessions/{id}/subagents/{sub} -- so the cache nests a subagent's copy under its session's and the same code serves both.