iris is the framework alone; the app is one crate in app-rust/
Iris: "the organization of the rust rewrite is a mess right now... there shouldn't be anything related to the app inside of iris. Iris is supposed to be the UI framework alone." And, on the crate count: "I'm confused why the app only code needs more than one crate though." Nine cargo workspaces become three, and the port's project code -- which sat in five places, four of them inside the framework -- becomes one crate, `ai-app`, in `app-rust/`: client-core -> app-rust/src/client iris/transcript-ui -> app-rust/src/ui iris/transcript-fixture -> app-rust/src/ui/fixture.rs + tests/ + touch/ iris/desktop-app -> app-rust/src/desktop + src/bin_desktop.rs iris/android-app -> app-rust/src/android + android-project/ android-shell -> app-rust/src/shell iris/ keeps core, macro, the iris crate, tabs-ui and rig-input, and now mentions no session, transcript, setup or server anywhere. Only two of the old splits had a reason that survived reading. event-model stays a crate at the repo root because server/ depends on it too, so a crate is what makes the backend and the app agree by construction. The two Android .so names looked like a hard constraint -- a package produces one library artifact -- until P2 turned out to already plan merging those two Android apps into one; both faces now come out of libai_app.so, picked apart by features so `--no-default-features --features shell` keeps wgpu, parley and iris out of the Compose app's APK. docs/RUST.md's "One app crate" has the rest, including what each remaining feature is for. DECISIONS.md and SUBAGENTS.md move into docs/ with everything else. Verified: ./run-tests.sh and `cd iris && cargo test` green, clippy and fmt clean in all five workspaces, `cargo ndk -t x86_64` links libai_app.so, build-apk.sh produces an APK that installs and launches on this checkout's emulator (Gl ... virgl, as expected), and the phone-sized headless screenshot renders the transcript unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e9a6562dc6
commit
6d5a231f5c
100 files changed
+924
-3295
No files matched your search
@@ -0,0 +1,141 @@
|
||||
# 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 `SUBAGENTS_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. **The parent's `tool_result` never finishes a subagent.** The Task tool
|
||||
runs in the background by default: the `tool_result` -- "Async agent
|
||||
launched..." -- arrives the moment it *starts*, while the subagent goes
|
||||
on working for however long its own turn takes, sometimes minutes. What
|
||||
ends it is its own turn ending: the raw API's `message_delta` on its
|
||||
stream carrying `stop_reason: "end_turn"` (a `stop_reason` of `tool_use`
|
||||
is the model about to call one, not an end), or a `result` line for its
|
||||
own turn if a future CLI version ever sends one. Either maps to
|
||||
`Status Exited`; the subagent's vocabulary has no `Idle`, so the
|
||||
equivalent event `dispatch` produces for an ordinary session is dropped
|
||||
rather than written. A shipped version of this finished on the
|
||||
`tool_result` instead, which read a running background agent as
|
||||
"finished" with its transcript truncated at the moment it launched.
|
||||
4. **A child line for a subagent that already finished reopens it**
|
||||
(`Status Running`) rather than being dropped: a background Task can be
|
||||
sent another message long after its first turn ended, and that is
|
||||
exactly what a further line for it means. Same transcript, same child
|
||||
`Translator`, just picking back up.
|
||||
5. 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 -- the same reopening #4 describes, whether
|
||||
what closed it was a restart or its own `end_turn`. If its turn ended while
|
||||
the backend was down nothing recorded that until the next line arrives, so
|
||||
its last status stays `Running`, which the list reports as **unknown**
|
||||
rather than as running (see the wire shape) until then.
|
||||
|
||||
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)`, `reopen(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.
|
||||
Reference in new issue
Block a user