---
slug: "oira666-pi-subagent"
source_type: "readme"
source_url: "https://cdn.jsdelivr.net/gh/gee666/pi-subagent@master/README.md"
repo: "https://github.com/gee666/pi-subagent"
source_file: "README.md"
branch: "master"
---
# Pi Subagent

Delegate tasks to specialized subagents.

## Install

```bash
pi install npm:oira666_pi-subagent
```

Or via git:

```bash
pi install git:github.com/gee666/pi-subagent.git
```

## Remove

```bash
pi remove npm:oira666_pi-subagent
```

## How It Works

Each subagent runs as a **separate `pi` process** — fully isolated memory, its own model/tool loop.
Processes are spawned via the operating system and communicate through JSON-line stdout.
Subagent sessions are persisted separately under a `sessions-subagents` directory (a sibling of Pi's normal `sessions` directory), so they can be resumed without mixing into the main session list.

- Full OS-level isolation — a crashed subagent cannot affect the parent
- True parallel execution across all CPU cores
- Each subprocess boots a fresh Node.js runtime
- Uses the same Pi CLI entrypoint as the parent process when available

Each subagent receives only the task string. The main agent in turn receives
only the **final text output** from subagents (no tool calls, no reasoning).

## Tool Call Shape

The delegation tool is called `subagents` (older sessions may contain the
legacy name `subagent`, which is still recognized when reading history):

```json
{ "tasks": [{ "agent": "code-writer", "task": "Implement the API" }] }
```

Multiple tasks run in parallel:

```json
{
  "tasks": [
    { "agent": "code-writer", "task": "Draft the implementation" },
    { "agent": "code-reviwer", "task": "Review the plan" }
  ]
}
```

Each task supports `agent` (the agent *type* to spawn) and `task`.

## Tool Prompt Overrides

The complete LLM-facing description of each extension tool can be replaced in
`pi-subagents.json`. Supported locations, from lowest to highest priority:

1. `~/.pi/pi-subagents.json`
2. `$PI_CODING_AGENT_DIR/pi-subagents.json` (normally `~/.pi/agent/pi-subagents.json`)
3. The nearest trusted project `.pi/pi-subagents.json`, walking up from the current directory

Project values override global values per tool. Missing prompts keep their
built-in defaults. Use a JSON object for `tool-prompts`:

```json
{
  "tool-prompts": {
    "subagents": "Your complete replacement prompt for the subagents tool.",
    "resume_subagents": "Your complete replacement prompt for the resume tool."
  }
}
```

## Bundled Agents

Four built-in agents ship with the extension and remain available alongside custom agents by default:

- `code-writer` — implementation and refactoring
- `code-reviwer` — code review and risk finding
- `code-architect` — technical design and approach selection
- `team-lead` — decomposition and delegated multi-agent implementation

## Defining Agents

Create Markdown files with YAML frontmatter:

- **User agents:** `~/.pi/agent/agents/*.md`
- **Env agents:** `$PI_CODING_AGENT_DIR/agents/*.md` *(when `PI_CODING_AGENT_DIR` is set)*
- **Project agents:** `.pi/agents/*.md` *(may prompt for confirmation — see `PI_SUBAGENT_CONFIRM_PROJECT_AGENTS`)*

Agent discovery priority (highest wins on name collision): project > env/user > built-in.
Built-in agents remain available alongside custom agents unless
`PI_SUBAGENT_HIDE_BUILTIN_AGENTS=true`. A custom definition with the same name
as a built-in agent overrides that built-in definition.

```markdown
---
name: writer
description: Expert technical writer
thinking: low
first-layer: enabled
last-layer: disabled
tools: read,write
---

You are an expert technical writer focused on clarity and conciseness.
```

### Frontmatter Fields

| Field         | Required | Default              | Description                                              |
| ------------- | -------- | -------------------- | -------------------------------------------------------- |
| `name`        | Yes      | —                    | Agent identifier used in tool calls                      |
| `description` | Yes      | —                    | What the agent does (shown to the main agent)            |
| `model`       | No       | Current parent model | Legacy fallback only when live parent model context is unavailable |
| `thinking`    | No       | Pi default           | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`       |
| `tools`       | No       | `read,bash,edit,write` | Comma-separated built-in tools                         |
| `first-layer` | No       | `enabled`            | Set to `disabled` to hide/block this agent at depth 1    |
| `last-layer`  | No       | `enabled`            | Set to `disabled` to hide/block this agent at max depth  |

Available tools: `read`, `bash`, `edit`, `write`.

The Markdown body becomes the agent's system prompt (appended to Pi's default, not replacing it).

## Delegation Guards

Depth and cycle guards prevent runaway recursive delegation. Layer availability is evaluated for the child being launched: depth 1 is the first layer, and `PI_SUBAGENT_MAX_DEPTH` is the last layer. The bundled `team-lead` agent sets `last-layer: disabled` so it cannot consume the final delegation layer.

| Config                         | Default | Description                                      |
| ------------------------------ | ------- | ------------------------------------------------ |
| `--subagent-max-depth` / `PI_SUBAGENT_MAX_DEPTH` | `3` | Max delegation depth (0 disables delegation) |
| `--subagent-prevent-cycles` / `PI_SUBAGENT_PREVENT_CYCLES` | `true` | Block same agent in delegation chain |

```bash
pi --subagent-max-depth 2         # one nested level
pi --subagent-max-depth 0         # disable delegation entirely
pi --no-subagent-prevent-cycles   # allow cycles (not recommended)
```

## Parallel Limits

| Env Var                          | Default | Description                              |
| -------------------------------- | ------- | ---------------------------------------- |
| `PI_SUBAGENT_MAX_PARALLEL_TASKS` | `30`    | Max tasks per single call                |
| `PI_SUBAGENT_MAX_CONCURRENCY`    | `8`     | Max subagents running simultaneously     |

## Timestamps & Status Footer

Subagent tool calls and live activity lines render a dim `hh:mm:ss` timestamp
(call start, per-subagent run start, and each live log entry).

In the interactive TUI the extension publishes the combined `total` usage line
(parent + all subagents, recursively) via Pi's normal `ctx.ui.setStatus()`
status line. Pi renders all extension statuses on the same footer status line.

## Steering Running Subagents

While a `subagents` tool call is running, mid-stream steering input can be broadcast to one or more child agents. The extension uses Pi's `InputEvent.streamingBehavior` metadata when available, so idle prompts and queued follow-ups continue to the parent normally; only true `steer` inputs open the broadcast routing prompt.

## Subagent Session Resume

> Requires Pi **0.81.0 or newer**. Crash recovery uses Pi's public full Provider SDK and session-replacement lifecycle.

Subagent subprocesses save sessions in `sessions-subagents`. When a main Pi session is resumed and its latest branch contains an unfinished `subagents` tool call (aborted, errored, or closed by Pi's synthetic unfinished-tool error), the extension can resume that delegation from the saved subagent sessions.

The same detection also runs after navigating the session tree in the TUI (Esc navigation): if you jump back to a point whose branch ends in an unfinished `subagents` call, the extension offers to resume those subagents from their saved sessions.

- TUI mode asks: **Resume subagents?**
- Non-UI modes (`pi -p`, JSON/RPC) resume automatically.
- Already-finished subagents are reused as completed; unfinished ones continue from their own saved sessions.
- Nested subagents use the same mechanism recursively.
- Provider fallback goes through the selected model's effective Pi provider, so custom providers, custom APIs, auth-derived endpoints, headers, and provider-scoped environment are preserved.
- Pending resume state and delayed callbacks are discarded on `/resume`, `/new`, `/fork`, and `/reload`, preventing stale work from an old runtime from leaking into the replacement session.

| Env Var | Default | Description |
| --- | --- | --- |
| `PI_SUBAGENT_RESUME_PROMPT` | `true` | Set to `false` to suppress the TUI yes/no prompt and auto-resume. |
| `PI_SUBAGENT_DISABLE_RESUME` | `false` | Set to `true` to disable automatic subagent resume detection entirely. |

Note: crash-resume covers `subagents` calls only. An interrupted `resume_subagents` call is not replayed automatically — the model can simply issue it again, since names stay valid (see below).

## Resumable Subagents by Name (`resume_subagents`)

Every subagent run is assigned a unique, durable name derived from its agent
type plus a per-type counter — `code-writer-01`, `code-writer-02`,
`code-reviewer-01`, ... The name is returned together with the subagent's
results and shown in the TUI tree.

The `resume_subagents` tool continues named subagents with a new task while
preserving their full previous context:

```json
{ "resumes": [{ "subagent": "code-writer-01", "task": "Now also update the tests." }] }
```

Naming is deliberately unambiguous: `agent` (in `subagents`) selects an agent
*type* to spawn; `subagent` (in `resume_subagents`) addresses an already-run
subagent *instance* by its unique name.

- All resumes in one call run **in parallel**.
- Names are unique within one delegation tree (everything spawned from one
  top-level session) and are persisted in a registry file under the subagent
  session root, so they survive restarts: you can resume a subagent in a later
  session of the same conversation.
- The registry location and the session's ownership identity are stored in the
  session itself (a custom metadata entry). Pi assigns resumed/branched
  sessions a new internal session id, but the persisted identity (plus a
  `parentSession` ancestor-walk fallback for sessions created before it
  existed) keeps the whole tree's names alive across process restarts — for
  the top-level session and every nested subagent alike.
- **Ownership & forks**: the agent that spawned a subagent (its *owner*)
  resumes the original session. A parent may pass names to its own subagents
  (in their task text); when a child resumes a name created by an ancestor, it
  transparently gets a **private fork** of that subagent (a copy of its
  session), so the owner's copy is never polluted by the child's continuation.
  Each child gets exactly **one fork per name** and keeps reusing it on
  subsequent resumes. Fork session locations are persisted too.
- Concurrent resumes of the same target are rejected (in-process and
  cross-process via crash-tolerant registry markers), because two processes
  continuing the same session file would corrupt it.
- If the original agent definition file has been removed, the resume still
  works: the registry remembers the agent's model/tool restrictions and the
  session itself carries the context.

| Env Var | Default | Description |
| --- | --- | --- |
| `DISABLE_RESUMABLE_SUBAGENTS` | `false` | Set to `true`/`on`/`1` to disable resumable subagents entirely: no names are allocated, the `resume_subagents` tool is not registered, and the system prompt omits the feature. |
| `PI_SUBAGENT_NAMES_FILE` | (internal) | Path of the shared name registry, propagated to child processes so the whole delegation tree allocates unique names. |

## Agent Discovery

| Env Var                 | Description                                                  |
| ----------------------- | ------------------------------------------------------------ |
| `PI_CODING_AGENT_DIR`   | Override Pi's agent config directory. Agents are read from `$PI_CODING_AGENT_DIR/agents/*.md`, and tool prompts from `$PI_CODING_AGENT_DIR/pi-subagents.json`. |
| `PI_SUBAGENT_HIDE_BUILTIN_AGENTS` | Set to `true`/`on`/`yes`/`1` to hide all bundled agents. By default they are available alongside custom agents. |

## CLI Argument Proxying

Flags passed to the parent `pi` process are forwarded to subagent child
processes, so they inherit the same provider, API key, and other runtime settings. At every new launch, the extension explicitly passes the parent's currently active model; changing `/model` mid-conversation therefore affects all subsequently started subagents. Flags the extension manages itself are blocked from being forwarded.

**Always forwarded verbatim:**

| Flag(s) | Purpose |
| --- | --- |
| `--provider` | AI provider |
| `--api-key` | API key |
| `--system-prompt` | Base system prompt override |
| `--session-dir` | Session storage directory |
| `--models` | Model cycling list |
| `--skill`, `--no-skills`/`-ns` | Skill loading |
| `--prompt-template`, `--no-prompt-templates`/`-np` | Prompt templates |
| `--theme`, `--no-themes` | Themes |
| `--verbose` | Verbose startup output |
| Unknown/custom flags | Forwarded with heuristic value detection |

**Forwarded as fallback** (agent frontmatter overrides if set):

| Flag | Overridden by |
| --- | --- |
| `--model` | Replaced at launch by the parent's currently active model (`model:` is only a no-context compatibility fallback) |
| `--thinking` | `thinking:` in agent frontmatter |
| `--tools` / `--no-tools` | `tools:` in agent frontmatter |

**Never forwarded** (managed by the extension itself):
`--mode`, `-p`/`--print`, `--session`/`--no-session`, `--continue`, `--resume`,
`--append-system-prompt`, `--offline`, `--extension`/`-e`, `--no-extensions`/`-ne`,
`--subagent-max-depth`, `--subagent-prevent-cycles`, `--export`, `--list-models`,
`--help`, `--version`.

---

## Programmatic Usage (JSON RPC)

When running `pi` programmatically with `--mode rpc` (or `--mode json`), the stream contains
`tool_result_end` events whenever the agent completes a `subagents` tool call. The `details` field
of these events carries the full stats for that delegation — including recursive usage and tool
call counts from all subagents in the tree.

### Stream event shape

```
tool_result_end
└── message
    ├── role:        "toolResult"
    ├── toolName:    "subagents"
    ├── toolCallId:  string
    ├── isError:     boolean
    ├── content:     [{ type: "text", text: "<final output>" }]
    └── details:     SubagentDetails
```

### `SubagentDetails` object

```ts
interface SubagentDetails {
  // Execution metadata
  mode: "single" | "parallel";          // one task vs multiple parallel tasks
  delegationMode: "spawn";              // always "spawn" (kept for backward-compatible serialization)
  projectAgentsDir: string | null;      // path to .pi/agents/ dir if used

  // Individual agent results (one per task)
  results: SingleResult[];

  // ── Stats summary (own + all descendants, recursively) ──────────────────
  aggregatedUsage: UsageStats;          // token counts and cost, full tree
  aggregatedToolCalls: ToolCallCounts;  // { toolName: callCount }, full tree

  // ── Per-agent breakdown ──────────────────────────────────────────────────
  usageTree: UsageTreeNode[];           // one root node per result
}

interface SingleResult {
  agent: string;                        // agent name
  agentSource: "user" | "project" | "builtin" | "unknown";
  task: string;                         // task string passed to this agent
  exitCode: number;                     // 0 = success, >0 = error, -1 = still running
  messages: Message[];                  // full conversation history of the subagent
  stderr: string;
  usage: UsageStats;                    // this agent's OWN token usage only
  toolCalls: ToolCallCounts;            // this agent's OWN tool calls only
  model?: string;
  stopReason?: string;                  // "end_turn" | "error" | "aborted" | ...
  errorMessage?: string;
}

interface UsageStats {
  input: number;                        // input tokens
  output: number;                       // output tokens
  cacheRead: number;                    // cache read tokens
  cacheWrite: number;                   // cache write tokens
  cost: number;                         // total cost in USD
  contextTokens: number;                // snapshot: last context window size (not summed in aggregates)
  turns: number;                        // number of assistant turns
}

// toolName → call count, e.g. { "bash": 5, "read": 3, "subagents": 1 }
type ToolCallCounts = Record<string, number>;

interface UsageTreeNode {
  agent: string;
  task: string;
  ownUsage: UsageStats;                 // only this agent's turns
  ownToolCalls: ToolCallCounts;         // only this agent's tool calls
  aggregatedUsage: UsageStats;          // ownUsage + all children recursively
  aggregatedToolCalls: ToolCallCounts;  // ownToolCalls + all children recursively
  children: UsageTreeNode[];            // one node per nested subagent invocation
}
```

### Important notes on stats

- **`SingleResult.usage`** and **`SingleResult.toolCalls`** cover **only that one agent's own work** —
  not its children. Children run in separate processes; their tokens never appear in the parent's usage.
- **`aggregatedUsage`** / **`aggregatedToolCalls`** on `SubagentDetails` (and on each `UsageTreeNode`)
  are the correct totals to use when you want the cost or tool call count for an entire delegation
  subtree.
- **`contextTokens`** is a point-in-time snapshot of the context window size at the last turn of that
  agent. It is **not** summed in aggregated stats (it would be meaningless as a cross-process sum).
- **`toolCalls`** includes **all** tool calls an agent made, including the `"subagents"` call itself.
  You can use the `"subagents"` count to see how many nested delegations an agent spawned.

### Annotated example JSON

The scenario below: main agent delegates to `code-writer`, which does some file work and then
delegates to `code-reviwer` before finishing.

```json
{
  "type": "tool_result_end",
  "message": {
    "role": "toolResult",
    "toolName": "subagents",
    "toolCallId": "toolu_01XYZ",
    "isError": false,
    "content": [
      {
        "type": "text",
        "text": "Feature implemented and reviewed. Added validation logic in auth.ts and updated the test suite."
      }
    ],
    "details": {
      "mode": "single",
      "delegationMode": "spawn",
      "projectAgentsDir": null,

      "aggregatedUsage": {
        "input": 2180,
        "output": 615,
        "cacheRead": 940,
        "cacheWrite": 120,
        "cost": 0.0079,
        "contextTokens": 0,
        "turns": 3
      },
      "aggregatedToolCalls": {
        "read":     3,
        "bash":     2,
        "edit":     1,
        "subagents": 1
      },

      "usageTree": [
        {
          "agent": "code-writer",
          "task": "Implement the auth feature and have it reviewed",
          "ownUsage": {
            "input": 1380,
            "output": 365,
            "cacheRead": 540,
            "cacheWrite": 120,
            "cost": 0.0058,
            "contextTokens": 2840,
            "turns": 2
          },
          "ownToolCalls": {
            "read":     1,
            "bash":     1,
            "edit":     1,
            "subagents": 1
          },
          "aggregatedUsage": {
            "input": 2180,
            "output": 615,
            "cacheRead": 940,
            "cacheWrite": 120,
            "cost": 0.0079,
            "contextTokens": 0,
            "turns": 3
          },
          "aggregatedToolCalls": {
            "read":     3,
            "bash":     2,
            "edit":     1,
            "subagents": 1
          },
          "children": [
            {
              "agent": "code-reviwer",
              "task": "Review the auth implementation in auth.ts",
              "ownUsage": {
                "input": 800,
                "output": 250,
                "cacheRead": 400,
                "cacheWrite": 0,
                "cost": 0.0021,
                "contextTokens": 1450,
                "turns": 1
              },
              "ownToolCalls": {
                "read": 2,
                "bash": 1
              },
              "aggregatedUsage": {
                "input": 800,
                "output": 250,
                "cacheRead": 400,
                "cacheWrite": 0,
                "cost": 0.0021,
                "contextTokens": 0,
                "turns": 1
              },
              "aggregatedToolCalls": {
                "read": 2,
                "bash": 1
              },
              "children": []
            }
          ]
        }
      ],

      "results": [
        {
          "agent": "code-writer",
          "agentSource": "builtin",
          "task": "Implement the auth feature and have it reviewed",
          "exitCode": 0,
          "stopReason": "end_turn",
          "model": "claude-opus-4-5",
          "stderr": "",
          "usage": {
            "input": 1380,
            "output": 365,
            "cacheRead": 540,
            "cacheWrite": 120,
            "cost": 0.0058,
            "contextTokens": 2840,
            "turns": 2
          },
          "toolCalls": {
            "read":     1,
            "bash":     1,
            "edit":     1,
            "subagents": 1
          },
          "messages": [
            "... full conversation history of code-writer (includes the nested subagent tool_result) ..."
          ]
        }
      ]
    }
  }
}
```

### Collecting stats across an entire session

If you are consuming the JSON stream programmatically and want to track the total cost and tool
usage across all subagent work in a session, listen for every `tool_result_end` event where
`message.toolName === "subagents"` (or the legacy `"subagent"` in old sessions) and sum `message.details.aggregatedUsage` across them.

```js
let totalCost = 0;
const totalToolCalls = {};

for await (const line of jsonLines) {
  const event = JSON.parse(line);
  if (
    event.type === "tool_result_end" &&
    ["subagents", "subagent", "resume_subagents"].includes(event.message?.toolName) &&
    event.message?.details
  ) {
    const { aggregatedUsage, aggregatedToolCalls } = event.message.details;
    totalCost += aggregatedUsage.cost;
    for (const [tool, count] of Object.entries(aggregatedToolCalls)) {
      totalToolCalls[tool] = (totalToolCalls[tool] ?? 0) + count;
    }
  }
}
```

Note: if you also track the main agent's own usage from `message_end` events, make sure **not** to
double-count the subagent costs there — the main agent's own token usage (from its own `message_end`
events) does not include subagent work.

---

## create-subagent Skill

If you want the agent to **create new subagent definition files** for itself, install the [`create-subagent` skill](https://github.com/gee666/pi-subagent/tree/main/create-subagent). Once installed, the agent will know how to scaffold new `.md` agent files in the right location with correct frontmatter.

## Attribution

Inspired by [vaayne/agent-kit](https://github.com/vaayne/agent-kit) and [mariozechner/pi-mono](https://github.com/badlogic/pi-mono).

## License

MIT
