# Architecture

## Terms

An **agent** is one Pi `AgentSession`. The **root agent** is the Pi session that loads this extension. A **child agent** is an `AgentSession` that the control plane creates. The **control plane** is the shared object that owns agent state and communication.

## Design selection

| Design | Streaming | Cancellation | Persistence | Process cleanup | Nested agents | Result |
|---|---|---|---|---|---|---|
| In-process `AgentSession` | Direct event subscription | Direct `abort()` call | `SessionManager` data and extension entries | No child process | Direct custom tools | Primary design |
| Pi JSON-mode child process | Event output only | Process signal | Child session file | Process tree cleanup is necessary | No control channel | Rejected |

The primary design uses in-process `AgentSession` objects. It gives the control plane direct access to each session.

The control plane can use events, messages, tools, models, reasoning settings, and cancellation. No unmanaged child process can remain.

## Future process isolation option

pi-subagents does not start one operating-system process for each child.

A process design needs a control protocol, process-tree cleanup, and unmanaged-process detection. It is not part of the current design.

## Control plane

One `ControlPlane` object exists for one root Pi session. It owns these items:

1. The agent graph and canonical paths.
2. One bounded mailbox for each agent.
3. The logical-agent limit and the execution permit limit.
4. The resident child sessions and their use order.
5. The persisted graph and history records.

The root path is `/root`. A child name uses lowercase letters, digits, and underscores. The control plane joins a relative name to the caller path. A canonical path starts with `/root`.

## Limits

Use these defaults and hard caps:

| Property | Default | Maximum (hard cap) |
|---|---|---|
| Resident threads (includes root) | 4 | 64 |
| Active child turns | 3 | 64 |
| Logical agents | 64 | 64 |
| Mailbox capacity (per agent) | 64 | 256 |
| History messages (per agent) | 64 | 128 |
| Message characters | 8,000 | 8,000 |
| Report storage (per final answer) | 64 KiB | 64 KiB |
| Report page characters | 6,000 | 6,000 |
| Retained reports (per agent) | 16 | 16 |
| Child turn retries | 2 | 16 |
| Budget limit (weighted tokens) | 0 (unlimited) | 1,000,000,000,000 |
| Budget reminder threshold | 10% of the limit | Up to the limit |
| Budget weights (sampling, prefill) | 1 | 1,000 |
| Time reminder interval (seconds) | 0 (off) | 3,600 |
| Event journal records | — | 1,024 |
| Inventory lines | 32 (hard cap) | — |
| Inventory characters | 2,000 (hard cap) | — |
| Tool output bytes (Pi-enforced) | 51,200 | No pi-subagents override |
| Tool output lines (Pi-enforced) | 2,000 | No pi-subagents override |
| Tree queue capacity | 256 | 1,024 |
| Task name characters | 64 (hard cap) | |
| Agent path depth (child segments) | 8 (hard cap) | |

A hard cap is the maximum value the code accepts. A project cannot raise a hard cap. It can only select a lower value.

The system also has these internal hard caps:

- 256 active tools per agent.
- 256 inherited extension tool names per agent.
- 64 extension paths per agent.
- 1,024 characters per extension path.
- 4,096 persistence records on restore.
- 128 stored messages per agent on restore.
- 256 persisted mailbox items per agent on restore.
- 64 KiB for the config file.
- 64 roles.
- 64 agent template files.
- 64 template items per collection field.

## Native orchestration

pi-subagents provides native orchestration instead of a packaged skill. The primary agent stays available and integrates results.

It delegates only substantial independent work. Each child receives a narrow, non-overlapping scope.

Built-in `agent_type` roles are leaf roles:

- `scout` - Requests low reasoning with no history and read-only tools.
- `implementation` - Requests medium reasoning.
- `specialist` - Requests high reasoning.

pi-subagents uses Pi model data to clamp built-in role requests. Existing custom roles remain valid.

The primary agent and user keep approval and escalation authority.

## Optional Codex conversion

Pi can load optional packages outside the pidex package root.

pi-subagents uses `ToolInfo.sourceInfo` to find a loaded `@howaboua/pi-codex-conversion` package.

It reads only the package manifest named by `sourceInfo.baseDir` for local package metadata.

It does not scan Pi settings, npm directories, or package dependencies.

It keeps normal extension paths active-only. It keeps conversion paths and tool names, even when inactive.

The child resource loader receives the conversion path and tool names.

The conversion package owns model compatibility, active tool selection, and prompt selection.

pi-subagents does not inspect provider names or model IDs. Role tool limits still apply after inheritance.

If Pi does not expose the package metadata, child behavior stays unchanged.

## Agent creation

