Subagents system architecture

How Pi creates isolated child sessions, coordinates direct delegation and scripted workflows, streams progress into the parent, and tears every child down safely.

Core idea

This is one child-session foundation with two orchestration surfaces: lightweight, persistent background subagents for ad hoc delegation, and bounded, in-memory workflow agents for multi-phase fan-out.

Direct concurrency
4
Spawn rejects above the cap
Workflow concurrency
4
Per run, semaphore queued
Workflow call budget
32
Agent calls per run
Tracked direct agents
64
Older settled sessions are pruned

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
Two execution modes over one child-session policy
DimensionDirect subagentWorkflow agent
Entry pointsubagent_spawnagent() inside workflow
Primary useOne-off background delegationOrdered phases, fan-out, aggregation
Session storagePersistent Pi session fileIn-memory session
Result deliveryAutomatic follow-up or explicit waitReturn value to orchestration script
Human controlInspect, steer, continue, abortObserve run and transcripts; no resume
Concurrency behaviorReject new spawn when four are activeQueue 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_spawn creates a child and returns immediately with an sa-N id.
  • subagent_wait blocks on selected ids and marks their settlement as consumed, preventing duplicate automatic follow-ups.
  • subagent_cancel clears queued input, aborts active work, and preserves the partial session transcript.
  • subagent_check reads status and current streaming text without consuming the result.
  • subagent_list returns 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.

  1. meta.ts parses the source with Acorn, extracts only static literal metadata, rejects other module syntax, and removes the metadata declaration without shifting line numbers.
  2. sandbox.ts launches a permission-restricted Node child process. The script has no imports, eval, timers, filesystem, network, or process API.
  3. The sandbox can only send authenticated IPC requests for phase and agent. Prompts and options cross back to the trusted parent process.
  4. RunController enforces a four-agent semaphore, a 32-call budget, abort propagation, task tracking, and bounded settlement.
  5. runner.ts creates one in-memory AgentSession per call, reports normalized transcript and usage progress, then always disposes it.
  6. 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.

Shared runtime responsibilities
FunctionResponsibility
createChildResourcesCreates fresh settings and resource loaders for the child cwd; loads global/package resources plus trust-gated project resources such as skills and AGENTS.md.
resolveStandaloneChildProjectTrustInherits 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.
childToolPolicyUses a denylist to remove recursive orchestration and user interaction while retaining normal built-ins and future trusted tools.
bindChildSessionExtensionsStarts child extension hooks in headless print mode.
shutdownAndDisposeChildSessionEmits 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, onSettled queues 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

What survives each execution path
ArtifactDirect subagentWorkflow
Native Pi sessionYes; created with SessionManager.create(cwd) and visible in /resumeNo; child sessions use SessionManager.inMemory(cwd)
Run metadataIn-memory manager registry for the parent sessionworkflow.json
Source and argsPrompt remains in the child sessionscript.js and optional args.json
ResultFinal assistant message in session transcriptBounded result.json
TranscriptsFull native session fileNormalized, bounded transcripts.json
LocationPi’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

Defense in depth

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

Primary implementation files
PathRole
extensions/subagents/index.tsTool/command registration, model and cwd validation, output delivery, status and message rendering
extensions/subagents/manager.tsDirect-agent registry, state machine, wait accounting, concurrency, cancellation, pruning, cleanup
extensions/subagents/takeover.tsLive transcript renderer, dashboard, steering and takeover interaction
extensions/shared/child-session.tsShared trust, resources, child tool policy, extension binding, idempotent shutdown
extensions/workflows/index.tsWorkflow tool control plane, progress, artifact persistence, background execution
extensions/workflows/controller.tsSemaphore, call budget, abort propagation, task settlement
extensions/workflows/sandbox.tsRestricted child process and validated IPC bridge
extensions/workflows/sandbox-child.cjsMinimal runtime implementing script primitives inside the sandbox
extensions/workflows/runner.tsIn-memory child execution, structured output, progress, usage and transcript capture
extensions/workflows/meta.tsStatic metadata parsing and safe source preparation
extensions/workflows/model.tsSerializable 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.
Most important invariant

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.