oira666_pi-subagent
Subagent extension for Pi coding agent. Delegate tasks to specialized agents.
Package details
Install oira666_pi-subagent from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:oira666_pi-subagent- Package
oira666_pi-subagent- Version
0.3.14- Published
- Aug 1, 2026
- Downloads
- 1,122/mo · 223/wk
- Author
- oira666
- License
- MIT
- Types
- extension
- Size
- 288.1 KB
- Dependencies
- 1 dependency · 4 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
Pi Subagent
Delegate tasks to specialized subagents.
Install
pi install npm:oira666_pi-subagent
Or via git:
pi install git:github.com/gee666/pi-subagent.git
Remove
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):
{ "tasks": [{ "agent": "code-writer", "task": "Implement the API" }] }
Multiple tasks run in parallel:
{
"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:
~/.pi/pi-subagents.json$PI_CODING_AGENT_DIR/pi-subagents.json(normally~/.pi/agent/pi-subagents.json)- 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:
{
"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 refactoringcode-reviwer— code review and risk findingcode-architect— technical design and approach selectionteam-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(whenPI_CODING_AGENT_DIRis set) - Project agents:
.pi/agents/*.md(may prompt for confirmation — seePI_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.
---
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. When cycle prevention is enabled, agents already in the current delegation stack are omitted from the child model's available-agent list. The runner still checks every task as a safety boundary: in a mixed parallel call, cyclic tasks fail while legal siblings still run.
A nested delegation failure is returned to its calling agent as a recoverable tool error. If that agent subsequently retries, completes the work itself, and produces a successful final answer, the earlier tool error does not incorrectly turn the completed agent—and all of its ancestors—into failures.
| 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 |
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 |
Subagent Liveness Timeouts
A delegated process cannot block its parents forever. The runner applies a
startup timeout before the first model turn and a semantic-inactivity timeout
after startup. Tool/turn events and changed nested-agent state reset the idle
timer; repeated unchanged progress heartbeats do not. On timeout or
cancellation, the runner terminates the child process tree and bounds cleanup;
even if a wedged OS process never reports close, the tool returns an error
result so every waiting parent can settle.
RPC completion is based on Pi's agent_settled event—not agent_end.
agent_end is only a low-level run boundary and may be followed by Pi's normal
provider retry, overflow compaction, or queued continuation. Rejected prompt
commands, signal exits, and processes that exit before agent_settled are
reported immediately as failures.
| Env Var | Default | Description |
|---|---|---|
PI_SUBAGENT_STARTUP_TIMEOUT |
120000 |
Milliseconds allowed to reach the first model turn; 0 disables |
PI_SUBAGENT_STARTUP_RETRIES |
2 |
Fresh retries after a startup timeout |
PI_SUBAGENT_IDLE_TIMEOUT |
1200000 |
Milliseconds without new semantic RPC activity after startup; 0 disables |
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). The collapsed tool
row keeps the normal compact progress text. Press Ctrl+O to expand it into the
subagent tree immediately; every running node shows its latest six activity
lines (thinking, tool starts/completions, and completed turns).
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.
- Durable child refs retain final output, own usage, model, and tool counts, so completed siblings survive a JSON/session restart without becoming
(no output)or losing accounting. - 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:
{ "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.
- The preferred shape is
{"resumes":[...]}. For compatibility, the common single-item shorthand{"subagent":"name","task":"..."}is normalized automatically before validation. - 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
parentSessionancestor-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
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 = process 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.usageandSingleResult.toolCallscover 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/aggregatedToolCallsonSubagentDetails(and on eachUsageTreeNode) are the correct totals to use when you want the cost or tool call count for an entire delegation subtree.contextTokensis 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).toolCallsincludes 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.
{
"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.
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. 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 and mariozechner/pi-mono.
License
MIT