`spawn_agent` reserves the canonical path before it creates the child. The reservation prevents duplicate paths. The control plane selects the model and reasoning setting before it creates the `AgentSession`.

### Session setup

Child session setup has separate phases: `load`, `creation`, and `extension_binding`. A setup failure disposes each available partial session. It then removes the child reservation and persisted child record. Failed initialization leaves no resident or logical child.

### Nicknames

Each child gets a default historical nickname. A role can provide its own nickname candidates. The control plane keeps each selected nickname stable across resume.

### Model selection

The system selects the model in this order:

1. Parent model.
2. Configured `defaultChildModel` (overrides parent).
3. Role model from a Markdown template (overrides default).
4. Explicit model from `spawn_agent` (overrides template).
5. Role model from a non-template role (overrides explicit).

The reasoning level follows a similar priority. A role can replace the model, reasoning, and instruction text. A full-history fork cannot select a role.

### Markdown agent templates

Trusted projects can add Markdown agent templates in `.pi/subagents/agents/*.md`. Frontmatter selects the model, reasoning, service tier, work class, tools, skills, and extensions. The Markdown body supplies child instructions.

Relative resource paths resolve from the template directory. pi-subagents rejects absolute paths, traversal, malformed fields, excessive collections, and reserved built-in names. It does not load project templates in untrusted projects.

### Default and role instructions

Default child instructions apply when a role does not supply instructions. Role instructions have higher priority.

### Service tiers

Service-tier validation uses the current model catalog. A supported role tier has priority. An explicit unsupported tier fails.

A model change removes an inherited unsupported tier.

### Fork turns

`fork_turns` accepts `none`, `all`, or a positive safe-integer string.

An omitted value means `none` for a work-class role. It means `all` for other children.

- A full-history fork (`all`) takes a live snapshot of the parent context. It keeps the parent role behavior.
- A number copies the last specified user turns and their results. It keeps user messages and final assistant answers. It removes tool calls and tool results.

### Tool inheritance

Pi exposes active tool names. It does not expose another extension's execution callback.

pi-subagents reloads active file-backed extensions through the public Pi resource loader. It then applies child role restrictions.

SDK tools have no reloadable file path. pi-subagents cannot copy their execution callbacks.

pi-subagents maps root `exec` and `exec_command` tools to equivalent local Pi tools. Child role restrictions apply after this mapping.

An optional conversion package can provide inactive tools. pi-subagents passes their names without adding a dependency.

## Communication

`send_message` adds a mailbox item. It does not start a child turn.

`followup_task` adds a work item. It starts a turn when the child is idle. If the child is active, Pi delivers the item as steering input at the next message boundary.

Report truncation in relays is intentional (bounded safety at the 8,000-character message limit). Thinking content is capped at 2,000 characters per part so the report text keeps the remaining budget; model and reasoning header lines are preserved.

Completed child answers additionally persist out of band as `report` records (bounded to 64 KiB, newest 16 per agent) so the tail survives. The completion notice carries a small `report` metadata object (`available`, `report_id`, `original_chars`, `stored_chars`, `complete`, `page_chars`). `get_agent_report` pages the stored text without loading the child session: repeat with `offset = next_offset` until `next_offset` is null, and check `complete` before claiming the answer is lossless. Records survive child eviction and session restore; legacy turns without a report return `available: false` and must be rerun.

`waitAgentEnabled` controls `wait_agent`. The default is `true`.

When enabled, the tool waits for mailbox activity. It returns a short summary without hidden child messages.

The root Pi `input` event wakes a wait for interactive steering and RPC prompt steering.

Direct RPC `steer` does not emit this event. It also does not abort the active tool signal.

The wait checks `ctx.hasPendingMessages()` every 25 milliseconds. This check detects direct RPC steering.

`interrupt_agent` calls `AgentSession.abort()`. It keeps the agent in the graph and returns the status from before the call.

A steering failure does not drain mail. The next valid turn can receive the same bounded items in their original order.

Only the root has full coordination authority. A child with the `mayDelegate` value `false` can use `send_message` and `followup_task` only with a target that is its parent. A child can use `interrupt_agent` only on its own descendants. A leaf child has no descendants. It cannot interrupt anyone.

## Child failure isolation

Each child has a health value: `healthy` or `unhealthy`. An unhealthy child always has a terminal status.

Failures use these structured phases: `load`, `creation`, `extension_binding`, `prompting`, `steering`, `aborting`, and `disposal`. The control plane bounds each failure message before it stores or sends the message.

A prompt or abort failure makes only its child unhealthy. The control plane stores the terminal status and failure phase before it sends the parent notice. It then removes listeners and disposes the failed session.

A steering failure stays an operation error because the active turn can continue. Its undelivered mail stays queued.

