A model in one terminal decides that three investigations can proceed independently. A moment later, the screen shows three named workers, each with its own conversation, tools, status, and result. Where did the extra agents come from?
The tempting answer is, “someone added another agent loop.” The more useful answer is the opposite: pi-subagents is a Pi extension and control plane. It turns Pi’s existing session abstraction into a managed fleet; it does not replace Pi’s model stream, tool loop, message history, or session runtime. “Control plane” is this chapter’s architectural interpretation of the records, queue, ownership, and control surfaces visible in the extension—not a name declared by the project. The extension’s runner ultimately creates a normal Pi AgentSession and calls session.prompt(). Pi’s SDK constructs the underlying Agent, and AgentSession delegates execution to that agent. (fleet state, pi-subagents runner, Pi session factory, Pi prompt path)
Version note. This chapter describes pi-subagents 0.18.0 at commit
3f9d35cd078d18a141eb5a6d8f4fc5010d756280and Pi 0.84.2 at commit914cf1472e715297caa30db4b9535d534a9eb718. Every implementation statement is pinned to those baselines. The pi-subagents manifest declares 0.18.0; Pi’s package declares 0.84.2; and the extension’s development dependencies select pi-ai, pi-coding-agent, and pi-tui 0.84.2. (pi-subagents package, Pi package, extension dependencies)
Recall, then predict
Before reading on, retrieve three ideas from ordinary TypeScript systems:
- A runtime executes work; a control plane decides what work exists and when it runs.
- An object can own a lifecycle without implementing the machinery used inside that lifecycle.
- Composition is easiest when a lower layer exposes a stable seam: create, start, observe, redirect, dispose.
Now predict: if Pi already knows how to run one coding-agent session, which layer should own model streaming and tool execution for the fourth agent? Which layer should own “queued,” “running,” “completed,” names, parent/child ownership, and result retrieval?
Keep your answer. We will test it against the source.
The architecture in one trace
main Pi AgentSession
│ model calls extension tool: Agent(...)
▼
pi-subagents ToolDefinition.execute()
│
▼
AgentManager ── record / queue / status / ownership
│
▼
runAgent() ── choose prompt, model, tools, resources, SessionManager
│
▼
Pi createAgentSession() ── new Agent + new AgentSession
│
▼
child session.prompt() ── Pi's ordinary agent/tool loop
│ events
▼
pi-subagents updates record, UI, notification, and tool result
Trace it from the top.
1. Pi supplies the extension seam. Its ExtensionAPI lets an extension subscribe to lifecycle events and register LLM-callable tools. The loader stores each registered tool on the extension and refreshes the live registry. (API contract, loader implementation)
2. pi-subagents installs controls, not a replacement runtime. Its extension factory defines and registers Agent, get_subagent_result, and steer_subagent. The Agent schema adds orchestration choices such as agent type, model override, foreground/background execution, resume, and context inheritance. (tool names, Agent definition, control-tool registration)
3. The manager turns a fresh call into managed work. For an unscheduled, non-resume background Agent call, execute() invokes manager.spawn(). AgentManager allocates an id and record, assigns status, optionally queues the run, and later maps completion or failure back onto that record. Scheduled calls register jobs instead, while resume calls reuse an existing record and session through manager.resume(). (tool dispatch, record and queue creation, settlement)
4. The runner configures a child using Pi building blocks. runAgent() constructs the child’s system prompt and resource loader, chooses a Pi SessionManager, supplies model and tool policy, then calls Pi’s createAgentSession(). It binds the child’s extensions before the first prompt. (prompt and resource construction, session construction)
5. Pi runs the child. createAgentSession() resolves services, constructs an Agent, wraps it in AgentSession, and returns it. The extension subscribes to session events for turns, text, tools, usage, and compaction, then starts work with await session.prompt(effectivePrompt). (pi factory, pi construction, extension observation and start)
6. The control plane translates the outcome. For a top-level background child, the manager’s completion callback emits extension lifecycle events, appends a compact record, and routes unconsumed results through group or individual notification paths. A foreground call instead returns the settled record’s output inline as the Agent tool result. (background outcome routing, foreground outcome)
The key seam is therefore AgentSession: pi-subagents owns many-session coordination; Pi owns what a session is and how it runs.
Who owns what?
| Concern | Owner at these commits | Evidence |
|---|---|---|
| Model streaming and the agent/tool loop | Pi | Pi’s Agent owns the transcript, lifecycle events, tool execution, and message queues. The SDK constructs it with the model stream function; AgentSession invokes agent.prompt() and agent.continue(). (Agent ownership, factory, run) |
| Messages, session persistence, event subscription | Pi | AgentSession listens to agent events, appends completed messages through SessionManager, and exposes subscribe(). (session internals, persistence, subscription) |
| Child prompt, model, tool, extension, and session policy | pi-subagents | runAgent() resolves these inputs and passes them into createAgentSession(). (resolution) |
| Fleet records, ids, queueing, foreground/background status | pi-subagents | AgentManager owns the record map, queue, spawn path, and completion transitions. (manager state, spawn path) |
| Steering and result retrieval surfaces | pi-subagents over Pi | The extension resolves an agent record and exposes tools; steering ultimately calls the child’s session.steer(), which queues a user message through pi. (extension controls, pi steering) |
| Child shutdown | Shared contract | Pi exposes session disposal; the extension closes the lifecycle it opened and disposes retained child sessions through its manager. (pi disposal, extension teardown) |
Notice the shape: pi-subagents adds policy and coordination around a reusable runtime object. Calling it “another loop” erases the most important design decision.
Read the seam like a TypeScript engineer
The following is a teaching model, not a type copied from either repository:
type FleetSeam = {
create(config: ChildPolicy): Promise<AgentSession>;
start(session: AgentSession, prompt: string): Promise<void>;
observe(session: AgentSession, listener: SessionListener): Unsubscribe;
redirect(session: AgentSession, message: string): Promise<void>;
close(session: AgentSession): void;
};
Pi already supplies the substance behind those five operations: createAgentSession(), prompt(), subscribe(), steer(), and dispose(). (session factory, session lifecycle, prompt, steer)
pi-subagents supplies ChildPolicy and the collection around it: it resolves configuration, retains AgentRecords, schedules starts, and translates session events into fleet-level state. (runner policy, manager collection) This separation is the chapter’s compact mental model: sessions do work; the fleet coordinates sessions.
Boundary myths
Myth 1: “A subagent is a recursive call into the parent agent.”
It is a separate AgentSession with its own selected session manager, resources, active tools, and prompt. Parent conversation inheritance is optional; when enabled, this extension renders user and assistant text plus compaction summaries from the parent branch, skips tool results, and prepends that rendering to the child prompt. It is a text projection, not a clone of the parent session. (child session options, inheritance path, context projection)
Myth 2: “Separate session means separate OS process.”
Not here. pi-subagents imports createAgentSession() and invokes it in the same TypeScript runtime. By contrast, Pi’s bundled subagent example explicitly launches a separate pi process, reads JSON events from stdout, sends SIGTERM on abort, and schedules a conditional SIGKILL attempt. These are two valid compositions over Pi, with different lifecycle boundaries. (in-process construction, child creation, upstream process example, spawn and abort)
Myth 3: “Every child reloads pi-subagents and creates another global fleet manager.”
Child sessions can load normal extensions, but this extension marks child construction with async-local context and returns early if its factory is re-entered there. Nested delegation is instead injected as scoped custom tools by the existing manager. (child-context marker, re-entry guard, nested-tool injection)
Myth 4: “The fleet owns the truth of each conversation.”
The manager’s record is orchestration state: status, id, counters, result, and a reference to the session. The actual conversation remains on the Pi AgentSession, and verbose result retrieval formats record.session.messages. (record creation, conversation readout)
Retrieval practice
Answer before opening each disclosure.
1. Which object is the architectural seam between “run one agent” and “manage a fleet”?
Answer
Pi’s AgentSession. The extension creates, observes, steers, and disposes multiple instances; Pi executes each instance. (child session path)
2. A child emits tool_execution_start. Which layer produced the event, and which layer projects it into fleet activity?
Answer
Pi’s session relays the agent event. For tool_execution_start, pi-subagents forwards it to the activity tracker so the UI can show the active tool. On the matching tool_execution_end, the manager increments record.toolUses and the activity tracker clears the active tool. (Pi relay, extension subscriber, manager counter)
3. What would you look for to detect a second agent-loop implementation?
Answer
Code that directly drives model calls, interprets tool calls, executes tools, and repeats until a stop condition. The traced child path instead delegates that work to createAgentSession() and session.prompt(). (construction, prompt)
Transferable lesson
Inference from this design: when a single-worker runtime exposes creation, prompting, events, redirection, and disposal, multi-worker orchestration can be a higher layer rather than a fork of the runtime. That keeps domain policy—names, queues, ownership, background completion—out of the execution kernel, while letting every worker reuse the kernel’s model, tool, message, and session semantics. Pi’s relevant seam is visible in createAgentSession(), subscribe(), prompt(), steer(), and dispose(). (factory, session lifecycle API, prompt and steer)
When reading the remaining chapters, keep asking one question: is this mechanism part of a Pi session, or policy for coordinating many Pi sessions? That question is the map.
Next: Chapter 2 crosses the first concrete seam: how Pi loads the extension and turns delegation policy into the model-callable Agent tool.
Source index
- pi-subagents
src/index.ts— extension entry point and public control tools. - pi-subagents
src/agent-manager.ts— fleet records, scheduling, settlement, and teardown. - pi-subagents
src/agent-runner.ts— adapter from orchestration policy to a child Pi session. - real-Pi integration test — verifies the runner against a real Pi session rather than a mocked session factory.
- nested real-session test — exercises a parent and two real child sessions with a deterministic model backend.
- Pi
sdk.ts—createAgentSession()and runtime assembly. - Pi
agent-session.ts— session lifecycle, events, prompting, steering, and disposal. - Pi process-based subagent example — a useful contrast with pi-subagents’ in-process composition.