System shape
The parent Pi session exposes two APIs to its model. Both eventually create real AgentSession instances through the Pi SDK, but they optimize for different jobs.
Parent AgentSession
├─ subagent_* tools ──> SubagentManager
│ ├─ persistent child AgentSession (sa-1)
│ ├─ persistent child AgentSession (sa-2)
│ └─ status, wait, cancellation, takeover UI
│
└─ workflow tool ─────> sandboxed orchestration process
├─ phase() ──IPC──> workflow state
└─ agent() ──IPC──> RunController
└─ in-memory child AgentSession(s)
Both paths ──> shared/child-session.ts
resources · trust · tool policy · extension binding · shutdown
| Dimension | Direct subagent | Workflow agent |
|---|---|---|
| Entry point | subagent_spawn | agent() inside workflow |
| Primary use | One-off background delegation | Ordered phases, fan-out, aggregation |
| Session storage | Persistent Pi session file | In-memory session |
| Result delivery | Automatic follow-up or explicit wait | Return value to orchestration script |
| Human control | Inspect, steer, continue, abort | Observe run and transcripts; no resume |
| Concurrency behavior | Reject new spawn when four are active | Queue behind a four-slot semaphore |
Direct subagents
Adapter layer
extensions/subagents/index.ts is the parent-facing adapter. It registers five tools, resolves model/provider and reasoning effort, validates the working directory, computes child trust, formats bounded outputs, updates the TUI status line, and renders completion messages.
subagent_spawncreates a child and returns immediately with ansa-Nid.subagent_waitblocks on selected ids and marks their settlement as consumed, preventing duplicate automatic follow-ups.subagent_cancelclears queued input, aborts active work, and preserves the partial session transcript.subagent_checkreads status and current streaming text without consuming the result.subagent_listreturns the manager’s current registry.
Lifecycle owner
SubagentManager in extensions/subagents/manager.ts is the source of truth. Its map holds identity, prompt, title, cwd, timestamps, status, error text, and the live AgentSession. It also owns concurrency reservations, waiter interest, cancellation guards, state listeners, pruning, and teardown.
spawn()
reserve slot before first await
→ create trust-aware resources
→ create persistent AgentSession
→ bind child extensions in print mode
→ subscribe to agent_start / agent_settled
→ register sa-N
→ prompt() asynchronously
→ release reservation
The reservation counter is important: concurrent tool calls cannot all pass the four-agent check before asynchronous session creation begins.
Workflow agents
The workflow system adds a programmable control plane around the same child-session runtime. The model supplies an async JavaScript body with phase(), agent(), parallel(), and args.
meta.tsparses the source with Acorn, extracts only static literal metadata, rejects other module syntax, and removes the metadata declaration without shifting line numbers.sandbox.tslaunches a permission-restricted Node child process. The script has no imports, eval, timers, filesystem, network, or process API.- The sandbox can only send authenticated IPC requests for
phaseandagent. Prompts and options cross back to the trusted parent process. RunControllerenforces a four-agent semaphore, a 32-call budget, abort propagation, task tracking, and bounded settlement.runner.tscreates one in-memoryAgentSessionper call, reports normalized transcript and usage progress, then always disposes it.- The trusted parent returns a serializable
{ ok, output, structured?, error? }result to the sandbox; script-level agent failures do not throw.
Structured results
When an agent() call includes a JSON Schema, the runner injects a one-shot structured_output tool and an accompanying system instruction. Calling the tool captures validated data and terminates the child run. Finishing without calling it is treated as an agent failure.
Background versus blocking
Blocking workflows stream throttled tool updates to the current turn. Background workflows return a run id immediately, remain visible through /workflows, and deliver a follow-up message on completion. Both forms are registered for shutdown cancellation.
Shared child runtime
extensions/shared/child-session.ts centralizes policy that would otherwise drift between direct and workflow children.
| Function | Responsibility |
|---|---|
createChildResources | Creates fresh settings and resource loaders for the child cwd; loads global/package resources plus trust-gated project resources such as skills and AGENTS.md. |
resolveStandaloneChildProjectTrust | Inherits the live parent decision only for the same directory; alternate directories must be explicitly trusted in Pi’s persisted trust store and failures close to untrusted. |
childToolPolicy | Uses a denylist to remove recursive orchestration and user interaction while retaining normal built-ins and future trusted tools. |
bindChildSessionExtensions | Starts child extension hooks in headless print mode. |
shutdownAndDisposeChildSession | Emits session_shutdown once with a deadline, then disposes once; a WeakMap makes teardown idempotent. |
The denied tools are subagent_spawn, subagent_wait, subagent_cancel, subagent_check, subagent_list, workflow, and ask_user. This prevents recursive agent trees and prevents a headless child from blocking on human input.
Lifecycle and result delivery
Direct agent state machine
prompt / steer
┌──────────────────────────┐
▼ │
running ── agent_settled ──> done
│ │
├─ error / abort ────────> error
│ │
└─ takeover send on idle ─┘
agent_settled, rather than only prompt() resolution, is the authoritative terminal signal because it accounts for queued steering, retries, compaction, and automatic continuation. Preflight errors are handled separately when no lifecycle event will arrive.
Exactly one parent delivery path
The manager keeps an interest count for every id currently covered by subagent_wait. At settlement:
- If interest is zero,
onSettledqueues a visible, turn-triggering follow-up message. - If one or more waits are interested, the result is marked consumed and returned through the wait tool instead.
- Cancellation establishes wait interest before aborting so an abort does not also generate a duplicate automatic result.
Outputs are byte- and line-bounded at every parent boundary. Truncated direct results point to the persistent child session file; workflow result and transcript artifacts have separate limits.
Shutdown
Parent session_shutdown stops accepting work, clears child queues, aborts active sessions with deadlines, emits each child’s shutdown hooks, and disposes resources. Workflow runs receive a run-wide abort and bounded task settlement before their completion handlers are awaited.
Persistence and observability
| Artifact | Direct subagent | Workflow |
|---|---|---|
| Native Pi session | Yes; created with SessionManager.create(cwd) and visible in /resume | No; child sessions use SessionManager.inMemory(cwd) |
| Run metadata | In-memory manager registry for the parent session | workflow.json |
| Source and args | Prompt remains in the child session | script.js and optional args.json |
| Result | Final assistant message in session transcript | Bounded result.json |
| Transcripts | Full native session file | Normalized, bounded transcripts.json |
| Location | Pi’s normal session storage | ~/.pi/agent/workflows/<runId>/ |
Operator surfaces
/subagents
takeover.ts provides a full-screen dashboard and a focused takeover view. The dashboard lists status, model, tokens, and elapsed time. The takeover view subscribes directly to session events, renders persisted and streaming messages, shows live tool execution, exposes queued steering, and accepts new input.
If the child is streaming, input uses the SDK steering queue; if it is idle, input starts a new prompt() run on the same session. Streaming repaint is throttled to avoid starving terminal input.
/workflows
The workflows dashboard groups agents by declared phase and shows live status, usage, transcript previews, and persisted run history associated with the launching Pi session. A separate status indicator remains visible while runs are active and until completed runs are acknowledged.
Safety and isolation boundaries
The design combines context isolation, trust-aware resource loading, recursive-tool denial, a process sandbox for model-authored orchestration, IPC validation, bounded serialization, cancellation deadlines, and idempotent disposal.
- Context isolation: children do not inherit the parent conversation; prompts must be self-contained.
- Trust isolation: an alternate child cwd does not inherit trust merely because the parent project is trusted.
- Capability isolation: children can code and inspect resources but cannot spawn more agents, launch workflows, or ask the user.
- Orchestration isolation: workflow JavaScript runs in a separate permission-restricted process, not in the trusted extension runtime.
- Protocol isolation: sandbox IPC is token-authenticated, schema-checked, count-limited, size-limited, and serializable.
- Resource isolation: every child receives a fresh loader/settings runtime; concurrent workflow agents do not share extension state.
Source map
| Path | Role |
|---|---|
extensions/subagents/index.ts | Tool/command registration, model and cwd validation, output delivery, status and message rendering |
extensions/subagents/manager.ts | Direct-agent registry, state machine, wait accounting, concurrency, cancellation, pruning, cleanup |
extensions/subagents/takeover.ts | Live transcript renderer, dashboard, steering and takeover interaction |
extensions/shared/child-session.ts | Shared trust, resources, child tool policy, extension binding, idempotent shutdown |
extensions/workflows/index.ts | Workflow tool control plane, progress, artifact persistence, background execution |
extensions/workflows/controller.ts | Semaphore, call budget, abort propagation, task settlement |
extensions/workflows/sandbox.ts | Restricted child process and validated IPC bridge |
extensions/workflows/sandbox-child.cjs | Minimal runtime implementing script primitives inside the sandbox |
extensions/workflows/runner.ts | In-memory child execution, structured output, progress, usage and transcript capture |
extensions/workflows/meta.ts | Static metadata parsing and safe source preparation |
extensions/workflows/model.ts | Serializable workflow state and presentation helpers |
Design notes and pressure points
- Denylist versus allowlist: children automatically retain new normal tools, which reduces maintenance but requires every new privileged/orchestration tool to be considered for exclusion.
- Caps are scoped, not process-global: direct subagents cap at four per manager and workflows cap at four per run. Multiple workflows plus direct agents can exceed four child sessions across the process.
- Direct spawn applies backpressure by rejection: callers must wait and retry. Workflow fan-out is smoother because excess calls queue behind the semaphore.
- Persistence intentionally differs: direct children favor resumability and takeover; workflow children favor bounded, reproducible run artifacts and guaranteed cleanup.
- Delivery semantics depend on wait interest at settlement: this avoids normal duplicates, but consumers should still treat result messages and explicit waits as two interfaces over the same terminal state rather than separate executions.
- No workflow resume: persisted artifacts support inspection, not continuation. A failed workflow is rerun.
Every child must cross the shared runtime boundary for trust, tool policy, extension startup, and shutdown. Bypassing that module risks inconsistent capabilities or leaked extension/session resources.