Parent notification failure cannot reject a completed child run. A full parent mailbox still receives an activity signal. Root and sibling sessions keep their own state, mailboxes, tools, and execution permits.

### Shutdown

Shutdown first closes mailboxes and marks child statuses as `shutdown`. It then attempts abort and disposal for every resident child. One failed abort or disposal does not stop cleanup for another child. Shutdown does not wait on a failed child operation that cannot settle.

## Resilience and observability

### History compaction

`autoCompactHistory` defaults to `true`. When a child history exceeds `historyCapacity`, the control plane compacts it instead of dropping the oldest messages. It keeps the original task envelope. It adds one bounded rollup line that summarizes the dropped region. It keeps the newest messages. The rollup is a user-role message. It flows through the existing persistence path. A history with no leading user message degrades to the tail-only trim.

### Tree spend budget

One in-memory budget accumulates weighted spend across all child turns. The root does not consume budget accounting.

Weighted spend per turn is `outputTokens * samplingWeight + nonCachedInput * prefillWeight`. When provider token counts are absent, the assistant text length (`text.length / 4`) stands in for the output side.

`budgetLimit` sets the limit in weighted tokens. The default `0` is unlimited. `budgetReminderAt` sets the reminder threshold. The default is 10% of the limit. A reminder fires once per crossed threshold per agent. It is a bounded mailbox message ("Budget: N units remaining"). Exhaustion rejects new child turns with "session budget exhausted". The agent status reflects the rejection.

Budget state is in memory. It does not survive an extension reload.

### Child turn retry

A child turn retries transient prompt failures. `maxChildTurnRetries` defaults to `2`. The delay before retry `n` is `250 ms * 2^(n-1)`. Progress shows "Reconnecting… n/max" during the backoff. Abort, interrupt, budget, and steering errors never retry. After the final retry, the failure path marks the child unhealthy.

### Sibling inventory

At spawn time, child instructions receive a bounded agent inventory. It lists the parent and live siblings as `- <path>: <nickname>` lines. It is bounded to 32 lines and 2,000 characters. The inventory is a snapshot at spawn time. It does not update during the child turn.

### Time reminders

`timeReminderIntervalSeconds` configures a time reminder. The default `0` disables it. When a child turn runs longer than the interval, the control plane queues a bounded "Current time: <ISO UTC>." message to the child mailbox. The existing delivery path carries it at the next message boundary. The reminder never interrupts a streaming turn. Timers stop at shutdown.

### Lifecycle event journal

The control plane appends version-1 `event` records for lifecycle events: `spawn`, `turn_start`, `turn_end`, `completion`, `error`, and `budget`. Each record carries a monotonic sequence. Restore keeps at most 1,024 event records. The `chimera_stats` tool derives counters from the journal: `spawned`, `completed`, `errored`, `interrupted`, `turns`, `retries`, `avg_turn_ms`, and `budget_reminders`.

### Completion guidance

A failed child notice carries a fixed next-action hint: "Child turn failed. Re-task it with followup_task or spawn a replacement." The hint is bounded. It fits inside the existing status budget.

## Live progress

Managed child sessions convert message and tool events into bounded progress updates.

The control plane stores the phase, active tool, times, and bounded activity text. It throttles text updates to 100 milliseconds.

The TUI refreshes at most four times each second.

Progress is transient. pi-subagents does not write progress fields to persistence records. It does not add progress text to model context. Restored agents receive only a new presentation state from their stored terminal status.

The agent view shows path, nickname, state, phase, active tool, elapsed time, last activity time, live activity, and stored messages. Completed, failed, and interrupted turns stop their elapsed timer. Headless modes do not create timers or call UI methods.

## Residency

The control plane can unload an inactive agent. It stores bounded history before it calls `dispose()`.

It unloads the least-recently-used eligible agent first. The path breaks ties. This rule makes eviction deterministic.

An active agent is not eligible for eviction. An agent with mailbox work is not eligible. A target operation reloads an unloaded agent before it uses the agent.

A completed or errored agent can resume after eviction. An interrupted agent remains `not_found` after eviction, as in Codex.

## Persistence and resume

The extension uses `pi.appendEntry()` for graph records, agent records, and bounded message records. These custom entries do not enter model context. `session_start` reads the root branch and rebuilds the current graph.

Each agent record contains identity, parent, role, model, status, health, failure, and use data.

Each message record contains one bounded message. The restore step keeps only the newest bounded history for each agent.

The extension starts child resources only after `session_start`. Root trust propagates to every child session. Untrusted roots do not load project pi-subagents configuration.

Each child registers lifecycle cleanup through `session_shutdown`: stop work, abort active sessions, remove listeners, clear timers, and dispose. Cleanup is idempotent.

