Fork of nicobailon/pi-subagents — diverged with bug fixes, token-economy optimizations, and custom agents.
pi-subagents lets Pi delegate work to focused child agents. Use it for code review, context building, implementation, parallel audits, saved workflows, background jobs, and anything else that benefits from a second or third set of model eyes.
Contributing / hacking on this? Start with
docs/ARCHITECTURE.md— the request lifecycle and a "where do I change X?" file map.
- Quick start
- Builtin agents
- Common workflows
- Configuring agents
- Direct commands
- Clarify and launch UI
- Optional companions
- Agents and chains (advanced)
- Chain files
- Structured output and named outputs
- Dynamic fanout (expand / collect)
- Skills
- Orchestration patterns
- Programmatic tool usage
- Worktree isolation
- Configuration reference
- Files, logs, and observability
- Live progress
- Session sharing
- Recursion guard
- Runaway stream guard
- Events
- Prompt-template integration
- Changes from upstream
- Runtime files
- License
pi install git:github.com/DarkoKuzmanovic/pi-subagentsThat is the only required step. You can add optional pieces later.
To pin a specific release once a tag is available:
pi install git:github.com/DarkoKuzmanovic/pi-subagents@v0.45.3You do not need to create agents, write config, or learn slash commands. After installing, ask Pi for delegation in plain language:
Use reviewer to review this diff.
Ask oracle for a second opinion on my current plan.
Use recon to understand this code based on our discussion then ask me clarification questions.
Run parallel reviewers: one for correctness, one for tests, and one for unnecessary complexity.
That is enough to start.
Pi is the parent session. A subagent is a focused child Pi session with its own job.
When you ask for a subagent, Pi starts the child, gives it the task, and brings the result back. Foreground runs stream in the conversation. Background runs keep working and can be checked later.
Installing the extension does not start an automatic reviewer in the background. It gives Pi a delegation tool. If you want every implementation reviewed, say that in your prompt or put it in your project instructions:
When you finish implementing, run a reviewer subagent before summarizing.
Foreground runs stream progress in the conversation. Background runs return control immediately and stay inspectable with subagent({ action: "status" }) or a specific run id.
A compact async widget and completion notifications are available. Parallel background runs show per-agent progress instead of fake chain steps. Chains with parallel groups keep grouped progress and results, so failed or paused agents stay visible next to completed ones.
Show me the current async runs.
/subagents-doctor
These cover most day-to-day use:
Ask oracle for a second opinion on my current plan. Challenge assumptions and tell me what I might be missing.
Use oracle to help solve this hard bug. Have it inspect the code and propose the best next move before we edit anything.
Run parallel reviewers on this diff. I want one focused on correctness, one on tests, and one on unnecessary complexity.
Have worker implement this approved plan. Afterward, run parallel reviewers, summarize their feedback, and apply the fixes that make sense.
Use recon to understand the auth flow, then have planner turn that into an implementation plan.
Those are ordinary Pi requests. Pi decides whether to call subagent, which agent to use, and whether a chain or parallel run makes sense.
The extension ships with six builtin agents you can use immediately:
| Agent | Use it when you want... |
|---|---|
recon |
Local or external context before planning: relevant files, entry points, data flow, risks, source-backed notes, and handoff material. |
planner |
A concrete implementation plan from existing context. It should read and plan, not edit code. |
worker |
Implementation work, including lane-based normal/hard routing and approved oracle handoffs. It edits files, validates, and escalates decisions. |
reviewer |
Code review, synthesis, and small fix-back guidance. It checks the implementation against the task/plan, tests, edge cases, and simplicity. |
oracle |
A second opinion before acting. It challenges assumptions, catches drift, and recommends the safest next move without editing. |
janitor |
Repository hygiene: dead code, stale docs, orphaned artifacts, naming issues, and structural cleanup. Use --review for audit-only. |
Compatibility agents (scout, researcher, synthesizer, test-writer, worker-light, worker-heavy, oracle-fresh, and deslopper) are preserved but disabled by default; use the roles above plus lane or context: "fresh" instead.
A simple rule of thumb: use recon before you understand the code or external facts, planner before a bigger change, worker to implement, reviewer to check or synthesize, oracle when the decision itself feels risky, and janitor for cleanup.
Packaged recon, worker, and reviewer have one optional direct CodeGraph MCP selection: mcp:codegraph/codegraph_explore. This is additive guidance, not a prerequisite; a missing MCP server or cache leaves each role usable through its native tools.
The direct exploration path is eligible only when the target checkout itself already contains .codegraph/codegraph.db. Pass an absolute projectPath, use one graph pass before broad cross-file discovery, and never initialize an index or use another worktree's index. Returned source is Read-equivalent, while grep/read remain authoritative for plain strings, configuration, same-file references, dynamic dispatch, and dead-code confirmation.
When deterministic structural checks are needed, use only $HOME/.pi/agent/bin/codegraph-query.sh PROJECT COMMAND [ARG ...]; it provides the sync-first guard and native fallback contract. Never call raw CodeGraph query commands. An unavailable direct tool, missing index, or insufficient graph result is a reason to continue with native search, not to fail. affected results identify candidate tests only; they never replace required full test gates.
Packaged worker and oracle default to forked context when a launch omits context; planning and context roles default to fresh context. Pass context: "fresh" when you intentionally want a fresh child run.
| Want | Ask naturally |
|---|---|
| Get a second opinion | "Ask oracle to review this plan and challenge assumptions." |
| Solve a hard problem | "Use oracle to investigate this bug before we edit." |
| Review a diff | "Use reviewer to review this diff." |
| Run parallel reviewers | "Run reviewers for correctness, tests, and cleanup." |
| Implement then review | "Implement this, then review it." |
| Execute a plan carefully | "Have worker implement this approved plan, then run reviewers and apply the feedback." |
| Gather context before planning | "Use recon to inspect the auth flow before planning." |
| Run in the background | "Run this in the background." |
| Browse agents | "Show me the available subagents." |
| Use a saved workflow | "Run /multireview on this branch." |
| See running work | "Show active async runs." |
| Check setup | "Check whether subagents are configured correctly." |
Builtin agents inherit your current Pi default model by default. This keeps new installs from depending on a provider you may not have configured. If you want a role to use a specific model, set an override instead of copying the bundled agent file.
For one run, put the override in the command:
/run reviewer[model=anthropic/claude-sonnet-4:high] "Review this diff"
For a persistent override, edit settings. This example pins the reviewer everywhere, adds a backup model for provider failures, and keeps the other builtins on your normal default model:
{
"subagents": {
"agentOverrides": {
"reviewer": {
"model": "anthropic/claude-sonnet-4",
"thinking": "high",
"fallbackModels": ["openai/gpt-5-mini"]
}
}
}
}Use ~/.pi/agent/settings.json for a user override or .pi/settings.json for a project override. The same agentOverrides block can change tools, skills, inherited context, prompt text, or disable a builtin. If you want a totally different agent, create a user or project agent with the same name; for normal tweaks, prefer overrides.
You can override selected builtin fields without copying the whole agent. Overrides live in settings:
- User:
~/.pi/agent/settings.json - Project:
.pi/settings.json
Example:
{
"subagents": {
"agentOverrides": {
"reviewer": {
"inheritProjectContext": false
}
}
}
}Supported override fields are model, fallbackModels, thinking, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, disabled, skills, tools, and systemPrompt. Use defaultContext: false in builtin overrides to clear an inherited context default. Project overrides beat user overrides.
Set disabled: true to hide a builtin from runtime discovery and agent-facing subagent({ action: "list" }) output. For bulk control, set subagents.disableBuiltins: true in settings.
Model lanes are named model/thinking pairs you attach to one agent. A parent picks a lane at dispatch time; the child never chooses its own. Lanes live in subagents.modelLanes under ~/.pi/agent/settings.json (user scope) or .pi/settings.json (project scope). The same lane name under both scopes is supported: project scope wins at resolution time, but the user lane stays editable and is never overwritten silently. /subagents config (and /subagents json or /subagents edit) seeds a starter worker.normal / worker.hard map on first use — only when subagents.modelLanes is absent in the user settings file, and never silently overwriting an existing map. All other lanes, including any reviewer lanes, are user-created.
The /subagents slash command opens an interactive editor for user-scope lanes. From the main agent list, press l to open the lane view for the selected role; the editor is staged — nothing is written to the settings file until you confirm with esc from the agent list, and ctrl+c discards everything.
Lane list and editing keys:
| Context | Key | Action |
|---|---|---|
| Main agent list | l |
Open lanes for the selected role |
| Main agent list | esc |
Apply every staged lane change and override, then close the overlay |
| Lane list | ↑↓ |
Move between lanes |
| Lane list | enter |
Open lane details |
| Lane list | n |
Create a new lane (name → model) |
| Lane list | d |
Delete the selected lane (asks for confirmation; default is Cancel) |
| Lane list | u |
Undo the last staged lane action (LIFO) |
| Lane list | esc |
Back to main agent list (changes still staged) |
| Lane detail | m |
Edit the lane's model |
| Lane detail | t |
Edit the lane's thinking level (offers inherit + supported levels) |
| Lane detail | r |
Rename the lane |
| Lane detail | esc |
Back to lane list |
| Any nested view | ctrl+c |
Discard every staged lane change and override |
The thinking picker shows only the levels the selected model supports, plus inherit to clear the override. There are two related but distinct behaviours — read them as the edit path vs the display path: edit path: when you change a lane's model in the TUI, the lane's currently-selected thinking level is kept if the new model supports it; otherwise it is clamped to off (or the first supported level when off is unavailable) — the same rule the agent model picker uses. So a previously-supported pairing persists by default and only changes when the new model can't represent it. display path: a level already stored in ~/.pi/agent/settings.json that the current model does not support is shown with a warning and left untouched (the lane's thinking field is not dirty-marked by the warning); the stored value persists in the file until you stage an explicit change.
Project lane rows are labeled effective · read-only and have no mutation keys. Pressing d on a project row shows an inline note that project lanes are edited in the project settings file. When a user lane is shadowed by a same-named project lane, the user row is labeled shadowed by project and displays the warning shadowed by project — the project lane still wins at dispatch. The shadowed user lane remains editable and deletable, but editing it does not change what dispatch resolves while the project lane exists. Note that esc on the lane list does not silently normalize an invalid name; the lane editor rejects unknown or duplicate names inline with a warning and keeps the draft.
Lane names created or renamed through the TUI must match /^[a-z0-9][a-z0-9-]*$/ — lowercase letters, digits, and hyphens, starting with a letter or digit. The store applies that rule only to create and rename targets. Existing free-form lane names in your settings keep working, keep resolving, and remain editable and deletable in place; they are tagged legacy name in the lane list.
After the overlay closes, /subagents writes the staged user-scope mutations to ~/.pi/agent/settings.json once, atomically, through the same merge-preserving writer used for built-in overrides. The store preserves all unrelated root fields, every other subagents field (including agentOverrides), other roles and lanes, and any unrelated properties on a targeted lane. A malformed subagents.modelLanes shape blocks the overlay from opening and notifies with the offending path — the file is never overwritten with a clean skeleton. If a project lane file is malformed, the overlay also does not open and the project path is named.
For hand-edited lanes, the /subagents config (or /subagents json / /subagents edit) shortcut still opens ~/.pi/agent/settings.json in your editor. The two control planes are mutually visible: TUI edits update the same file, and edits you make there appear the next time you open the overlay. Setting subagents.modelLanes directly in the project file is display-only — the project file is never written by the editor.
If settings.json changes on disk while the lane editor is open (a hand-edit, another tool, or a second /subagents session), lanes added externally are merged in at save time — the store re-reads the file just before writing. Lanes you staged a remove or rename against, however, can disappear between staging and save: if any staged mutation targets a lane that no longer exists when the file is re-read, the entire staged batch is rejected with an error naming the file, agent, and lane, your staged lane edits are lost, and you have to redo them. The agent-override save and the lane-batch save are also separate writes — one can succeed while the other fails, and the failure notice names which one did not land rather than implying a single transactional save.
Dispatch semantics are unchanged: lane precedence (project > user), and the unknown-lane throw in resolveModelLaneOverrides are not affected by the lane editor. Unknown lanes still fail before a child model is invoked.
Skip this section until you want exact syntax.
| Command | Description |
|---|---|
/run <agent> [task] |
Run one agent; omit the task for self-contained agents |
/chain agent1 "task1" -> agent2 "task2" |
Run agents in sequence |
/parallel agent1 "task1" -> agent2 "task2" |
Run agents in parallel |
/chain <chainName> -- <task> |
Launch a saved .chain.md workflow (also supports inline agent "task" -> agent syntax) |
/subagents-doctor |
Show read-only setup diagnostics |
Commands validate agent names locally, support tab completion, and send results back into the conversation.
Use -> to separate steps and give each step its own task:
/chain recon "scan the codebase" -> planner "create an implementation plan"
/parallel recon "find security issues" -> reviewer "check code style"
Both double and single quotes work. You can also use -- as a delimiter:
/chain recon -- scan code -> planner -- analyze auth
Steps without a task inherit behavior from the execution mode. Chain steps get {previous}, the prior step's output. Parallel steps use the first available task as a fallback.
/chain recon "analyze auth" -> planner -> worker
# recon gets "analyze auth"; planner gets recon output; worker gets planner output
For a shared task, list agents and place one -- before the task:
/chain recon planner -- analyze the auth system
/parallel recon reviewer -- check for security issues
Append [key=value,...] to an agent name to override defaults for that step:
/chain recon[output=context.md] "scan code" -> planner[reads=context.md] "analyze auth"
/run worker[lane=hard] summarize this codebase
/parallel reviewer[skills=code-review+security] "review backend" -> reviewer[model=openai/gpt-5-mini] "review frontend"
| Key | Example | Description |
|---|---|---|
output |
output=context.md |
Write results to a file. For /chain and /parallel, relative paths live under the chain directory; for /run, relative paths resolve against cwd. |
outputMode |
outputMode=file-only |
Return only a concise file reference for saved output instead of the full saved content. Requires output; default is inline. |
reads |
reads=a.md+b.md |
Read files before executing. + separates multiple paths. |
model |
model=anthropic/claude-sonnet-4 |
Override model for this step. |
skills |
skills=planning+review |
Override injected skills. + separates multiple skills. |
progress |
progress |
Enable progress tracking. |
lane |
lane=hard |
Resolve a named model/thinking lane from subagents.modelLanes before dispatch. |
Set output=false, reads=false, or skills=false to disable that behavior explicitly. Do not use output=false for file-only returns; use outputMode=file-only with an output path.
Add --bg to run in the background:
/run recon "audit the codebase" --bg
/chain recon "analyze auth" -> planner "design refactor" -> worker --bg
/parallel recon "scan frontend" -> recon "scan backend" --bg
Add --fork to start each child from a real branched session created from the parent's current leaf:
/run reviewer "review this diff" --fork
/chain recon "analyze this branch" -> planner "plan next steps" --fork
/parallel recon "audit frontend" -> reviewer "audit backend" --fork
You can combine them in either order:
/run reviewer "review this diff" --fork --bg
/run reviewer "review this diff" --bg --fork
Background runs are detached. If the parent agent has other independent work, it should keep working. If it has nothing useful to do until the background result arrives, it should end the turn instead of running sleep or status-polling loops. Pi will deliver the completion when the run finishes.
The oracle and worker builtins are designed for an explicit decision loop. A typical pattern is to ask oracle for diagnosis and a recommended execution prompt, then only run worker after the main agent approves that direction.
Chains open a clarify UI by default so you can preview and edit the workflow before it runs. Single and parallel tool calls can opt into the same flow with clarify: true; slash commands launch directly.
Common clarify keys:
Enterruns in the foreground, or in the background if background is toggled onEsccancels or backs out↑↓moves between steps or taskseedits the task/templatemselects a modeltselects thinking levelsselects skillsbtoggles background executionwedits output/write behavior where supportedredits reads where supportedptoggles progress tracking where supported
Picker screens use ↑↓, Enter, Esc, and type-to-filter. The full-screen editor supports word wrapping, paste, Esc to save, and Ctrl+C to discard.
pi-subagents works without pi-intercom. Install pi-intercom only if you want child agents to talk back to the parent Pi session while they are running. Install it as an optional companion package.
pi install npm:pi-intercomThe bridge recognizes the normal npm package install and legacy local extension checkouts.
Most users do not call intercom directly. After pi-intercom is installed, pi-subagents can automatically give child agents a private coordination channel back to the parent session.
Use it for work where the child might need a decision instead of guessing:
Run this implementation in the background. If the worker gets blocked or needs a product decision, have it ask me through intercom.
Ask oracle to review this plan. If it sees a decision I need to make, have it ask me instead of assuming.
The child can use one dedicated coordination tool:
contact_supervisor: the child contacts the parent/supervisor session that delegated the task. Usereason: "need_decision"for blocking decisions or clarification, andreason: "progress_update"for short non-blocking updates when a discovery changes the plan. Do not ask for clarification when the only conflict is review-only/no-edit versus progress-writing or artifact-writing instructions; no-edit wins.
Child-side routine completion handoffs are still not expected. With the intercom bridge active, parent-side pi-subagents sends grouped completion results through pi-intercom: one grouped message per foreground parent subagent run and one per completed async result file. Acknowledged foreground delivery returns a compact receipt with artifact/session paths; if unacknowledged, the normal full output is preserved. Grouped messages include child intercom targets and full child summaries.
If a child appears stalled, needs-attention notices can show up in the parent session with useful next actions, such as checking subagent({ action: "status" }), interrupting the run, or nudging the child.
If messages do not show up, run:
/subagents-doctor
For normal use, you do not need to configure anything. Advanced users can tune the bridge with intercomBridge in the configuration section below.
For web-backed research, use recon with a clear web-research prompt and make sure the parent Pi session has web tools available. Common options include:
- pi-web-access for
web_search,fetch_content, andget_search_content. @counterposition/pi-web-searchforweb_searchandweb_fetch.
Install either companion:
pi install npm:pi-web-access
# or
pi install npm:@counterposition/pi-web-searchAgents are markdown files with YAML frontmatter and a system prompt body. They define the specialist that will run in the child Pi process.
Agent locations, lowest to highest priority:
| Scope | Path |
|---|---|
| Builtin | ~/.pi/agent/extensions/subagent/agents/ |
| User | ~/.pi/agent/agents/**/*.md |
| Project | .pi/agents/**/*.md |
Project discovery also reads legacy .agents/**/*.md files. Nested subdirectories are discovered recursively. .chain.md files do not define agents. If both .agents/ and .pi/agents/ define the same parsed runtime agent name, .pi/agents/ wins. Use agentScope: "user" | "project" | "both" to control discovery; both is the default and project definitions win runtime-name collisions.
Builtin agents load at the lowest priority, so a user or project agent with the same name overrides them. They do not pin a provider model; they inherit your current Pi default model unless you set subagents.agentOverrides.<name>.model. oracle is an advisory reviewer that critiques direction and proposes an execution prompt without editing files. worker is the implementation agent for normal tasks and approved oracle handoffs.
Subagents are designed to be narrow by default. Custom agents start with a clean system prompt and only the context you intentionally give them. They do not automatically inherit Pi's whole base prompt, project instruction files, or discovered skills catalog.
Use these fields when an agent should see more:
| Field | Effect |
|---|---|
systemPromptMode: append |
Append the agent prompt to Pi's normal base prompt. |
inheritProjectContext: true |
Keep inherited project instructions from files like AGENTS.md and CLAUDE.md. |
inheritSkills: true |
Let the child see Pi's discovered skills catalog. |
defaultContext: fork |
Use forked session context when a launch omits context; explicit context: "fresh" still wins. |
Builtin agents opt into project instruction inheritance by default so they follow repo-specific rules out of the box. Custom agents created without explicit frontmatter use conservative generic defaults (systemPromptMode: replace, no implicit project/skill inheritance).
A typical agent looks like this:
---
name: helper
# Optional: registers this as code-analysis.helper while preserving name: helper
package: code-analysis
description: Focused project helper
tools: read, grep, find, ls, bash, mcp:chrome-devtools
extensions:
model: claude-haiku-4-5
fallbackModels: openai/gpt-5-mini, anthropic/claude-sonnet-4
thinking: high
systemPromptMode: replace
inheritProjectContext: false
inheritSkills: false
skills: safe-bash, chrome-devtools
output: context.md
defaultReads: context.md
defaultProgress: true
interactive: true
maxSubagentDepth: 1
---
Your system prompt goes here.maxSubagentDepth: 1 above is an example of an individual agent tightening the inherited/global limit; the default global limit is documented in the configuration table below.
Important fields:
| Field | Notes |
|---|---|
package |
Optional package identifier. A file with name: helper and package: code-analysis registers as code-analysis.helper; serialization keeps name and package separate. |
tools |
Builtin tool allowlist. mcp: entries select direct MCP tools when pi-mcp-adapter is installed. |
extensions |
Omitted means normal extensions; empty means no extensions; comma-separated values allowlist specific extensions. |
model |
Default model. Bare ids prefer the current provider when possible, then unique registry matches. |
fallbackModels |
Ordered backup models for provider/model failures such as quota, auth, timeout, or unavailable model. Ordinary task failures do not trigger fallback. |
thinking |
Appended as a :level suffix at runtime unless a suffix is already present. |
systemPromptMode |
replace by default; append keeps Pi's base prompt. |
inheritProjectContext |
Keeps or strips inherited project instruction blocks. |
inheritSkills |
Keeps or strips Pi's discovered skills catalog. |
defaultContext |
Optional fresh, lineage, or fork launch context default for this agent. |
skills |
Injects specific skills directly, regardless of inheritSkills. |
output |
Default single-agent output file. |
defaultReads |
Files to read before running in chain/parallel behavior. |
defaultProgress |
Maintain progress.md. |
interactive |
Parsed for compatibility but not enforced in v1. |
maxSubagentDepth |
Tightens nested delegation for this agent's children. |
If tools is omitted, pi-subagents does not pass --tools, so the child gets Pi's normal builtin tools. If tools is present, regular tool names become an explicit allowlist. mcp: entries are split out and forwarded as direct MCP selections. Path-like tools entries, such as extension paths or .ts/.js files, are treated as tool-extension paths rather than builtin tool names.
Examples:
toolsomitted andextensionsomitted: normal builtins and normal extensions.tools: mcp:chrome-devtools: normal builtins plus direct Chrome DevTools MCP tools.tools: read, bash, mcp:chrome-devtools: onlyreadandbashas builtins, plus direct Chrome DevTools MCP tools.
Direct MCP tools require pi-mcp-adapter. Subagents only receive direct MCP tools when mcp: entries are listed in their frontmatter; global directTools: true in mcp.json is not enough by itself. The generic mcp proxy tool can still be used for discovery when available. The adapter caches tool metadata at startup, so after connecting a new MCP server for the first time, restart Pi before relying on direct tools.
extensions controls child extension loading:
# Omitted: all normal extensions load
# Empty: no extensions
extensions:
# Allowlist
extensions: /abs/path/to/ext-a.ts, /abs/path/to/ext-b.tsWhen extensions is present, it takes precedence over extension paths implied by tools entries.
Chains are reusable .chain.md workflows stored separately from agent files.
| Scope | Path |
|---|---|
| User | ~/.pi/agent/chains/**/*.chain.md |
| Project | .pi/chains/**/*.chain.md |
Nested subdirectories are discovered recursively. If user and project scopes define the same parsed runtime chain name, the project chain wins. Chains support the same optional package frontmatter as agents; name: review-flow plus package: code-analysis runs as code-analysis.review-flow.
Example:
---
name: context-plan
description: Gather context then plan implementation
---
## recon
output: context.md
Analyze the codebase for {task}
## planner
reads: context.md
model: anthropic/claude-sonnet-4-5:high
progress: true
Create an implementation plan based on {previous}Each ## agent-name section is a step. Config lines such as output, outputMode, reads, model, skills, and progress go immediately after the header. A blank line separates config from task text.
For output, reads, skills, and progress, chain behavior is three-state: omitted inherits from the agent, a value overrides, and false disables.
Create chains by writing .chain.md files directly or with the subagent({ action: "create", config: ... }) management action. Run them with natural language or:
/chain context-plan -- refactor authentication
| Chain | Steps | Description |
|---|---|---|
go |
recon → planner → worker → reviewer | Full implementation pipeline: gather context, plan, implement, and review. |
The former review chain has been retired. Use /multireview for model-diverse review plus synthesis.
Task templates support:
| Variable | Description |
|---|---|
{task} |
Original task from the first step. |
{previous} |
Output from the prior step, or aggregated output from a parallel step. |
{chain_dir} |
Path to the chain artifact directory. |
{outputs.name} |
A prior step's captured output, by its as name (see Structured output). |
Parallel outputs are aggregated with clear separators before being passed to the next step:
=== Parallel Task 1 (worker) ===
...
=== Parallel Task 2 (worker) ===
...
A chain step (sequential, or a parallel task) can require its child to return schema-valid structured output instead of free-form prose. Set outputSchema to a JSON Schema object; the child must finish by calling the structured_output tool with a value that validates against it. The value is captured out-of-band (not parsed from prose) — a prose-only or invalid result fails the step.
Expose a step's result with as, then reference it from a later step with {outputs.name} (substituted with compact structured JSON when the step produced structured output, otherwise the step's text):
{
chain: [
{
agent: "recon",
task: "List the changed source files as JSON.",
as: "changed",
outputSchema: { type: "object", properties: { files: { type: "array", items: { type: "string" } } }, required: ["files"] },
},
{ agent: "reviewer", task: "Review these files: {outputs.changed}" },
],
}Bindings are validated before execution: as names must be unique valid identifiers, and {outputs.name} may only reference a step that has already produced its output. Structured output works in the foreground and in async/background runs, on chain steps (sequential steps and parallel tasks within a chain) — not top-level single dispatch. outputSchema and as are configured through the subagent({ chain: [...] }) JSON form or a saved .chain.js chain; .chain.md files do not parse outputSchema/as. The structured_output tool is always available to the child (even when the agent declares a custom tools: allowlist), and structured-output temp dirs are cleaned up after capture regardless of success or failure.
A chain step can expand an array from a prior step's structured output into N parallel subagent tasks, then collect the results back into a single named array. Use a single parallel template object (not an array) together with expand and collect:
{
chain: [
{
agent: "recon",
task: "Return JSON: a list of files to refactor.",
as: "plan",
outputSchema: { type: "object", properties: { files: { type: "array", items: { type: "string" } } }, required: ["files"] },
},
{
expand: { from: { output: "plan", path: "/files" }, item: "file", maxItems: 20 },
parallel: { agent: "worker", task: "Refactor {file}" },
collect: { as: "results" },
},
{ agent: "reviewer", task: "Summarize: {outputs.results}" },
],
}expand.fromaddresses the source array by a priorasname + a JSON Pointerpath.itemnames the per-item template variable ({item},{item.field});maxItemscaps the fanout (thedynamicFanoutMaxItemsconfig knob sets the default); per-item keys are de-duplicated.onEmptycontrols an empty source array:skip(default) stores an empty result array and continues the chain;failaborts the step.concurrencyandfailFastmirror the static-parallel semantics for the materialized tasks.collect.asstores the per-item results (optionally validated withcollect.outputSchema), referenceable as{outputs.<as>}. The substituted value is a JSON array of per-item result objects — each carries the expandeditem, theagent, and that item'stext/structuredoutput — not a single scalar, so its size grows with the item count. Per-itemasis not supported on a dynamic template; aggregate viacollect.as.
Dynamic fanout requires a prior step that produced a structured array (via as + outputSchema) — structured output (above) is a hard prerequisite. It is available through direct subagent({ chain: [...] }) JSON and saved .chain.js files, and works in the foreground and in async/background runs: the background runner materializes the per-item tasks at runtime from the prior step's structured output and collects them into {outputs.<as>} for downstream steps. Two async-only caveats apply: output at the expand step level is unsupported in async chains (use per-parallel-task output paths, which are automatically namespaced per child), and expand-template output paths inside an async chain must be relative (absolute paths cause a hard error).
Skills are SKILL.md files injected into an agent's system prompt.
Discovery uses project-first precedence:
.pi/skills/{name}/SKILL.md- Project packages and project settings packages via
package.json -> pi.skills - Current task cwd package via
package.json -> pi.skills .pi/settings.json -> skills~/.pi/agent/skills/{name}/SKILL.md- User packages and user settings packages via
package.json -> pi.skills ~/.pi/agent/settings.json -> skills
Use agent defaults, override them at runtime, or disable them:
{ agent: "worker", task: "..." }
{ agent: "worker", task: "...", skill: "test-writer" }
{ agent: "worker", task: "...", skill: false }For chains, skill at the top level is additive. A step-level skill overrides that step; false disables skills for that step.
Injected skills use this shape:
<skill name="safe-bash">
[skill content from SKILL.md, frontmatter stripped]
</skill>Missing skills do not fail execution. The result summary shows a warning.
The package bundles a pi-subagents skill that is automatically available to the parent agent when the extension is installed. It is for the orchestrating parent only: child subagents never receive it, and their context is explicitly filtered to strip parent-only orchestration instructions.
What the bundled skill covers:
- Delegation patterns: when to launch which agent, whether to use single, parallel, chain, or async mode, and whether to use fresh or forked context
- Prompt workflow recipes: how to apply the packaged techniques directly with
subagent(...)when the user describes the workflow in natural language instead of invoking a slash command. This includes parallel review, parallel research, parallel handoff-plan, brainstorm, write-plan, and parallel cleanup - Role-agent prompting guidance: compact contract prompts instead of long scripts, what to include in role-specific meta prompts, and retrieval budgets for context-building research
- Safety boundaries: child agents must not run subagents, must not invent intercom targets, and must escalate unapproved decisions
- Intercom conventions: when to ask vs send, and how parent-side result delivery works with
pi-intercom - Control and diagnostics: attention signals, soft interrupts, status, and the
doctoraction
If you are writing an agent that orchestrates subagents, the bundled skill helps it behave correctly without guessing the patterns. If you are a human user, you do not need to read it directly; the README and prompt shortcuts encode the same workflows in user-facing form.
The package also bundles a test-writer skill (skills/test-writer/SKILL.md) for subagents tasked with writing tests. It guides the agent through mandatory test infrastructure discovery — finding the exact test runner command, loader shims, existing helpers, and mock patterns — before writing any test code. Use it by injecting the skill into worker for test-focused implementation.
Child-safety boundaries are enforced at runtime. Spawned child sessions do not register the subagent tool, do not receive the bundled pi-subagents skill, and receive explicit boundary instructions that they are not the parent orchestrator and must not propose or run subagents. Forked child context filtering also removes parent-only subagent artifacts (including old hidden orchestration-instruction messages, slash/status/control messages, and prior parent subagent tool-call/tool-result history) so children only see genuine task-relevant context.
Use orchestration as parent-agent guidance, not as a runtime workflow mode. For implementation work, the recommended loop is:
clarify → planner → worker → fresh reviewers → worker
Use the optional prompt shortcuts below when you want the pattern to be repeatable.
The package includes reusable prompt templates for common workflows. You do not need them, but they are handy when you want the same shape every time:
| Prompt | Use it for |
|---|---|
/multireview |
Launch fresh-context reviewers with distinct angles, then synthesize what to fix. |
/multirecon |
Quick parallel recon pass; add deep for artifact-backed lane synthesis. |
/mesh-handoff |
Combine external research and recon passes into an implementation handoff plan and meta-prompt. |
/mesh-context |
Run parallel recon passes for planning or implementation handoff context. |
/mesh-cleanup |
Run review-only cleanup passes after implementation; add autofix to apply only fixes worth doing now. |
/brainstorm |
Design-first exploration before any implementation, with clarifying questions and approach tradeoffs. |
/write-plan |
Author an implementation plan from a spec/intent with explicit validation commands and a placeholder scan. |
/gather-context-and-clarify |
Gather focused context, then ask the remaining clarification questions before planning or implementation. |
These are the parameters the LLM passes when it calls the subagent tool. Most users ask naturally or use slash commands instead.
// Single agent
{ agent: "worker", task: "refactor auth" }
{ agent: "recon", task: "find todos", output: "reports/context.md", outputMode: "file-only" }
{ agent: "recon", task: "investigate", output: false }
{ agent: "worker", task: "write a large report", output: "reports/worker.md", outputMode: "file-only" }
// Forked context
{ agent: "worker", task: "continue this thread", context: "fork" }
// Parallel
{ tasks: [{ agent: "recon", task: "a" }, { agent: "reviewer", task: "b" }] }
{ tasks: [{ agent: "recon", task: "audit auth", count: 3 }] }
{ tasks: [{ agent: "recon", task: "audit frontend" }, { agent: "reviewer", task: "audit backend" }], context: "fork" }
// Chain
{ chain: [
{ agent: "recon", task: "Gather context for auth refactor" },
{ agent: "planner" },
{ agent: "worker" },
{ agent: "reviewer" }
]}
// Chain without TUI, suitable for background execution
{ chain: [...], clarify: false, async: true }
// Chain with fan-out/fan-in
{ chain: [
{ agent: "recon", task: "Gather context" },
{ parallel: [
{ agent: "worker", task: "Implement feature A from {previous}" },
{ agent: "worker", task: "Implement feature B from {previous}" }
], concurrency: 2, failFast: true },
{ agent: "reviewer", task: "Review all changes from {previous}" }
]}
// Chain with structured output + named references
{ chain: [
{ agent: "recon", task: "List changed files as JSON", as: "files",
outputSchema: { type: "object", properties: { files: { type: "array", items: { type: "string" } } }, required: ["files"] } },
{ agent: "worker", task: "Refactor each file in {outputs.files}" }
]}
// Dynamic fanout: expand a prior step's array into N parallel tasks (foreground)
{ chain: [
{ agent: "recon", task: "List modules as JSON", as: "mods",
outputSchema: { type: "object", properties: { mods: { type: "array", items: { type: "string" } } }, required: ["mods"] } },
{ expand: { from: { output: "mods", path: "/mods" }, maxItems: 10 },
parallel: { agent: "worker", task: "Document module {item}" },
collect: { as: "docs" } },
{ agent: "reviewer", task: "Review module docs from {outputs.docs}" }
]}
// Worktree isolation
{ tasks: [
{ agent: "worker", task: "Implement auth" },
{ agent: "worker", task: "Implement API" }
], worktree: true }Agent definitions are not loaded into context by default. Management actions let the LLM discover, inspect, create, update, and delete agents and chains at runtime.
{ action: "list" }
{ action: "list", agentScope: "project" }
{ action: "get", agent: "recon" }
{ action: "get", agent: "code-analysis.helper" }
{ action: "get", chainName: "review-pipeline" }
{ action: "create", config: {
name: "Code Helper",
package: "code-analysis",
description: "Scans codebases for patterns and issues",
scope: "user",
systemPrompt: "You are a focused codebase helper...",
systemPromptMode: "replace",
inheritProjectContext: false,
inheritSkills: false,
model: "anthropic/claude-sonnet-4",
fallbackModels: ["openai/gpt-5-mini", "anthropic/claude-haiku-4-5"],
tools: "read, bash, mcp:github/search_repositories",
extensions: "",
skills: "codebase-onboarding",
thinking: "high",
output: "context.md",
reads: "shared-context.md",
progress: true
}}
{ action: "create", config: {
name: "review-pipeline",
description: "Context then review",
scope: "project",
steps: [
{ agent: "recon", task: "Scan {task}", output: "context.md" },
{ agent: "reviewer", task: "Review {previous}", reads: ["context.md"] }
]
}}
{ action: "update", agent: "code-analysis.helper", config: { model: "openai/gpt-4o" } }
{ action: "update", chainName: "review-pipeline", config: { steps: [...] } }
{ action: "delete", agent: "helper" }
{ action: "delete", chainName: "review-pipeline" }create uses config.scope, not agentScope. config.name is the local frontmatter name; optional config.package registers the runtime name as {package}.{name} and is saved as separate name and package frontmatter. update and delete use the runtime name and agentScope only when the same runtime name exists in multiple scopes. To clear optional string fields, including package, set them to false or "".
| Param | Type | Default | Description |
|---|---|---|---|
agent |
string | - | Agent name for single mode, or target for management actions. |
task |
string | - | Task string for single mode. |
action |
string | - | list, get, create, update, delete, status, interrupt, resume, steer, follow-up, wrap-up, recover, inspect, attach, detach, or doctor. |
chainName |
string | - | Chain name for management actions. |
config |
object/string | - | Agent or chain config for create/update. |
output |
string | false |
agent default | Override single-agent output file. |
outputMode |
"inline" | "file-only" |
inline |
Return saved output inline or as a concise saved-file reference. file-only requires an output path. |
skill |
string | string[] | false |
agent default | Override skills or disable all. |
model |
string | agent default | Override model. |
tasks |
array | - | Top-level parallel tasks. Supports agent, task, cwd, count, output, outputMode, reads, progress, skill, and model. |
concurrency |
number | config or 4 |
Top-level parallel concurrency. |
budget |
number | config default | Per-run output-token ceiling. Counts completed child output tokens only; when spent output reaches the ceiling, later chain/background steps are skipped as budget-exhausted while already-launched parallel children finish. It does not terminate an in-flight child and has no later dispatch boundary to enforce in single-agent or top-level parallel mode. |
worktree |
boolean | false | Create isolated git worktrees for parallel tasks. |
chain |
array | - | Sequential and parallel chain steps. |
context |
fresh | lineage | fork |
agent default or fresh |
fresh starts a clean independent child session; lineage starts clean but links the child session under the parent tree; fork creates a branched child session with the inherited parent transcript. Packaged worker and oracle default to fork; planner and other planning roles default to fresh. |
chainDir |
string | temp chain dir | Persistent directory for chain artifacts. |
clarify |
boolean | true for chains | Show TUI preview/edit flow. |
agentScope |
user | project | both |
both |
Agent discovery scope. Project wins on collisions. |
async |
boolean | false | Background execution. Chains require clarify: false. |
cwd |
string | runtime cwd | Override working directory. |
maxOutput |
object | 200KB, 5000 lines | Post-run truncation of text returned inline (bytes/lines). Full output remains in artifacts/session data. It does not limit model generation or child runtime. |
artifacts |
boolean | true | Write debug artifacts. |
includeProgress |
boolean | false | Include full progress in result. |
share |
boolean | false | Upload session export to GitHub Gist. |
sessionDir |
string | derived | Override session log directory. |
control |
object | defaults below | Attention and liveness controls. activeNoticeAfter* fields notify only; inactivity and overall wall-clock limits can terminate children. |
context: "fork" fails fast when the parent session is not persisted, the current leaf is missing, or the branched child session cannot be created. It never silently downgrades to fresh. In multi-agent runs, if any requested agent has defaultContext: fork and the launch omits context, the whole invocation uses forked context; pass context: "fresh" when you intentionally want a fresh run.
context: "lineage" is the middle ground between fresh and fork: the child gets a blank model context, but its session header points at the parent session file so Pi can show the relationship in session-tree tools. Use it when you want traceable subagent branches without paying to copy the parent transcript. Lineage does not remove child Pi process startup latency — it is an organization/linking feature, not a performance one. V1 requires all children to run in the top-level cwd; use fork when you need the child to continue the parent's conversation without a summary handoff. fork-only intercom mode intentionally does not activate for lineage children.
Use outputMode: "file-only" when a saved output may be large and the parent only needs a pointer. The returned text is a compact reference like Output saved to: /abs/report.md (48.2 KB, 2847 lines). Read this file if needed. Failed runs and save errors still return normal inline output for debugging. In chains, later {previous} steps receive the same compact reference when the prior step used file-only mode.
Sequential and parallel chain tasks accept agent, task, cwd, output, outputMode, reads, progress, skill, model, lane, and thinking. Parallel tasks also accept count. Parallel step groups accept parallel, concurrency, failFast, and worktree. Chain steps and parallel tasks additionally accept as (expose the result under a name) and outputSchema (require schema-valid structured output) — see Structured output. A dynamic-fanout step replaces agent/task with expand + a single parallel template object + collect — see Dynamic fanout.
Status and control actions:
subagent({ action: "status" });
subagent({ action: "status", id: "<run-id>" });
subagent({ action: "interrupt", id: "<run-id>" });
subagent({ action: "resume", id: "<run-id>", message: "follow-up question" });
subagent({
action: "resume",
id: "<run-id>",
index: 1,
message: "follow-up for child 2",
});
subagent({ action: "doctor" });resume sends the follow-up directly when an async child is still reachable over intercom. After completion, it revives the child by starting a new async child from the stored child session file. Multi-child async runs and remembered foreground single, parallel, or chain runs can be revived by passing index to choose the child. Revive starts a new child process from the old session context; it does not restart the same OS process, and it requires the chosen child to have a persisted .jsonl session file. Revived multi-child async runs attach the revived child's results as an update to the original run.
steer, follow-up, and wrap-up deliver control text to a live run (foreground or async) through the durable live-control file route. Target a run by id (an unambiguous prefix works); parallel and chain runs require index to pick the exact child. steer and follow-up require message; wrap-up needs none and always sends the canonical wrap-up directive over the steer path.
subagent({ action: "steer", id: "<run-id>", message: "switch to plan B" });
subagent({ action: "follow-up", id: "<run-id>", index: 1, message: "also cover the edge case" });
subagent({ action: "wrap-up", id: "<run-id>" });
// Idempotent retry: reusing requestId returns the original durable result instead of delivering twice.
subagent({ action: "steer", id: "<run-id>", message: "...", requestId: "req-1" });Every response reports the actual durable disposition, never a guess: accepted-by-pi with started-turn (the run was idle), queued-steer, or queued-follow-up; rejected with the durable reason; submitted when the owner has not acknowledged within the wait window; or outcome-unknown when delivery was attempted but never acknowledged (the crash window — never silently retried). Acceptance means the owning Pi session accepted or queued the message, not that the model acted on it. steer is never silently downgraded to follow-up. Delivery rides the child's 250ms control poll, so expect sub-second latency in the common case; a steer queues after the current turn's tool calls — it does not interrupt mid-token.
recover, inspect, attach, and detach let you find, observe, and gain control of a run — including after an extension reload or parent crash, when in-memory state is empty. Target a run by id (an unambiguous prefix works).
subagent({ action: "recover", id: "<run-id>" });
subagent({ action: "inspect", id: "<run-id>" });
subagent({ action: "attach", id: "<run-id>", index: 1 });
subagent({ action: "detach", attachmentId: "<attachment-id>" });recoverreports whether a run is currently resolvable (live in-memory, or durably recorded on disk). A durable handle is recorded at every foreground and async launch and deleted on completion/cleanup, so a run that started before a reload can be found again. Recovering a handle never itself grants steering — useattachto verify live-control capability beforesteer/follow-up/wrap-up. Foreground runs are only resolvable while in-memory: after a reload they are not recoverable (their host process is the parent, so PID liveness is useless), andrecoverreports this honestly.inspectreturns a compact state summary for any run — live or completed. For completed async runs it reads the result file; for completed nested runs it reads the registry. No transcript or output fields are included.attachverifies live-control capability (owner epoch + capability token) and records a durableattachmentId. The response distinguishes steering-capable (live owner verified;steer/follow-up/wrap-upmay be used) from inspection-only (no live owner, e.g. a completed or result-only async run — control actions are not available). A recovered async or nested handle must be attached before steering.detachrevokes an attachment byattachmentId(falls back toid). Idempotent.
Nested descendants are not recorded as separate handles: a nested run is rediscovered via its parent's route and the durable file-based nested registry, so recover/inspect reach it through the parent.
A detached async run outlives the turn that launched it. In an interactive session a completion notification eventually arrives, but a non-interactive pi -p run can exit with children still going, and a skill that launches work then needs the results has nothing to block on. subagent_wait is that block.
subagent_wait() // return when the first tracked run settles
subagent_wait({ all: true }) // drain every tracked run
subagent_wait({ id: "a451bd3f" }) // wait for one run
subagent_wait({ all: true, timeoutMs: 120000 }) // cap the block at two minutesA run counts as settled when it reaches complete, failed, or paused — or when it raises needs_attention, so an escalation waiting on a decision cannot be slept through. Run state is read from each run's own status.json rather than the session's in-memory projection.
The default timeout is 10 minutes and the hard maximum is 30. A timeout or an aborted turn returns honestly and never interrupts the runs; they keep going and can be waited on again. subagent_wait reports state only — read output with subagent({ action: "status" }).
Parallel agents can clobber each other if they edit the same checkout. worktree: true gives each parallel child its own git worktree branched from HEAD.
{ tasks: [
{ agent: "worker", task: "Implement auth", count: 2 },
{ agent: "worker", task: "Implement API" }
], worktree: true }
{ chain: [
{ agent: "recon", task: "Gather context" },
{ parallel: [
{ agent: "worker", task: "Implement feature A from {previous}" },
{ agent: "worker", task: "Implement feature B from {previous}" }
], worktree: true },
{ agent: "reviewer", task: "Review all changes from {previous}" }
]}Requirements:
- run inside a git repo
- working tree must be clean
node_modules/is symlinked into each worktree when present- task-level
cwdoverrides must be omitted or match the shared cwd - configured
worktreeSetupHookmust return valid JSON before timeout
After a worktree parallel step completes, per-agent diff stats are appended to the output and full patch files are written to artifacts. Worktrees and temp branches are cleaned up in finally blocks.
pi-subagents reads optional JSON config from ~/.pi/agent/extensions/subagent/config.json.
| Setting | Type | Default | Description |
|---|---|---|---|
asyncByDefault |
boolean | false |
Use background execution when the request does not explicitly set async. Callers can still force foreground with async: false unless forceTopLevelAsync is enabled. |
forceTopLevelAsync |
boolean | false |
Force depth-0 single, parallel, and chain runs into background mode and bypass clarify UI. Nested calls keep their own inherited settings. |
parallel.maxTasks |
number | 8 |
Maximum parallel tasks. |
parallel.concurrency |
number | 4 |
Maximum concurrent parallel tasks. Per-call concurrency takes precedence. |
defaultSessionDir |
string | derived | Session directory. Precedence: params.sessionDir → config.defaultSessionDir → derived from parent session. |
maxSubagentDepth |
number | 2 |
Nested delegation limit when no inherited PI_SUBAGENT_MAX_DEPTH is in effect. Per-agent maxSubagentDepth can tighten but not relax an inherited limit. |
sessionTokenBudget |
number | none | Default per-run output-token ceiling used when a call omits budget. Counts completed child output tokens only; exhausted budgets stop later chain/background dispatch and report budget-exhausted without killing in-flight children. It has no enforcement boundary in single-agent or top-level parallel runs; maxOutput only truncates returned text after completion. |
control.enabled |
boolean | true |
Enable control notices and timeout enforcement. |
control.needsAttentionAfterMs |
number | 120000 |
Emit needs-attention after this much inactivity. |
control.activeNoticeAfterMs |
number | 240000 |
Emit a long-running notice after elapsed time; notice only, not a kill threshold. |
control.activeNoticeAfterTurns |
number | none | Optional completed-turn notice threshold; notice only. |
control.activeNoticeAfterTokens |
number | none | Optional reported input+output-token notice threshold; notice only. |
control.failedToolAttemptsBeforeAttention |
number | 3 |
Consecutive mutating-tool failures before needs-attention. |
control.notifyOn |
array | active/attention | Control event types surfaced to the parent. |
control.notifyChannels |
array | event/async/intercom | Available control notification channels. |
control.stepInactivityTimeoutMs |
number | 300000 |
No-activity timeout for a running child step. |
control.runWallClockTimeoutMs |
number | 1800000 |
Overall run timeout for foreground and background execution, enforced even while activity continues. What it does when it fires is governed by control.timeoutAction. |
control.timeoutAction |
string | escalate_then_kill |
Action for both the inactivity and overall wall-clock timeouts: notify, escalate_then_kill, or auto_kill. Under notify the run has no duration backstop — stop it with interrupt. |
control.escalationGraceMs |
number | 30000 |
Grace after an inactivity escalation before termination. Doubles as the wrap-up lead time: the run wall-clock nudge fires this long before the deadline. |
intercomBridge.mode |
string | "always" |
"always", "fork-only" (inject only for forked runs), or "off". |
intercomBridge.instructionFile |
string | built-in | Optional Markdown template replacing the default bridge instructions. {orchestratorTarget} is interpolated. Relative paths resolve from ~/.pi/agent/extensions/subagent/. |
worktreeSetupHook |
string | none | Script to run once per created worktree. Paths must be absolute, ~/..., or repo-relative. stdin is JSON with repoRoot, worktreePath, agentCwd, branch, index, runId, baseCommit. stdout must be JSON, e.g. { "syntheticPaths": [".venv"] }. |
worktreeSetupHookTimeoutMs |
number | 30000 |
Timeout for the worktree setup hook. |
inlineReadMaxBytes |
number | 204800 |
Max bytes for inline-read content in fresh-context children. Range: [1024, 8MB]. |
dynamicFanoutMaxItems |
number | none | Default cap on dynamic-fanout expanded items when a step omits expand.maxItems. A dynamic step with no effective cap (neither here nor on the step) is rejected before execution. |
toolBudget.hard |
number | none | Tool-call ceiling for each child. Once a child has made this many tool calls, budgeted tools are blocked for the rest of its run so a runaway explorer still finishes with a final text answer. Blocked attempts do not consume budget. Enforced inside the child process; omit the whole toolBudget object to leave children uncapped. |
toolBudget.soft |
number | none | Nudge threshold. The first tool result after this many calls carries a one-time [tool budget] line telling the child to start converging. Ignored unless it is below hard. |
toolBudget.block |
array | none | Tool names blocked at the hard limit. Omit it and every tool is blocked except structured_output, contact_supervisor, and intercom, so a child can always report back. |
subagents.modelLanes |
object | none | Named model/thinking pairs in subagents.modelLanes.<agentName>.<laneName> = { model?, thinking? }. Lives in ~/.pi/agent/settings.json (user scope, editable in the /subagents TUI) or .pi/settings.json (project scope, display-only in the TUI). Project lanes override user lanes at dispatch time. Users must write project lanes by hand. Lane names created or renamed via the TUI must match /^[a-z0-9][a-z0-9-]*$/; pre-existing free-form lane names are accepted on read and remain editable in place. TUI persistence is atomic and merge-preserving. See Model lanes and the /subagents lane editor. |
Bridge activation requires pi-intercom to be installed and enabled, a targetable session name, and pi-intercom in any explicit agent extensions allowlist. The default injected guidance tells children to use contact_supervisor with reason: "need_decision" when blocked and reason: "progress_update" for meaningful updates.
Each chain run creates a user-scoped temp directory like:
<tmpdir>/pi-subagents-<scope>/chain-runs/{runId}/
It may contain files such as context.md, plan.md, progress.md, and parallel-{stepIndex}/.../output.md. Directories older than 24 hours are cleaned up on extension startup.
Debug artifacts live under {sessionDir}/subagent-artifacts/ or a user-scoped temp artifact directory. Per task you may see:
{runId}_{agent}_input.md{runId}_{agent}_output.md{runId}_{agent}.jsonl{runId}_{agent}_meta.json
Metadata records timing, usage, exit code, final model, attempted models, and fallback attempt outcomes.
Session files are stored under a per-run session directory. With context: "fork", each child starts with --session <branched-session-file> produced from the parent's current leaf. That is a real session fork, not an injected summary.
Async completions notify only the originating session. The result watcher emits subagent:async-complete, and the extension consumes that event to render completion notifications.
Async runs write:
<tmpdir>/pi-subagents-<scope>/async-subagent-runs/<id>/
status.json
events.jsonl
output-<n>.log
subagent-log-<id>.md
status.json powers the widget and subagent({ action: "status" }) output. events.jsonl contains wrapper events plus child Pi JSON events annotated with run and step metadata. output-<n>.log is a live human-readable tail. Fallback information is persisted so background runs are debuggable after completion.
Foreground runs show compact live progress for single, chain, and parallel modes: current tool, recent output, token counts, duration, activity freshness, and current-tool duration.
Press Ctrl+O to expand the full streaming view with complete output per step.
Sequential chains show a flow line like done recon → running planner. Chains with parallel steps show per-step cards instead.
Pass share: true to export a full session to HTML, upload it to a secret GitHub Gist through your gh credentials, and return a https://shittycodingagent.ai/session/?<gistId> URL.
{ agent: "recon", task: "...", share: true }This is disabled by default. Session data may contain source code, paths, environment variables, credentials, or other sensitive output. You need gh installed and authenticated.
Subagents can call subagent, which can get expensive and hard to observe. A depth guard prevents unbounded nesting.
By default, nesting is limited to two levels: main session → subagent → sub-subagent. Deeper calls are blocked with guidance to complete the current task directly.
Configure the limit with:
PI_SUBAGENT_MAX_DEPTHbefore starting Piconfig.maxSubagentDepthmaxSubagentDepthin agent frontmatter, which can only tighten the inherited limit
export PI_SUBAGENT_MAX_DEPTH=3
export PI_SUBAGENT_MAX_DEPTH=1
export PI_SUBAGENT_MAX_DEPTH=0PI_SUBAGENT_DEPTH is internal and propagated automatically. Do not set it manually.
Each child process is spawned with --mode json, and its event stream is watched for runaway output. When any guard below trips, the child is aborted (SIGINT, then SIGTERM after 1s) and the step fails with a runaway output aborted: ... error that names the cause; async runs record it in status.json and events.jsonl.
- Degenerate streaming loops. A model can get stuck repeating a fragment forever — e.g. reissuing the trailing key-value pair of a tool call's JSON arguments (
, "timeout": 60000, "timeout": 60000, ...) and never closing the object. A periodic-suffix scan over the normalized per-content-block delta tail detects this within seconds and aborts with the repeated fragment named. It is tolerant of cycling values and shifting chunk boundaries (numeric literals and whitespace are normalized), and only aborts when the normalized pattern is confirmed against the raw tail — so real incrementing tables/CSVs that normalize to the same shape but differ verbatim are never killed by the loop detector. - Rolling no-progress thinking floods. The primary guard counts delta-aware model output since the most recent text/tool activity and aborts once it crosses the 8 MB no-progress trip — so a model that keeps thinking without ever committing to text or a tool call is stopped, while a fine-grained streamer whose
--mode jsonsnapshots amplify a small amount of real thought into tens of MB of raw stdout is not falsely killed. Every parsed event credits its bytes back to a separate 32 MB non-JSON backstop, so fully-parsed streams survive regardless of raw volume. - Hard cap. A backstop bounds cumulative unaccounted (unparsed) raw stdout to 1 GB — every successfully-parsed JSON event credits its serialized bytes back, so fully-parsed streams never hit this limit even at extreme volumes (~1,024 MB raw observed in production with a 121x snapshot-amplification run, survived). The 200 MB model-output cap counts delta-aware accounted bytes (the payload each
message_updateactually adds, not the full re-serialized snapshot).
These guards are internal and not configurable; they sit below the per-step inactivity timeout and overall run wall-clock timeout, which govern liveness. Foreground retries, parallel siblings, queued work, and chain steps inherit one run-wide wall-clock deadline; queued children are not launched after it expires. Under escalate_then_kill (the default) a timed_out_escalating notice fires one escalationGraceMs before that deadline so a live child can be told to wrap up while it still has time; the deadline itself stays a synchronous hard stop. Setting timeoutAction: "notify" makes the deadline advisory instead: nothing is killed and no dispatch is blocked, so the run has no duration backstop and must be stopped with interrupt.
Async events:
subagent:async-startedsubagent:async-complete
Intercom delivery events:
subagent:control-intercomsubagent:result-intercom
The result watcher emits subagent:async-complete; src/extension/index.ts registers the notification handler that consumes it. Control/attention events are surfaced as visible parent notices and persisted for async runs. With pi-intercom, needs-attention notices and grouped parent-side subagent result deliveries can reach the orchestrator over intercom.
pi-subagents works standalone through natural language, the subagent tool, slash commands, and the packaged prompt shortcuts listed above. Pi loads bundled prompt templates from this package's pi.prompts manifest entry plus user/project prompt directories. The prompt-template bridge lets those templates request subagent runs with prescribed agents, context modes, outputs, and model settings.
Example:
---
description: Take a screenshot
model: claude-sonnet-4-20250514
subagent: browser-screenshoter
cwd: /tmp/screenshots
---
Use url in the prompt to take screenshot: $@Then /take-screenshot https://example.com switches to Sonnet, delegates to browser-screenshoter with /tmp/screenshots as cwd, and restores your model when done. Runtime overrides like --cwd=<path> and --subagent=<name> work too.
For additional reusable workflows on top of subagents, add custom prompt templates under ~/.pi/agent/prompts/, project .pi/prompts/, or another package that declares pi.prompts.
Bug fixes:
- Fix 1 — Output recovery on non-zero exit: When a child exits with an error but produced partial output, the recovered text is returned with an
[Subagent exited with error: …]footer instead of bare"terminated". No more losing useful output to a non-zero exit code. - Bug A —
defaultReadsdropped on fresh-context single-mode: An agent's frontmatterdefaultReadswas silently ignored when called without explicitreadsin single-mode fresh context. Now flows through correctly; onlyreads: falseexplicitly opts out. - Bug B — "Pre-loaded files" lying when a read fails: Failed reads were wrapped in the
Pre-loaded files (do not Read these)header, telling the child not to Read a file it manifestly should. Failed entries are now pulled out and emitted as a separate[Read from: <failed paths>]hint. - Bug C — Truncation marker mislabel: Truncation footer said
bytesbut sliced characters (up to 4× off on multi-byte text). Now sayscharactersand usesBuffer.byteLengthfor the cap comparison. - Bug D —
parseReadSpecregex greedily eats colons in filenames: A file namedfoo:5-10.bakwas mis-parsed as pathfoo+ range5-10. Now stat-checks the literal path first; only parses as a range when the literal doesn't exist. - Bug F — Tilde expansion in glob specs:
reads: ["~/.pi/.../*.ts"]failed because~wasn't expanded beforefs.globSync. Fixed with the same tilde-expansion pattern asresolveChainPath.
Features:
- Auto-inline reads on
context: "fresh": Files listed inreadsare pre-loaded into the child's first user message, saving the child a full context-fork (~261K tokens → ~13K tokens observed). This was partially upstream (parallel/chain paths); our fork extends it to single-agent paths and fixes the plumbing. - Glob support in
reads:reads: ["src/**/*.ts"]expands glob patterns with deterministic sorting and a 50-match cap. Literal files with glob characters (weird[brackets].ts) are handled via stat-fallback (same pattern as Bug D). Zero-match globs emit a[Read from glob (no matches): ...]hint. - Config-exposed
inlineReadMaxBytes: The 200KB inline-read cap is now configurable viainlineReadMaxBytesin extension config, with a[1024, 8MB]range guard. - Token-economy footer: Fresh-context results append
[mode=fresh, in=…, out=…, cache_read=…, cache_write=…]so savings are observable without digging into JSONL. - Recovery telemetry: When Fix 1's output-recovery triggers, a structured
subagent_recoveryevent (withrunId,agent,exitCode,recoveredChars,elapsedMs) is emitted into the parent's session JSONL (display: false). - Compatibility agents preserved disabled-by-default: Legacy roles such as
oracle-freshremain on disk for explicit opt-in, while the default visible roster stays focused on recon, planner, worker, reviewer, oracle, and janitor. plannerflipped todefaultContext: fresh: The bundled planner now defaults to fresh context with curateddefaultReads, routing every existing caller through the cheaper path without code changes.--no-context-filesfor fresh children: Fresh-context children now get--no-context-filesin their spawn args, preventingAGENTS.md/CLAUDE.mdfrom leaking into the child's system prompt. Forked children keep their normal context loading.
Optimizations:
- Cache + deduplicate inline reads: A process-scoped
Map<string, string>keyed by(absPath, mtimeMs, size)cachesreadFileSyncresults. Same file across N parallel tasks = 1 read, N-1 cache hits. Automatic invalidation on file change via mtime+size. - Drop redundant
resolveStepBehaviorcall: The single-mode fresh-reads patch resolved output/skills/model/progress just to extractreads. Replaced with an inline fallback:const reads = readsOverride ?? agentConfig.defaultReads ?? false.
Ported from upstream (v0.39.0):
- Structured output (
outputSchema+{outputs.name}) and dynamic fanout (expand/collect) were reimplemented from upstream — both in the foreground and in the async/background runner (async dynamic fanout landed in v0.40.0). Upstream's acceptance-gate and workflow-graph machinery were intentionally not ported. See the Structured output and Dynamic fanout sections.
Visible cleanup role:
janitor: Repository hygiene agent withcontact_supervisorescalation. Designed for dead code removal, stale docs, orphaned artifact audits, and structural cleanup. The olddeslopperfile remains as a disabled compatibility alias.- Compatibility aliases can be re-enabled explicitly through builtin overrides when needed.
All current unit and integration tests pass in this checkout; see the latest changelog entry for current counts.
The main runtime files are:
| File | Purpose |
|---|---|
src/extension/index.ts |
Extension registration, tool registration, message/render wiring. |
src/agents/agents.ts |
Agent and chain discovery, frontmatter parsing. |
src/runs/foreground/subagent-executor.ts |
Main execution routing for single, parallel, chain, management, status, interrupt, and doctor actions. |
src/runs/foreground/execution.ts |
Core foreground runSync handling. |
src/runs/background/subagent-runner.ts |
Detached async runner. |
src/runs/background/async-execution.ts |
Background launch support. |
src/runs/background/async-status.ts |
Status discovery and formatting for async runs. |
src/runs/foreground/chain-execution.ts / src/agents/chain-serializer.ts |
Chain orchestration and .chain.md parsing. |
src/shared/settings.ts |
Chain behavior, instructions, and config helpers. |
src/runs/shared/worktree.ts |
Git worktree isolation. |
src/runs/shared/usage.ts / exit-drain.ts / output-buffer.ts / stdio-parser.ts |
Shared runner primitives: usage accumulation, drain timer constants, recent-output ring buffer, JSON line processor. |
src/tui/subagent-hub.ts |
Subagent hub TUI for browsing agents and configuring model overrides before launch. |
src/intercom/intercom-bridge.ts |
Runtime intercom bridge instructions and diagnostics. |
src/extension/schemas.ts / src/shared/types.ts |
Tool schemas, shared types, and event constants. |
src/runs/shared/structured-output.ts / chain-outputs.ts / dynamic-fanout.ts |
Structured-output capture and validation, {outputs.name} resolution, and dynamic-fanout materialization. |
test/unit/ / test/integration/ |
Unit and loader-based integration tests. |
MIT © Darko Kuzmanovic