subagent — Delegate Work to an Isolated Child Agent
A single agent doing everything suffers context pollution, model mismatch, and limited parallelism. subagent gives the main agent a controlled delegation outlet.
TL;DR
subagent's value is not "can spawn a subprocess" — it gives the main agent a controlled delegation outlet: the right work to the right child agent and model, with context isolation and results streamed back. pi deliberately omits this; piex fills the minimal viable gap via the Extension API.
Intro
A single agent doing everything hits common failure modes:
- Context pollution: the scout phase reads a pile of files and crowds out the implementation phase's context window
- Model mismatch: burning Opus on a grep scout, or under-serving a code review with Haiku
- Limited parallelism: the main agent thinks serially and cannot run "scout maps code + reviewer audits diff"
- Cascading failure: one subtask goes sideways and drags the whole turn down
subagent solves delegation: the main agent calls the subagent tool once, which spawns an isolated pi subprocess with its own system prompt, tool set, model config, and task, then brings the result back. @piex-dev/subagent ships as a pi extension: single (one task) / parallel (cross-agent) orchestration, four built-in role agents, per-agent model config.
Scope
This package is a delegation primitive, not a higher-level workflow. It does not replace @piex-dev/review / @piex-dev/plan; those are future upstream consumers. Status: implemented (MVP).
How it works
Process isolation, not in-process sessions
The core is process-level isolation: each call spawns an independent pi --mode json -p --no-session subprocess with its own context window, tools, and model. The main agent's context is not polluted, and the child's tool calls do not crowd the main conversation.
Why not in-process child sessions? Because pi's Extension API does not expose session nesting. The subprocess route needs only child_process.spawn + a JSON event stream — pure Extension API, no kernel changes — and is the pi-recommended path.
In-process is also possible, with hard constraints
pi SDK's createAgentSession() + SessionManager.inMemory() can build a child AgentSession in-process, but only with the 7 built-in tools (noExtensions: true) and no approval/sandbox/headers inheritance. Not worth it for users who lean on piex extensions, so MVP is subprocess-only; in-process is P1.
Child communication: JSON event stream
The child runs in --mode json and emits an NDJSON event stream on stdout; the parent parses it line-by-line with JsonLineDecoder (16MB per-line cap): message_update (streaming progress), tool_execution_start (which tool the child is calling), agent_end (full messages — extract the last assistant message as output). stderr is accumulated separately (128KB cap) for diagnostics. An abort signal fires SIGTERM → SIGKILL after 5s, killing the whole process group.
Blocking semantics (product constraint)
subagent is a blocking tool: until it returns, the main agent cannot handle steering and the user feels the current turn is held. So the prompt is hard-coded: delegate only when you must wait for the result to continue; never call subagent for simple Q&A, single-file tweaks, or anything the main agent can do directly. Background async is a P1 high priority.
Model config: a real three-tier precedence
Key correction
Not passing --model ≠ inherit. Without it, the child pi uses the user's global default model, not the parent session's current selection. The parent may have just /model-switched to sonnet:high; the child still runs the default haiku. So inherit must explicitly read the parent's current model and thinkingLevel and pass them through.
| agent | model | thinking | reason |
|---|---|---|---|
| reviewer | strong model | high | adversarial review needs depth |
| scout | cheap fast model | off | recon needs no heavy reasoning |
| worker | inherit parent | parent | stay consistent with the main agent |
System prompt: replace, don't append
Role agents use --system-prompt to replace the default coding-assistant prompt, not --append-system-prompt. Appending keeps the "you are a coding assistant" persona and layers "don't edit" on top — the model wavers. scout/reviewer/planner need a clean role prompt.
| flag | MVP default | reason |
|---|---|---|
--system-prompt | always | clean role persona |
--no-extensions | always | avoid recursive subagent load, control cold start |
--no-context-files | no (loads AGENTS.md by default) | project conventions usually help reviewer/worker |
| agent extension tools (hashline etc.) | not loaded in MVP | matches --no-extensions; P1 allowlist |
Context passing: optional context, no auto-magic
The child is an empty session by default with only the task text. MVP pins strategy B: an optional context field the main agent fills explicitly (diff / plan / file summary). No auto-injected git diff, recent N turns, or implicit parent-transcript sharing (P1).
Nesting depth limit
A child agent cannot spawn its own child by default (PIEX_SUBAGENT_DEPTH, default maxDepth=1). The child's env injects PIEX_SUBAGENT_DEPTH=<parent+1>; before execution it checks and throws if over the limit. Tunable via PIEX_SUBAGENT_MAX_DEPTH. Hard backstop: the child runs with --no-extensions, so even a misconfigured depth cannot re-register the subagent tool.
Extension loading & env
| item | MVP policy |
|---|---|
| extensions | --no-extensions (load nothing, including this package) |
| env | inherit parent env; only override/inject PIEX_SUBAGENT_DEPTH |
| auth | rely on the parent env's existing API key / ~/.pi/agent/auth.json (OAuth) |
No aggressive env allowlist: a same-uid child can already read auth.json, and gutting env is more likely to break provider-specific variables than to help. The security boundary is tool permissions, depth, cwd, timeout, and abort reclamation.
Usage
Install
pi install npm:@piex-dev/subagent
Source: extensions/subagent
Built-in agents
| agent | role | tools | recommended model |
|---|---|---|---|
scout | read-only code recon | read/grep/find/ls/bash | cheap fast + off |
planner | read-only planning | read/grep/find/ls | strong + high |
reviewer | adversarial code review | read/grep/find/ls/bash | strong + high |
worker | full built-in tools, implements | read/bash/edit/write/grep/find/ls | inherit parent |
Single source of truth for config
| file | contents |
|---|---|
~/.pi/piex-dev/subagent/agents.yaml | user agent definitions (override built-in by name, whole object) |
~/.pi/piex-dev/subagent/settings.json | package settings: defaultModel, defaultThinking, maxDepth, … |
subagent config is not stuffed into pi's global settings.json — keeps it from tangling with pi's own fields. Path follows the piex convention: join(dirname(getAgentDir()), "piex-dev", "subagent"). MVP does not do project-local agents; P1 adds them with confirmation.
agents.yaml example:
- name: reviewer
description: Adversarial review; surface the main agent's blind spots
systemPrompt: |
You are a reviewer subagent. Review changes adversarially.
Report PASS/FAIL/PARTIAL with evidence. Do not edit files.
tools: [read, grep, find, ls, bash]
model: anthropic/claude-opus-4-1
thinkingLevel: high
- name: scout
description: Fast code recon
systemPrompt: |
You are a scout subagent. Explore the codebase quickly and report grounded findings.
Do not edit files.
tools: [read, grep, find, ls, bash]
model: inherit
thinkingLevel: off
settings.json example:
{
"defaultModel": "inherit",
"defaultThinking": null,
"maxDepth": 1,
"timeoutMs": 600000
}
Tool parameters
// single
{ agent: "reviewer", task: "...", context?: string, timeoutMs?, thinkingLevel? }
// parallel (each item carries its own agent; cross-role)
{
tasks: [
{ agent: "scout", task: "..." },
{ agent: "reviewer", task: "...", context?: string }
],
timeoutMs?, // top-level default, overridable per task
}
Constraints: single requires agent + task; parallel requires tasks[] where each item has its own agent + task, max 8 items, concurrency max 4; "one top-level agent + many task strings" is rejected (it would weaken the common cross-role parallel case).
Usage
use scout to find all entry points in src/
→ subagent({ agent: "scout", task: "..." })
use reviewer to review the current diff
→ subagent({ agent: "reviewer", task: "Review for correctness and tests", context: "" })
run scout on auth flow and reviewer on the diff in parallel
→ subagent({ tasks: [ { agent: "scout", task: "Map the auth flow" }, { agent: "reviewer", task: "Review the diff", context: "..." } ] })
Helper command: /subagents lists available agents and their effective model/thinking.
Verify
pi -e ./extensions/subagent/src/subagent.ts -p "what is 1+1" --no-session
pi -e ./extensions/subagent/src/subagent.ts -p "use scout to list files in src/" --no-session
Implementation
Package path: extensions/subagent, target ~1500–1800 lines, ~7 source files.
File structure
extensions/subagent/
├── package.json # @piex-dev/subagent
├── tsconfig.json
├── README.md
├── LICENSE
└── src/
├── subagent.ts # entry: registerTool + registerCommand + tool_result hook
├── types.ts # AgentConfig / SubagentParams / SingleResult
├── agents.ts # 4 built-in agents + loadAgents + resolveAgent + resolveModel
├── subprocess.ts # buildPiArgs + getPiInvocation + runSingleAgent + terminate + JsonLineDecoder
├── execution.ts # depth check + single/parallel + concurrency + status
└── render.ts # renderCall / renderResult
MVP does not introduce a transport.ts / ManagedAgent abstraction. With only the subprocess path, plain functions are clearer; P1 will extract SubagentTransport when in-process lands.
Subprocess spawn key points
pi bin resolution is a three-way (replicating narumitw's getPiInvocation): argv[1] is an executable .js → node <argv1> (dev); execPath is node/bun → pi (global install); otherwise <execPath> (compiled binary).
pi --mode json -p --no-session --no-extensions \
--system-prompt \
--model \ # always explicit (incl. inherited parent model)
--thinking \ # only if set
--tools \ # empty array → --no-tools
""
env inherits process.env and sets PIEX_SUBAGENT_DEPTH=<depth+1>. Termination: detached: true forms a process group; on abort process.kill(-pid, SIGTERM) → SIGKILL after 5s.
Depth check
function assertSubagentDepthAllowed(): void {
const depth = parseInt(process.env.PIEX_SUBAGENT_DEPTH ?? "0", 10) || 0;
const maxDepth = parseInt(process.env.PIEX_SUBAGENT_MAX_DEPTH ?? "1", 10) || 1;
if (depth >= maxDepth) {
throw new Error(`Subagent recursion depth limit reached (${maxDepth})`);
}
}
promptGuidelines (written into the tool description)
- Use only when delegation's payoff is clear; don't delegate trivial tasks (cold start + extra tokens are expensive)
- Call only when you must wait for the result to continue; this is a blocking call
- Fill
contextfor review/implementation tasks (diff, plan, relevant paths) - Use parallel only for independent tasks; avoid multiple workers writing the same file
- scout/planner/reviewer are read-only; use worker to change code
Design notes
| project | form | isolation | piex choice |
|---|---|---|---|
| nicobailon pi-subagents | Extension | subprocess only | borrow role split; reject chain/worktree/watchdog/acceptance |
| oh-my-pi swarm | fork | subprocess (kernel API) | don't fork; borrow parallel-orchestration idea |
| opencode task | standalone agent | in-process child session | reject in-process kernel model; borrow centralized agent config, depth |
| narumitw pi-subagents | Extension | subprocess + in-process | main reference: JSON stream, depth, spawn details; MVP does not copy transport/stateful |
Core trade-off: borrow narumitw's runnable mechanism, not its architectural complexity. MVP does only subprocess + single/parallel; P1 adds in-process / background / chain as needed.
Relation to plan / review
| package | responsibility | vs subagent |
|---|---|---|
@piex-dev/plan | read-only explore → plan → execution progress | future: delegate a step to worker; MVP not coupled |
@piex-dev/review | single-agent code review | future: multi-agent review consumes subagent; MVP not coupled |
@piex-dev/subagent | delegation primitive | no built-in review/plan workflow |
Changelog
Roadmap
MVP (current): subprocess execution + single/parallel; 4 built-in agents + agents.yaml/settings.json; real inherit (explicitly pass the parent session's current model); --system-prompt replace + --no-extensions; optional context; depth limit + abort process-group reclamation; blocking semantics in promptGuidelines.
P1 (by value): background async (high priority, so the TUI isn't pinned); extensions allowlist (child can load chosen piex extensions); in-process transport (lower cold start); chain orchestration; project-local agents + confirmation; auto-context strategy; propose createChildSession() to pi.
Not-doing list (explicit boundaries)
- fan-in aggregator
- stateful runtime (ManagedAgent tree, mailbox, follow-up)
- watchdog / acceptance / worktree
- MVP-period background async / in-process / chain
- MVP-period transport abstraction
- aggressive env allowlist
- stuffing config into pi's global settings.json
Risks & mitigations
| risk | mitigation |
|---|---|
| inherit passes the wrong model | always resolve the parent session's current model/thinking explicitly before passing --model |
| default persona pollutes the role | --system-prompt replace, not append |
| recursive subagent load | child --no-extensions + depth gate, double backstop |
| cold-start cost / over-delegation | promptGuidelines bound usage; default timeout; P1 background/in-process |
| parallel same-file write conflict | docs + guidelines; read-only agents are the default workhorse |
| child auth failure | inherit parent env + rely on auth.json; no env gutting |
| JSON stream hang | bounded decoder + timeout + abort + stderr cap |
| yaml parse failure | schema validation + notify warning, fall back to built-in |
| no agent_end | on non-zero exit use stderr + recent output, mark error |
Acceptance criteria
Positive
- Local
pi -e ./extensions/subagent/...loads;/subagentslists the 4 built-in agents - single:
use scout to find entry filesreturns recon results - parallel: cross-agent (scout + reviewer) runs concurrently
- After agents.yaml gives reviewer a strong model, the child actually uses it
- After the parent switches models, an inherit agent follows the parent (not the global default)
- A reviewer call with
contextreflects that context in the output
Negative
- Unknown agent name → clear error, no spawn
- timeout → process killed,
timedOut=true - depth exceeded → throws "depth limit reached", no grandchild
- Main agent abort → child process group reclaimed, no zombies
- Smoke:
pi -e ... -p "what is 1+1" --no-sessiondoes not trigger subagent, no error
Appendix: feature-by-feature comparison of pi subagent ecosystem
| capability | nicobailon | oh-my-pi | opencode | narumitw | piex MVP |
|---|---|---|---|---|---|
| form | Extension | fork | standalone agent | Extension | Extension |
| isolation | subprocess | subprocess | in-process | subprocess+in-process | subprocess |
| transport abstraction | ❌ | ❌ | ❌ | ✅ | ❌ (P1) |
| per-task multi-agent parallel | ✅ | ✅ | ❌ | ✅ | ✅ |
| real parent-model inherit | ✅ | ✅ | ✅ | ✅ | ✅ (explicit pass) |
| system-prompt replace | ✅ | ✅ | ✅ | ✅ | ✅ |
| optional context | ✅ | partial | task text | ✅ | ✅ |
| load extensions by default | yes | yes | yes | configurable | ❌ (--no-extensions) |
| background async | ✅ | ❌ | ✅ | ✅ | ❌ (P1 high priority) |
| chain / fan-in / stateful | heavy | medium | light | medium-heavy | ❌ |
| depth limit | ✅ | ❌ | ✅ | ✅ | ✅ |
| size | 90+ files | ~6 files | single tool | 21 files | ~7 files |
piex subagent's position: a runnable delegation primitive, not an orchestration platform. Get model inheritance, role prompts, context, and cross-agent parallelism right first; leave complex orchestration to P1 or the main agent.
Source Markdown: docs/packages/subagent.md