## Pi user experience

In an interactive session, `/subagents` (bare) opens a read-only agent picker; `tell <target> <message>` and `steer <text>` are subcommands. `/subagents-tool-check` verifies tool registration.

The picker shows the path, role, state, and unread count. Selecting an agent shows its bounded transcript in a read-only view. The view keeps fewer than 50 kilobytes and at most 2,000 rendered lines.

RPC, print, and JSON modes keep command and shortcut registrations stable. Each UI operation checks `ctx.hasUI`.

The root owns user input. Child `AgentSession` objects have no direct editor. Child work enters only through the initial task and the eight coordination tools. This design rejects direct user input to a parent-owned child.

The extension also registers navigation shortcuts. The shortcuts move through agents in first-seen order. A terminal agent remains visible while its logical record exists.

## Provider transport

Codex adds encrypted-input and output-schema metadata to Responses API tools.

Pi custom tools use TypeBox input schemas. Pi has no public field for either Codex annotation.

Pi provider transport controls request encryption. pi-subagents returns the required runtime JSON output shapes.

## License rule

The Codex source uses the Apache License 2.0. Pi uses the MIT license. This project uses public contracts, current service-tier capability data, and public Pi APIs. `LICENSE` and `NOTICE` contain the applicable attribution.

## Talk-while-they-work (operator routing)

While the tree has children and the root's turn is active, typed interactive
input that does not start with `/` is routed to the root's mailbox as a
`user` envelope (author `operator`) instead of reaching the host: the `input`
handler returns `{ action: "handled" }` before any steer/follow-up branch, so
the running turn is never interrupted and children keep working.

- `src/input-routing.ts` holds the pure routing decision (unit-tested):
  `/`-prefixed input is never routed; `!<text>` is the escape hatch
  (transform, strip the prefix, host path); `userInputRouting: "host"`
  disables routing; empty trees and idle roots pass through.
- `ControlPlane.routeUserInput` pushes a persisted `user` envelope into the
  root mailbox WITHOUT immediate delivery; `wait` surfaces it at the next
  wait boundary as `Mailbox message received` with a bounded preview
  (`from`, `kind`, `text` ≤ `maxMessageChars`, `total`), drains the mailbox,
  and delivers every envelope to the root session as a queued steer (never
  aborting the turn). The host/steer path keeps returning
  `Wait interrupted by new input.` — the two results are deliberately
  distinct.
- `/subagents tell <target> <message>` (command, so it executes immediately
  even during streaming) delivers through the existing mailbox queue with
  root authorship; an idle target starts a turn, a running target receives
  the message at the next steer boundary, and the root itself receives a
  `user` envelope. `/subagents steer <text>` is the deliberate escape hatch:
  it delivers the text as a steer and wakes any pending wait with the steer
  signal.
- User envelopes reuse the `mail` persistence records, so they survive turns
  and session restores; the `#restore` whitelist accepts the `user` kind.

## Split notes (pi-chimera → pi-subagents)

This repository holds the multi-agent extension that previously lived in
`pi-chimera`. The split (GOAL.md Workstream 2):

- **History:** the pi-subagents scaffold commit (`f752d7c`) is the repo root;
  the multi-agent code landed here as new commits, so moved-file history is
  not preserved. `plans/001` was relocated byte-for-byte as review material
  (the authorized plans/ relocation); `plans/README.md` was updated only to
  keep references consistent.
- **Renames:** config file `.pi/pi-chimera.json` → `.pi/subagents.json`;
  agent templates `.pi/pi-chimera/agents/` → `.pi/subagents/agents/`;
  persistence customType `"pi-chimera"` → `"subagents"`; session name
  `pi-chimera-tools-ok` → `subagents-tools-ok`; command
  `pi-chimera-tool-check` → `subagents-tool-check`; `/pi-chimera-agents` is
  retired — `/subagents` is the primary command (bare picker, `tell`, and
  `steer` subcommands). Tool names are unchanged (`spawn_agent` … 
  `chimera_stats`).
- **Compat contract:** the `@howaboua/pi-codex-conversion` discovery logic
  (`src/extension-inheritance.ts`) is duplicated as a tiny contract module in
  both this repo and the pi-chimera compat extension, because Pi 0.83.0
  offers no cross-extension API to share code or call another extension's
  helper. pi-subagents computes the metadata itself from `pi.getAllTools()`
  and projects conversion tools into child sessions; the pi-chimera compat
  extension produces the same metadata for its own reporting tool/command.
  Neither imports the other; both stay standalone. The behavior matrix
  (subagents alone / + compat / + compat + conversion / conversion alone) is
  encoded in tests.
