Decisions

ADR-108: Sub-agent fires as first-class playbook fires

Today the factory runs two parallel execution worlds and only one of them is visible.

#108

ADR-108: Sub-agent fires as first-class playbook fires

Status: Proposed Date: 2026-04-26 Related: ADR-067 (playbooks-unified-primitive), ADR-090 (ai-as-scaffold-pipelines-as-runtime), ADR-104 (dry-run-on-history-adaptive-gate), ADR-107 (gates-as-playbook-primitives)

Context

Today the factory runs two parallel execution worlds and only one of them is visible.

The playbook runtime is first-class. Fires land as rows in playbook_runs, stream over the Server-Sent Events (SSE) endpoint built in warp #225, render through the LiveRunView stepper-and-terminal surface from warp #224, and surface in /operate for review.

The sub-agent fleet is second-class. When the parent Claude Code session fires a sub-agent (today's morning swarm fired GAA/GAB/GAC/GAD/GAF/GAG nine times) the work exists only as /tmp/claude-1000/.../tasks/<id>.output JSONL transcripts on the operator's laptop, visible only via the parent agent's after-the-fact summary. There is no DB row, no SSE stream, no /operate entry, no /playbooks page. The operator cannot watch sub-agent work happen.

ADR-107 made the principle explicit: every safety mechanism in the factory must survive the handoff away from Claude. The corollary is the visibility one: every execution surface must survive the handoff too. If "what an agent did last night" lives only in the parent's chat scrollback, removing the parent removes the audit trail. The 2026-04-26 deploy postmortem (warp #321) called this out as the next gap to close.

The work to close it is tracked in warp #333 (sub-tickets P-A1 through P-A6).

Decision

Sub-agent invocations register as kind: agent_task rows in the existing playbook_runs table and ride the same SSE / DB / UI surface as any other playbook fire. The parent Claude session continues to fire sub-agents through the existing Agent tool; a bridge tails the sub-agent's JSONL transcript and forwards parsed events (text content, tool-use markers, completion + token usage) to the runtime's existing SSE endpoint. The UI reuses LiveRunView at /playbooks/agent_task?run=<id>, with panels for the prompt brief, the stepper (derived from TodoWrite or chunk markers), the streamed terminal, the tool-use sidebar, and the outputs (PRs opened, files changed, warp items completed). Spawn relationships are tracked via parent_run_id, so today's 4-in-flight swarm becomes a navigable parent-spawn tree rather than a flat list.

The runtime stays minimal in the same shape ADR-107 required for gates: no agent-specific logic, just one new kind value and the same execution-event contract every other fire uses.

Consequences

What becomes easier.

  • Sub-agent work surfaces in /operate next to ansible and shell fires, with a kind chip filter (warp #320 cross-link). The "I can't see what agents are doing" gap closes in the same UI surface that already exists.
  • The parent-spawn tree is navigable. Click into a parent fire, see its children + grandchildren, drill into any node's LiveRunView. The morning's swarm becomes a tree, not a chat-scrollback hunt.
  • Cost and token telemetry per agent and per spawn-tree fall out for free — the <usage> blocks already arrive in completion notifications, the tracking shape from warp #297 (cost dashboard widget) reuses directly.
  • Audit trails outlive the parent session. A fresh agent or operator can read what last night's swarm did from playbook_runs, not from the previous parent's /tmp transcripts.

What becomes harder.

  • A bridge daemon now sits between the Agent tool and the runtime SSE endpoint, tailing JSONL and forwarding events. It is one more component to deploy, restart, and watch. The mitigation: it lives in the same controller surface as the playbook runtime and reuses its SSE plumbing, so failures show up in the same /operate health pane.
  • playbook_runs schema gains a kind: agent_task value plus the agent-specific input/output columns (prompt, model, time_box_minutes, parent_session_id, parent_run_id, final_report, files_changed, prs_opened, warp_items_completed). A migration must run before sub-agents stream.
  • LiveRunView grows agent-task panels (brief, outputs). Existing ansible/shell renderings stay unchanged, but the component file approaches its 900-line ceiling — a split may be needed (panel components per kind).

What we sign up to maintain.

  • The bridge daemon, including its lifecycle, restart policy, and back-pressure behaviour when the runtime SSE endpoint is slow.
  • The playbook_runs.kind=agent_task schema and migrations for new fields. The schema is the contract every consumer (UI, /operate, audit, cost dashboard) reads from.
  • The parent-spawn-tree model. parent_run_id must be populated on every nested fire, with a recursion-depth guard so a runaway agent firing itself indefinitely fails fast.

Alternatives considered

  • Leave sub-agents as JSONL only and surface them via a separate /agents page. Rejected: it preserves the two-world split, doubles the UI surface, and means cost dashboards / /operate / audit queries each need a parallel code path. The whole point of ADR-067's unified-primitive line is one execution surface; this would re-fork it.
  • Make the parent agent POST events directly (no bridge daemon). Rejected: it couples the Agent tool to runtime endpoint shape changes, and silently drops events any time the parent is offline (laptop closed, session crashed). The bridge tails the file, so events survive parent-session death.
  • Track sub-agents in warp items only (no playbook_runs rows). Rejected: warp items are work intent, not execution traces. Cross-cutting queries like "what did the fleet run yesterday" mix the two and lose the per-event timeline LiveRunView needs.

Supersedes: none Superseded by: none yet