Extension Delegation Subprocess

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:

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 SIGTERMSIGKILL 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

1. agent's model / thinkingLevel — agents.yaml (highest)
2. global default defaultModel / defaultThinking — settings.json
3. inherit the parent session's current model — read explicitly and passed via --model/--thinking

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.

agentmodelthinkingreason
reviewerstrong modelhighadversarial review needs depth
scoutcheap fast modeloffrecon needs no heavy reasoning
workerinherit parentparentstay 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.

flagMVP defaultreason
--system-promptalwaysclean role persona
--no-extensionsalwaysavoid recursive subagent load, control cold start
--no-context-filesno (loads AGENTS.md by default)project conventions usually help reviewer/worker
agent extension tools (hashline etc.)not loaded in MVPmatches --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

itemMVP policy
extensions--no-extensions (load nothing, including this package)
envinherit parent env; only override/inject PIEX_SUBAGENT_DEPTH
authrely 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

bash
pi install npm:@piex-dev/subagent

Source: extensions/subagent

Built-in agents

agentroletoolsrecommended model
scoutread-only code reconread/grep/find/ls/bashcheap fast + off
plannerread-only planningread/grep/find/lsstrong + high
revieweradversarial code reviewread/grep/find/ls/bashstrong + high
workerfull built-in tools, implementsread/bash/edit/write/grep/find/lsinherit parent

Single source of truth for config

filecontents
~/.pi/piex-dev/subagent/agents.yamluser agent definitions (override built-in by name, whole object)
~/.pi/piex-dev/subagent/settings.jsonpackage 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:

yaml
- 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:

json
{
  "defaultModel": "inherit",
  "defaultThinking": null,
  "maxDepth": 1,
  "timeoutMs": 600000
}

Tool parameters

typescript
// 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

text
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

bash
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

text
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).

bash
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

typescript
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)

  1. Use only when delegation's payoff is clear; don't delegate trivial tasks (cold start + extra tokens are expensive)
  2. Call only when you must wait for the result to continue; this is a blocking call
  3. Fill context for review/implementation tasks (diff, plan, relevant paths)
  4. Use parallel only for independent tasks; avoid multiple workers writing the same file
  5. scout/planner/reviewer are read-only; use worker to change code

Design notes

projectformisolationpiex choice
nicobailon pi-subagentsExtensionsubprocess onlyborrow role split; reject chain/worktree/watchdog/acceptance
oh-my-pi swarmforksubprocess (kernel API)don't fork; borrow parallel-orchestration idea
opencode taskstandalone agentin-process child sessionreject in-process kernel model; borrow centralized agent config, depth
narumitw pi-subagentsExtensionsubprocess + in-processmain 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

packageresponsibilityvs subagent
@piex-dev/planread-only explore → plan → execution progressfuture: delegate a step to worker; MVP not coupled
@piex-dev/reviewsingle-agent code reviewfuture: multi-agent review consumes subagent; MVP not coupled
@piex-dev/subagentdelegation primitiveno 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)

Risks & mitigations

riskmitigation
inherit passes the wrong modelalways resolve the parent session's current model/thinking explicitly before passing --model
default persona pollutes the role--system-prompt replace, not append
recursive subagent loadchild --no-extensions + depth gate, double backstop
cold-start cost / over-delegationpromptGuidelines bound usage; default timeout; P1 background/in-process
parallel same-file write conflictdocs + guidelines; read-only agents are the default workhorse
child auth failureinherit parent env + rely on auth.json; no env gutting
JSON stream hangbounded decoder + timeout + abort + stderr cap
yaml parse failureschema validation + notify warning, fall back to built-in
no agent_endon non-zero exit use stderr + recent output, mark error

Acceptance criteria

Positive

  1. Local pi -e ./extensions/subagent/... loads; /subagents lists the 4 built-in agents
  2. single: use scout to find entry files returns recon results
  3. parallel: cross-agent (scout + reviewer) runs concurrently
  4. After agents.yaml gives reviewer a strong model, the child actually uses it
  5. After the parent switches models, an inherit agent follows the parent (not the global default)
  6. A reviewer call with context reflects that context in the output

Negative

  1. Unknown agent name → clear error, no spawn
  2. timeout → process killed, timedOut=true
  3. depth exceeded → throws "depth limit reached", no grandchild
  4. Main agent abort → child process group reclaimed, no zombies
  5. Smoke: pi -e ... -p "what is 1+1" --no-session does not trigger subagent, no error

Appendix: feature-by-feature comparison of pi subagent ecosystem

capabilitynicobailonoh-my-piopencodenarumitwpiex MVP
formExtensionforkstandalone agentExtensionExtension
isolationsubprocesssubprocessin-processsubprocess+in-processsubprocess
transport abstraction❌ (P1)
per-task multi-agent parallel
real parent-model inherit✅ (explicit pass)
system-prompt replace
optional contextpartialtask text
load extensions by defaultyesyesyesconfigurable❌ (--no-extensions)
background async❌ (P1 high priority)
chain / fan-in / statefulheavymediumlightmedium-heavy
depth limit
size90+ files~6 filessingle tool21 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.