# Subagent Live Progress Rendering Design **Date:** 2026-04-12 **Package:** `pi-subagents` ## Goal Make a running subagent show what the child agent is actually doing instead of collapsing every live update to raw event names like `tool_call` and `tool_result`. ## Current state Today `src/tool.ts` handles every child event with the same parent-facing update: - `Running subagent: ${event.type}` That loses the useful semantics already present in normalized events: - assistant turns may already contain human-readable status text - tool calls carry actionable args like file paths and commands - tool results can tell whether a step finished or failed `src/wrapper/render.mjs` already renders transcript lines, but its tool fallback is still JSON-shaped and the parent live UI does not reuse it. As a result, the live progress display is much less informative than the underlying event stream. ## Chosen approach Use a **shared progress formatter**. Behavior order: 1. prefer child `assistant_text` when it contains non-empty text 2. otherwise render humanized tool activity from normalized tool events 3. use concise completion/failure wording for tool results based on remembered tool context Use the same formatter for: - parent live progress updates in `src/tool.ts` - transcript event rendering in `src/wrapper/render.mjs` Keep the wrapper event protocol, runner behavior, artifacts, and `result.json` semantics unchanged. ## Scope ### Modify - `src/tool.ts` - `src/wrapper/render.mjs` - new shared formatter module for normalized progress events - `src/tool.test.ts` - `src/wrapper/render.test.ts` ### Do not modify - `index.ts` - `src/process-runner.ts` - `src/tmux-runner.ts` - `src/wrapper/cli.mjs` event/result lifecycle - `src/wrapper/normalize.mjs` - artifact layout or `result.json` fields ## Design ### 1. Progress priority When a normalized event is `assistant_text` and `text` is non-empty: - show that text to the parent as the live status - render that same text in the transcript as today - do not replace it with a generic tool label This matches the product goal best because the child assistant can express intent directly, for example: - `I’m inspecting the auth flow now.` - `I found the schema; next I’m checking validation.` Blank assistant text should not generate a noisy no-op status line. ### 2. Tool activity fallback When there is no usable assistant text, render normalized tool events into action text. Examples of desired fallback wording: - `read` -> `Reading src/auth.ts` - `grep` -> `Searching code for auth` - `find` -> `Scanning for *.test.ts` - `ls` -> `Listing src/` - `edit` -> `Editing src/tool.ts` - `write` -> `Writing README.md` - `bash` -> keep the shortened shell command, because the command already states the action - unknown tool -> `Running ` The formatter should use available arguments conservatively: - prefer exact paths, patterns, and commands already present in the event - do not invent missing detail - if args are absent, fall back to a generic tool name message instead of guessing ### 3. Tool result wording `tool_result` should not surface as bare `tool_result`. Instead, it should render a concise completion/failure message tied to the most recent relevant tool context when available, for example: - `Finished reading src/auth.ts` - `Search finished` - `Edit failed: src/tool.ts` If no matching prior tool context is available, use a conservative generic result line such as: - `read finished` - `grep failed` This keeps progress understandable without requiring protocol changes. ### 4. Shared rendering boundary The new shared formatter module owns conversion from normalized child events to user-facing progress text. Responsibilities: - map known tools to humanized action strings - preserve assistant text when present - derive concise result wording from recent tool context - keep unknown tools conservative `src/tool.ts` should use this formatter for parent `onUpdate` messages. `src/wrapper/render.mjs` should use the same formatter for transcript event lines so live UI and transcript do not drift. The existing transcript header stays unchanged and generic. ### 5. Failure and noise handling Keep behavior minimal and predictable: - blank assistant text -> emit nothing - unknown tool -> generic `Running ` / ` finished` / ` failed` - no tool context for a result -> generic result wording - artifact/result writing behavior stays unchanged - no new event types, no wrapper protocol expansion ## Testing strategy Follow TDD. ### First failing tests 1. `src/tool.test.ts` - assistant text is surfaced directly when present - fallback tool updates are humanized - parent progress no longer shows raw `tool_call` / `tool_result` 2. `src/wrapper/render.test.ts` - known tools render as action text instead of JSON-shaped fallback lines - result lines use completion/failure wording - bash still renders as shortened command text ### Verification after implementation Run at least: - targeted test files for touched units - full package suite: `npm test` ## Non-goals - changing child wrapper completion behavior - changing normalized event schema - adding runner-specific progress logic - redesigning artifact files or transcript headers - inventing semantic summaries beyond the data already present in events ## Expected outcome After the change, a running subagent will show meaningful status like assistant intent text or readable tool activity instead of raw event-type labels. The parent live UI and saved transcript will use the same progress language while keeping runner behavior, wrapper semantics, and result artifacts stable.