| 1 | # Local Sub-Agent Progress |
| 2 | |
| 3 | Status: **implemented** — per-child progress previews for local sub-agent runs |
| 4 | (`task`, `read_only_task`, `parallel_tasks`, `fleet`) in the desktop app and the |
| 5 | CLI, on top of the persisted transcripts and `read_subagent_result` (see |
| 6 | [`CHECKPOINTS.md`](CHECKPOINTS.md) for the persistence model). |
| 7 | |
| 8 | ## Goal |
| 9 | |
| 10 | While a sub-agent works, the user should see **what it is doing** without the |
| 11 | sub-agent's reasoning/text bodies entering the parent conversation: a progress |
| 12 | card shows the child's phase, running elapsed time, and recent activity; the |
| 13 | desktop card can be expanded for a bounded reasoning / response / notice |
| 14 | preview, and the CLI shows the same previews in `/verbose` mode. Everything is |
| 15 | zero-configuration — there are no new settings. |
| 16 | |
| 17 | ## Wire contract |
| 18 | |
| 19 | Progress previews reuse the existing `ToolProgress` event with four reserved |
| 20 | `Tool.Name` values. These names are an internal contract between the agent |
| 21 | progress tracker and local frontends; they must never be presented as |
| 22 | provider-visible tool names: |
| 23 | |
| 24 | | Name | Payload | |
| 25 | |---|---| |
| 26 | | `reasonix.subagent.status` | exactly one of `queued`, `running`, `reasoning`, `responding`, `tool`, `retrying`, `completed`, `failed`, `cancelled` | |
| 27 | | `reasonix.subagent.reasoning` | bounded UTF-8 text delta (the child's thinking) | |
| 28 | | `reasonix.subagent.text` | bounded UTF-8 text delta (the child's response preview) | |
| 29 | | `reasonix.subagent.notice` | bounded UTF-8 text delta (the child's notices) | |
| 30 | |
| 31 | Field conventions: |
| 32 | |
| 33 | - `Tool.ID` — the child task card ID (progress lookup is by ID, never by body). |
| 34 | - `Tool.Output` — the phase value (status) or a text delta (previews). |
| 35 | - `Tool.Truncated` — set when this round's preview was truncated or merged. |
| 36 | - `Tool.DurationMs` — the final duration, carried on terminal status events. |
| 37 | - `Tool.ParentID` — follows the existing nesting relationship (empty for a |
| 38 | top-level `task`; the group call ID for `parallel_tasks`/`fleet` children). |
| 39 | |
| 40 | ## Behavior |
| 41 | |
| 42 | State machine (emitted by the unified run chain in `RunProfileSpec`, shared by |
| 43 | `task`, `read_only_task`, `parallel_tasks`, and `fleet` — no per-entry copies): |
| 44 | |
| 45 | - Foreground runs start with `running`. |
| 46 | - Background runs emit `queued` at registration and `running` once the job |
| 47 | acquires its execution slot. |
| 48 | - `parallel_tasks`/`fleet` group cards get an explicit lifecycle of their own: |
| 49 | `running` when children start and exactly one terminal after every child |
| 50 | settles (`completed`, `cancelled` for cancellation/deadline, `failed` when |
| 51 | any child failed or the call errored — including validation failures). |
| 52 | Frontends never infer group completion from the children observed so far, |
| 53 | since background children dispatch asynchronously and a fast first child |
| 54 | can finish before later ones appear. |
| 55 | - The child's `Reasoning` / `Text` / `Notice` / `Retrying` events become the |
| 56 | corresponding preview channels; the child's real tool activity flips the |
| 57 | phase to `tool` while the nested tool cards render as before. |
| 58 | - Every run emits exactly **one** terminal status: `completed` on success, |
| 59 | `cancelled` for context cancellation or deadline, `failed` for provider, |
| 60 | tool, storage, or panic errors. Pending previews are flushed synchronously |
| 61 | before the terminal; events arriving after the terminal are ignored. |
| 62 | |
| 63 | Pacing and memory bounds (per parent task group): |
| 64 | |
| 65 | - One pending slot per (child, channel); previews merge for up to 250 ms before |
| 66 | one event is emitted, so deltas never accumulate unboundedly. |
| 67 | - At most 32 non-terminal events/sec per group — phase transitions and content |
| 68 | previews share the same budget, round-robined across children so one hot |
| 69 | child cannot starve the others. Only the initial `queued`/`running` states |
| 70 | and the terminal event bypass the limit. |
| 71 | - When the budget trims buffered content, the loss is flagged `Truncated` on |
| 72 | the next actually-emitted channel (or surfaced as a truncated notice at the |
| 73 | terminal flush), so frontends always learn that some preview was dropped. |
| 74 | - Each child's unsent pending buffer is capped at 8 KiB total (notice is |
| 75 | dropped first, then reasoning, then text); overflow keeps a UTF-8-safe tail |
| 76 | and sets `Truncated`. The desktop retains per-channel preview caps (8 KiB |
| 77 | reasoning/text, 2 KiB notice); the CLI keeps 4 KiB reasoning/text tails for |
| 78 | `/verbose`. |
| 79 | |
| 80 | What is **not** done: |
| 81 | |
| 82 | - The child's `Message`, reasoning, and text bodies never enter the parent |
| 83 | transcript or provider context. |
| 84 | - No new event kinds, no new wire fields, no provider tool list/schema/system |
| 85 | prompt changes, no configuration. |
| 86 | - Previews are never persisted: after a restart the complete sub-agent |
| 87 | transcript (and `read_subagent_result`) remains the source of truth. |
| 88 | - ACP and bot consumers keep ignoring `ToolProgress` bodies entirely. |
| 89 | |
| 90 | ## Desktop |
| 91 | |
| 92 | - A sub-agent tool card shows a phase chip (phase + running elapsed + "N s |
| 93 | ago" recent activity) in its header; the chip ticks once a second while the |
| 94 | child is live and settles to a phase + duration summary. |
| 95 | - Expanding the card shows isolated reasoning / response preview / notices — |
| 96 | never mixed with ordinary tool output. |
| 97 | - A background call that already returned its job id stays in the running |
| 98 | state while child progress is non-terminal; `parallel_tasks`/`fleet` group |
| 99 | cards settle only from their own lifecycle terminal event, so neither a |
| 100 | job-id result arriving before any child nor a fast first child finishing |
| 101 | before later children dispatch can settle the group prematurely. |
| 102 | - `completed` / `failed` / `cancelled` reuse the existing done / error / |
| 103 | stopped visuals; after a terminal the card folds by default unless the user |
| 104 | explicitly expanded it. |
| 105 | |
| 106 | ## CLI |
| 107 | |
| 108 | - Each child keeps its own progress state and a fixed transcript slot keyed by |
| 109 | its call ID — independent of the single live tool stream, so concurrent |
| 110 | children never cross-stream. |
| 111 | - By default only the phase, elapsed, and recent activity are shown; the |
| 112 | reasoning/text bodies appear in `/verbose` (Ctrl+O) mode, bounded to the |
| 113 | recent 4 KiB tails. |
| 114 | - Terminal children fold to a one-line summary; verbose keeps the bounded |
| 115 | preview. |
| 116 | - Terminals without in-place redraw (Termux native scrollback) print a status |
| 117 | line on phase changes and terminal only; verbose previews print at most once |
| 118 | every 2 seconds per child. |
| 119 | |
| 120 | ## Contract stability |
| 121 | |
| 122 | Frontends match the reserved names by the `reasonix.subagent.` prefix, so a |
| 123 | future channel added by a newer agent is ignored (never appended to ordinary |
| 124 | tool output) by older frontends. |
| 125 |