Skip to content

Repository files navigation

pi-subagents

pi-subagents

pi-subagents lets Pi delegate work to focused child agents. Use it for code review, scouting, implementation, parallel audits, saved workflows, background jobs, and anything else that benefits from a second or third set of model eyes.

pi-subagents-chain.mp4

Installation

pi install npm:pi-subagents

That is the only required step. You can add optional pieces later.

Try this first

You 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 scout 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.

External CLI agent profiles

Agent profiles can opt into a local one-shot command instead of a Pi child. External runners add no install dependency, but the configured executable must exist at runtime. They are async-only, receive one combined system/task prompt over stdin, and use argv arrays without a shell:

runner:
  type: external-cli
  command: node
  args: ["./scripts/local-reviewer.mjs"]
  promptDelivery: stdin
async: true

External CLI runners support status artifacts, stdout/stderr logs, timeout, and stop. Full stdout and stderr are written to log files, while the in-memory final stdout response and stderr error are limited to their last 64 KiB. Foreground/clarify, steer/resume/interrupt-as-pause, Pi models/tools/extensions, skills, structured output, nested subagents, and fallback models are intentionally unsupported.

What happens

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.

Good first prompts

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.
Run a review loop on this change until reviewers stop finding fixes worth doing, with a max of 3 rounds.
Use scout 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 how to express composed work with workflowScript.

Common workflows

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.”
Review until clean “Run a review loop on this change with a max of 3 rounds.”
Execute a plan carefully “Have worker implement this approved plan, then run reviewers and apply the feedback.”
Scout before planning “Use scout 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 the review chain on this branch.”
See running work “Show active async runs.” or “Show the subagent fleet.”
Check setup “Check whether subagents are configured correctly.”

The extension ships with builtin agents you can use immediately.

Builtin agents in plain English

Agent Use it when you want...
scout Fast local codebase recon: relevant files, entry points, data flow, risks, and where another agent should start.
researcher Web/docs research with sources: official docs, specs, benchmarks, recent changes, and a concise research brief.
planner A concrete implementation plan from existing context. It should read and plan, not edit code.
worker Implementation work, including approved oracle handoffs. It edits files, validates, and escalates unapproved decisions instead of guessing.
reviewer Code review and small fixes. It checks the implementation against the task/plan, tests, edge cases, and simplicity.
context-builder A stronger setup pass before planning: gathers code context and writes handoff material such as context.md and meta-prompt.md.
oracle A second opinion before acting. It challenges assumptions, catches drift, and recommends the safest next move without editing.
delegate A lightweight general delegate when you want a child agent that behaves close to the parent session.

A simple rule of thumb: use scout before you understand the code, researcher before you trust external facts, planner before a bigger change, worker to implement, reviewer to check, and oracle when the decision itself feels risky.

Changing an agent's model

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 every subagent without its own model to use a different default, set subagents.defaultModel. If you want a role to use a specific model, set an override instead of copying the bundled agent file.

{
  "defaultModel": "deepseek-v4-pro",
  "subagents": {
    "defaultModel": "deepseek-v4-flash",
    "agentOverrides": {
      "oracle": {
        "model": "deepseek-v4-pro"
      }
    }
  }
}

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"]
      }
    }
  }
}

Recommended model tiering (optional)

A setup that works well in practice is routing agents by task shape instead of running everything on one model. Four tiers:

  1. Fast workhorse — the cheapest capable model at low thinking, for recon, lookups, and mechanical edits. Example: openai-codex/gpt-5.6-luna:low on scout.
  2. Standard well-scoped — a mid-tier model at medium thinking, for most delegations: routine multi-file edits, focused reviews, straightforward implementation. Example: openai-codex/gpt-5.6-terra:medium on worker, reviewer, and a lightweight delegate agent.
  3. Deep but bounded — a top reasoning model at high thinking, only for hard tasks that arrive with explicit goals and completion criteria. These models tend to loop on vague goals, so keep them off open-ended work. Example: openai-codex/gpt-5.6-sol:high on planner and oracle-style agents.
  4. Taste and intent — a model that reads human intent well and makes judgment calls without looping, for ambiguous work: UX and design decisions, product tradeoffs, planning from vague requirements, writing quality. Example: anthropic/claude-fable-5 at low for lighter passes and medium for harder ones.

The routing rule: use the capability tiers (1–3) when the task is well-scoped, and the intent tier (4) when scoping or judging is the task itself.

Give tier-4 agents cross-provider fallbackModels so subscription usage limits degrade gracefully instead of failing the run — fallback triggers on rate-limit and overload errors automatically:

---
name: shaper
description: Open-ended design/UX/product/planning agent for ambiguous tasks
model: anthropic/claude-fable-5
thinking: medium
fallbackModels: openai-codex/gpt-5.5:high
---

One more interaction worth knowing for tier 4: forked context over an Anthropic parent transcript with signed thinking blocks forces the child's thinking off, so intent-tier agents work best with fresh context.

Use ~/.pi/agent/settings.json for a user override or the project config settings file (.pi/settings.json in standard Pi) for a project override. subagents.defaultModel applies to builtin, package, user, and project agents that do not set model in frontmatter. Per-run model overrides and agentOverrides.<name>.model still win, and explicit agent frontmatter still wins over the global default. The same agentOverrides block can change tools, skills, inherited context, prompt text, or disable a builtin. Matching user and project agents also receive override fields that their frontmatter leaves unset, so a shared project config agent can keep the persona while local settings choose the model.

By default, project settings resolve from the nearest parent directory that contains .pi or .agents, preserving existing nested-project behavior. In monorepos or git worktrees where an incidental nested .pi directory should not shadow the repository-level config, set this in the repository root .pi/settings.json:

{
  "subagents": {
    "projectRootResolution": "git-root"
  }
}

"git-root" keeps package discovery, project agents, chains, and agentOverrides anchored to the git worktree root when that root also has Pi project config. A nested project can still opt back into nearest-root behavior by setting "projectRootResolution": "nearest" in its own .pi/settings.json.

Set subagents.defaultThinking to give builtin, package, user, and project agents without a thinking value a shared thinking level, independent of the parent session's default. Project settings win over user settings. Explicit frontmatter, agentOverrides.<name>.thinking, and per-run thinking overrides still win; thinking: false remains an explicit opt-out:

{
  "subagents": {
    "defaultThinking": "medium",
    "agentOverrides": {
      "reviewer": { "thinking": "high" }
    }
  }
}

If your provider rejects model IDs with thinking suffixes, set subagents.disableThinking: true in user or project settings. That clears bundled builtin thinking defaults in one place; an explicit higher-precedence agentOverrides.<name>.thinking value can opt a role back in. Existing custom-agent frontmatter remains authoritative.

Set subagents.defaultExtensions to give builtin, package, user, and project agents without an extensions field a shared extension allowlist. Absent preserves Pi's normal ambient extension discovery. Present as an empty array, the default sets extensions: [] for agents that do not explicitly define it, disabling ambient extension loading. Present as a non-empty array, the default supplies that allowlist to agents that do not explicitly define one. Project settings win over user settings. Use agentOverrides.<name>.extensions for per-agent settings; explicit custom-agent frontmatter remains authoritative.

{
  "subagents": {
    "defaultExtensions": [],
    "agentOverrides": {
      "researcher": {
        "extensions": ["./tools/research.ts"]
      }
    }
  }
}

A non-array value, an array containing a non-string entry, or an empty/whitespace-only string raises a settings error naming defaultExtensions and the offending settings file, matching the validation pattern used by defaultModel and defaultThinking.

To inspect what pi-subagents has actually loaded right now, use:

/subagents-models
/subagents-models reviewer

That reports the live runtime mapping, which can differ from settings on disk until you reload Pi.

You do not have to spell a model exactly. Model ids are matched fuzzily against the registry, so provider separator variations (anthropic/claude-sonnet-4, anthropic:claude-sonnet-4, or anthropic.claude-sonnet-4), id separator variations (claude-haiku-4.5 vs claude-haiku-4-5), case differences (Claude-Sonnet-4 vs claude-sonnet-4), and optional trailing date stamps (claude-haiku-4-5-20251001 or claude-haiku-4-5-2025-10-01 vs claude-haiku-4-5) all resolve to the same model. Exact provider/id matches still win, and a qualified provider query never silently switches providers — it only matches within the named provider. Ambiguous bare ids that exist under multiple providers still require a provider prefix or the current session's provider to disambiguate.

Choosing a watchdog model

The subagent watchdog is not the reviewer subagent. subagents.defaultModel and subagents.agentOverrides.reviewer do not configure it. The watchdog is an opt-in adversarial change reviewer, so it should usually use a strong complementary model rather than a cheap/light model.

The watchdog reviews repo edits, not ordinary conversation. It runs at the safe agent_end boundary only when the current agent or child writer changed the final repo state since the start of that turn. Multiple edits in one turn are coalesced into one review of the final changed state, unchanged/reverted diffs are skipped, and generated .pi-subagents/ or tmp/ artifacts do not trigger review. In orchestrated runs, each writing child can review its own edited worktree, and the parent can still review the aggregate repo diff after child changes are applied.

When enabled, the watchdog also keeps a bounded in-memory current-scope artifact from real user prompts and prepends it to review input by default (subagents.watchdog.scope.enabled). Newer prompts supersede and mutate older prompts, so the reviewer can flag work that no longer serves the current scope as scope-drift. Watchdog auto-follow prompts are not recorded as scope.

You can opt into Scopey-style scope monitoring, inspired by Scopey, by setting subagents.watchdog.cadence.everyNTools to run additional non-blocking reviews every N tool results. Cadence warnings are transcript-visible and delivered with Pi's steer mode after the current tool boundary; they are never hidden. The same configured watchdog model is used for all checks, so choose a cheap model for frequent monitoring or a strong model for rarer adversarial review.

When the watchdog displays a blocker at agent_end, the existing subagents.watchdog.autoFollow policy can queue a visible follow-up user message asking the agent to address it. Auto-follow only runs while the watchdog is enabled, respects maxAttempts, and stops on repeated identical blockers using stalemateRepeats.

When the watchdog is enabled, it also checks changed TypeScript and JavaScript files for fresh language-server diagnostics before the model review. It auto-detects typescript-language-server from the project node_modules/.bin or PATH; it never installs tools or scans the whole workspace. LSP errors surface as watchdog blockers, warnings as concerns, and info/hints stay in status details. Slow or missing servers are reported in /subagents-watchdog status without blocking the turn or emitting late mid-turn warnings. Configure the bounds with subagents.watchdog.lsp.enabled, timeoutMs, maxFiles, and maxDiagnostics.

Use /subagents-watchdog recommend-model to ask pi-subagents for the current strong pairing. The current recommendation policy is Opus 4.8 with thinking high or GPT 5.5 with thinking high. If your main session is using one, the watchdog should use the other when that model is authenticated.

/subagents-watchdog recommend-model
/subagents-watchdog session model recommended
/subagents-watchdog model recommended

session model recommended changes only the current Pi session. model recommended saves the recommendation to ~/.pi/agent/settings.json; it does not turn the watchdog on. Enable it separately with /subagents-watchdog on when you want the extra review pass.

You can also set the model explicitly:

/subagents-watchdog model anthropic/claude-opus-4-8:high
/subagents-watchdog model openai-codex/gpt-5.5:high
/subagents-watchdog model inherit
/subagents-watchdog check

For settings files, use subagents.watchdog.main.model and subagents.watchdog.main.thinking for the main watchdog. If main.model is omitted, the main watchdog uses the current session model and thinking level. If main.model is set without a thinking suffix or main.thinking, it runs with thinking off, so prefer :high or "thinking": "high" for the strong-watchdog pairing.

Default strong-reviewer profile:

{
  "subagents": {
    "watchdog": {
      "enabled": true,
      "main": {
        "model": "anthropic/claude-opus-4-8",
        "thinking": "high"
      }
    }
  }
}

Scopey-style scope monitoring profile:

{
  "subagents": {
    "watchdog": {
      "enabled": true,
      "main": {
        "model": "anthropic/claude-haiku-4-5",
        "thinking": "medium"
      },
      "scope": { "enabled": true },
      "cadence": { "everyNTools": 10 },
      "autoFollow": {
        "blockers": true,
        "maxAttempts": 3,
        "stalemateRepeats": 3
      }
    }
  }
}

For child subagent watchdogs, use subagents.watchdog.children.model as the default child watchdog model, or subagents.watchdog.children.overrides.<agent>.model for a specific child role. Child watchdogs are still opt-in and follow the same edit-gated rule: read-only children do not trigger watchdog reviews, while writer children are reviewed at their own agent_end if their worktree changed.

Agents can configure the same values through the tool when you ask them to set up the watchdog:

subagent({ action: "watchdog.recommend-model" })
subagent({ action: "watchdog.configure", model: "recommended", scope: "session" })
subagent({ action: "watchdog.configure", model: "recommended", scope: "project" })

Persistent scopes (user or project) should only be used when you ask for a lasting default. Otherwise the agent should use scope: "session".

To keep subagents inside a budget or compliance profile, enforce a model scope. Put subagents.modelScope in user or project settings (project overrides user):

{
  "subagents": {
    "modelScope": {
      "enforce": true,
      "allow": ["anthropic/*", "openai/gpt-5-*"]
    }
  }
}

allow is a list of glob patterns matched against the resolved provider/id (only * is special, case-insensitive). A resolved model that matches none of the patterns is rejected. Models you pass explicitly — the tool-call model, --model, or a clarify pick — error and abort the run. Models that come from agent frontmatter, subagents.defaultModel, or the inherited parent session model only warn, so existing configurations keep working while you tighten the scope. enforce: true requires a non-empty allow list; otherwise the config is rejected at load time.

Where running subagents show up

Foreground runs stream progress in the conversation while they run. They default to a generous 30-minute wall-clock timeout when neither the call nor the selected agent provides a timeout; explicit timeoutMs/maxRuntimeMs and agent defaults win.

Background runs keep working after control returns to you. Inspect active runs with subagent({ action: "status" }), or a specific run with subagent({ action: "status", id: "..." }). In the TUI, a persistent FleetView below the editor by default shows main plus active children with task, elapsed time, and token totals. Set fleetViewPlacement to "aboveEditor" to move it above the editor. When the focused editor is empty, press or to activate FleetView, then use / or j/k to select a child and Enter to inspect it; printable navigation keys are never intercepted before activation.

/subagents-fleet opens the live fleet inspector with current-session foreground work, recent async children, structured Markdown/tool transcripts, and completed output/session paths. Use / or j/k to select a child, Shift+K/Shift+J to scroll one line, PgUp/PgDn to scroll one page, x/Ctrl+O to toggle tool details, r to refresh, and Esc to close. For a selected live async child, s sends an acknowledged steer message and D stops its top-level async run after confirmation. Ctrl+Alt+F opens the same inspector even while a foreground turn is active and slash input is queued. Without a TUI, /subagents-fleet retains the textual subagent({ action: "status", view: "fleet" }) fallback, and mutations use explicit commands: run /subagents-stop and pick from the selector, or use /subagents-stop <run-id> / subagent({ action: "stop", id: "..." }) when you already know the id. Use /subagents-detach [run-id] only for an active foreground single-subagent run you want to leave running without terminating; the eventual result remains available through status/wait. To inspect one background child in text, use subagent({ action: "status", id: "...", view: "transcript" }); add index for a specific child in a parallel or chain run.

FleetView replaces the legacy above-editor async widget by default. Successful background completions stay quiet so inactive Pi tabs are not marked unread, while failed or paused completions still notify the originating session. Parallel runs show every active child independently. Chains with parallel groups keep their grouped shape in progress and results, so failed or paused agents stay visible next to completed ones. When a child is explicitly allowed to fan out with tools: subagent, its nested runs appear under that parent child in the main status tree instead of being hidden inside the child process.

When Pi runs inside Herdr, pi-subagents automatically reports active async-run counts through Herdr pane metadata. The bridge is enabled only when Herdr supplies HERDR_ENV=1 and HERDR_PANE_ID; outside Herdr it registers no listeners or timers. It restores current-session active runs after /reload or /resume, refreshes metadata while work is active, and clears it on completion or shutdown. To show the reported label in the expanded Agent sidebar, include state_text or $summary in its row layout, for example:

[ui.sidebar.agents]
rows = [
  ["state_icon", "workspace", "tab"],
  ["agent", "state_text"],
]

The bridge uses Herdr's existing herdr:blocked sibling event when an async child needs attention. It also emits herdr:busy while async work remains. Herdr versions that support that sibling event keep the pane's semantic state working; older versions ignore it safely and still display the metadata label while the Pi integration remains the lifecycle authority.

Herdr 0.7.5+ can also open an on-demand inspector for an existing async run:

subagent({ action: "inspector.open", id: "<run-id>", index: 0, focus: true })
subagent({ action: "inspector.status", id: "<run-id>", index: 0 })
subagent({ action: "inspector.close", id: "<run-id>", index: 0 })

The inspector is a raw dashboard pane, not the child process and not a literal attach. It reads lifecycle/status/output/mission artifacts and sends steer or stop through pi-subagents' existing control inbox. Closing it never stops the run. Herdr remains optional, ordinary launches stay headless, and missing/older Herdr versions affect only Herdr-specific inspector and project-pane actions. FleetView opens the selected active async child with H. Use focus only with inspector.open; Herdr 0.7.5 cannot focus an arbitrary existing raw pane id.

For substantial work in another codebase, Herdr 0.7.5+ can open a project-owned Pi pane rooted in that repository:

subagent({ action: "project.open", cwd: "/path/to/repo", message: "Own the auth refresh mission for this project." })
subagent({ action: "project.status", cwd: "/path/to/repo" })
subagent({ action: "project.close", cwd: "/path/to/repo" })

A project pane runs its own Pi session in the target directory, so subagents launched from that pane use that project's config, agents, skills, files, git state, and missions. The parent session keeps coordination authority; existing headless runs are not moved into the pane. Pane bindings live under <projectRoot>/.pi-subagents/project-panes/herdr.json and are only a local pointer to the Herdr pane.

You can also ask naturally:

Show me the current async runs.

Lifecycle artifact v3 adds process-terminal-candidate.json (private runner evidence) and process-terminal.json (the public proof projection). A proof is observed only after the live parent observes the exact detached runner's close event, every recorded child writer has a close record, and any tracked canonical-session lease is free. If the observer is unavailable, the proof is unknown; do not infer process exit from endedAt, result-file existence, PID disappearance, or lease-directory absence. The subagent:process-terminal event and RPC ping.capabilities.processTerminalProof expose this status. Process proof is point-in-time evidence and remains separate from execution success or stopped/non-resumable state.

Async runs also write machine-readable lifecycle artifacts for observability and workflow gates. For a top-level async run, details.asyncDir points at a directory containing status.json, events.jsonl, output-<index>.log, and subagent-log-<runId>.md; the final summary is written to Pi's subagent results directory as <runId>.json. Nested async runs use the same shape under the nested async root and are discoverable through status projections that read the nested-run registry. These files are append/update artifacts only; interactive foreground behavior is unchanged.

Foreground and async runners share bounded child-protocol handling. A child JSONL line above 16 MiB fails with structured protocolError code protocol_output_limit; oversized Pi turn_end and agent_end aggregates are the exception because they duplicate granular events, so runners replace them with bounded lifecycle records while preserving agent_end.willRetry. Stderr retains only its latest 128 KiB, split UTF-8 and final unterminated JSON events remain valid, and agent_end.willRetry defers completion until the child settles. Current Pi builds use agent_settled as the terminal watermark; older builds retain the bounded terminal-message fallback.

The stable v1 status/result fields are lifecycleArtifactVersion, runId/id, sessionId, mode, state, startedAt, lastUpdate, endedAt, durationMs, cwd, asyncDir, sessionFile, outputFile, workflowGraph, steps, results, totalTokens, totalCost, model/attemptedModels/modelAttempts, toolCount, turnCount, optional launchResolvedExtensions, optional runtimeAcknowledgedExtensions, and nested children when a child is allowed to launch subagents. launchResolvedExtensions is parent-resolved launch intent only: it reports opaque extension identifiers and whether ambient extensions were disabled, without exposing raw extension paths or claiming the child runtime acknowledged that those extensions loaded. Cooperating child extensions can acknowledge child-runtime registration by emitting subagent:acknowledge-extension on the child process pi.events bus with payload { id: string }. Acknowledgement ids are self-declared opaque strings, must be non-empty, at most 128 characters, contain only A-Z, a-z, 0-9, ., _, :, @, +, or -, and must not contain /, \\, or ... The reported runtimeAcknowledgedExtensions projection is { version: 1, source: "child-runtime", ids, omitted }, deduplicates ids, keeps at most 32 ids, and counts additional valid unique ids in omitted. It is best-effort observability only: absence means no cooperating extension acknowledged, and presence means only that the extension registered in the child runtime, not that its tools, health checks, or features succeeded. Late acknowledgements after terminal serialization are ignored. events.jsonl records lifecycle transitions such as subagent.run.started, subagent.step.started, subagent.step.completed/failed/paused/stopped, control attention events, nested interrupt failures, and subagent.run.completed/stopped; run boundary events include the lifecycle artifact version. Consumers should read these JSON files instead of scraping terminal output; unknown fields and event types should be ignored for forward compatibility.

Other Pi extensions can use the versioned in-process event-bus RPC instead of scraping slash output or calling internal modules. Listen for subagents:rpc:v1:ready, send requests on subagents:rpc:v1:request, and read replies from subagents:rpc:v1:reply:<requestId>. The ping capability metadata also advertises events.asyncComplete for exact process-local completion correlation after RPC spawn. Delegation v1/v2 progress updates carry runId as soon as foreground execution allocates it, so a caller can retain the package-owned revival target even if its own tool turn is interrupted before the terminal response. Foreground details.results[] rows also include a numeric index that is unique within the run and stable across partial progress snapshots and the final result; use (runId, index) instead of row position to correlate single, counted parallel, and chain children.

const requestId = crypto.randomUUID();
pi.events.on(`subagents:rpc:v1:reply:${requestId}`, (reply) => {
  // { version: 1, requestId, success: true, data } or
  // { version: 1, requestId, success: false, error: { code, message } }
});
pi.events.emit("subagents:rpc:v1:request", {
  version: 1,
  requestId,
  method: "spawn",
  params: { agent: "reviewer", task: "Review the current diff", context: "fresh" }
});

The v1 methods are ping, status, spawn, steer, interrupt, stop, and resume. status, steer, interrupt, and resume reuse the normal package-owned actions. ping.capabilities.launchResolvedExtensions advertises the optional launch-resolved extension projection in status details. ping.capabilities.runtimeAcknowledgedExtensions advertises the optional child-runtime acknowledgement projection and event name. When ping.capabilities.fleetStatus is { version: 1 }, successful status replies additionally include data.fleet: { version: 1, entries, totalActive, omitted }. Entries are bounded, current-session public display records with an opaque reconciliation key, resolved agent, optional role, model, effort, caller-facing goal, safe startedAt, and { input, output, total } tokens. totalActive and omitted preserve overflow information beyond the bounded entry window. The DTO intentionally never exposes run, async, or tool IDs; clients must ignore unknown fields and fall back to status text when the capability is absent. steer requires an async run id (plus optional child index) and a non-empty message; its reply preserves the normal acknowledged-delivery result. RPC steering disables the direct tool's pause-and-revive recovery so an extension keeps authority over the exact child it spawned; ping.capabilities.nonRecoveringSteer advertises this guarantee. resume requires a run target and non-empty message; it delegates to the existing revival path, which validates current-session ownership, persisted session/recovery metadata, stopped/live state, capability ceilings, and the exclusive session lease before returning the new async run details. Callers may request a file-only output path for the revived result without overriding its model, tools, or budgets. ping.capabilities.resume advertises this seam. spawn is async-only: omit async or set async: true, omit clarify or set clarify: false, and do not pass management action values. It goes through the same executor as the subagent tool, so agent discovery, validation, session attribution, configured spawn caps, child-safety depth, artifacts, and async status all behave the same. stop targets current-session top-level async runs through the stop control channel and records a stopped lifecycle instead of reporting a timeout.

pi.events is in-process only. It does not reach separate Pi processes or child subagents; use the file lifecycle artifacts or pi-intercom for cross-process coordination.

If something feels misconfigured, run:

/subagents-doctor

or ask:

Check whether subagents and intercom are set up correctly.

Recommended orchestration pattern (scaffolding)

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.

Packaged planner, worker, oracle, and advisor default to forked context when a launch omits context; pass context: "fresh" when you intentionally want a fresh child run.

Child-safety boundaries are enforced at runtime. Spawned child sessions do not receive the bundled pi-subagents skill, and forked child context filtering removes parent-only subagent artifacts (including old hidden orchestration-instruction messages, slash/status/control messages, and prior parent subagent tool-call/tool-result history) while preserving ordinary prose and unrelated tool calls/results. By default, children do not register the subagent tool and receive boundary instructions that they are not the parent orchestrator and must not propose or run subagents. The explicit exception is an agent whose resolved builtin tools includes subagent; that child gets a child-safe subagent tool for the fanout work the parent assigned, still bounded by maxSubagentDepth.

Optional shortcuts

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
/parallel-review Launch fresh-context reviewers with distinct angles, then synthesize what to fix.
/review-loop Run parent-controlled worker, reviewer, and fix-worker cycles until clean or capped.
/parallel-research Combine researcher and scout for external evidence, local code context, and practical tradeoffs.
/parallel-context-build Run context-builder agents in parallel to produce planning handoff context and meta-prompts.
/parallel-handoff-plan Combine external research and context-builder passes into an implementation handoff plan and meta-prompt.
/gather-context-and-clarify Scout/research first, then ask the user the clarification questions that matter.
/parallel-cleanup Run review-only cleanup passes after implementation.

Add autofix to /parallel-review or /parallel-cleanup to apply only the synthesized fixes worth doing now after reviewers return.

Native supervisor coordination

Child agents can talk back to the parent Pi session without installing pi-intercom. pi-subagents now provides the child-facing contact_supervisor tool and the parent-facing subagent_supervisor({ action: "reply" }) path natively. If no external pi-intercom tool owns the intercom name, the native channel also exposes intercom as a compatibility fallback.

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. Use reason: "need_decision" for blocking decisions or clarification, reason: "interview_request" for structured input, and reason: "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.

The parent replies with subagent_supervisor({ action: "reply", replyTo, message }) or checks pending requests with subagent_supervisor({ action: "pending" }). Supervisor messages are scoped to the exact Pi session id that spawned the child. A second Pi session in the same repository does not receive those requests.

Child-side routine completion handoffs are still not expected. 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.

At this point, you know enough to use the plugin. The rest of this README is reference material for exact command syntax, custom agents, scripted workflows, and configuration.

Native child tool permissions

Native permissions are opt-in and apply only to Pi child runtimes. With no rules configured, every tool call passes through unchanged. Configure explicit non-bash rules globally in ~/.pi/agent/extensions/subagent/config.json:

{
  "permissions": {
    "rules": {
      "read": "allow",
      "write": "ask",
      "edit": "deny"
    }
  }
}

Custom agents can override matching global rules with a permission: or permissions: frontmatter block:

---
name: worker
permission:
  write: allow
  edit: ask
---

Rules support allow, ask, and deny. Agent rules override matching global rules; omitted and unknown tools default to allow. Explicit allow removes an inherited restriction. The gate is not registered when the resolved policy has no ask or deny rules.

An explicit ask pauses that exact tool call and sends a bounded, redacted preview to a one-call permission arbiter owned by the built-in child watchdog. The arbiter uses the configured child-watchdog model and returns only approve or deny; it does not notify the parent agent. Enable and configure subagents.watchdog.children before using ask rules. A disabled watchdog, missing model/auth, timeout, malformed response, or runtime error denies the call with a clear error.

Asked requests and decisions are written to bounded audit JSONL, including decisionSource: "watchdog" and bounded failure reasons. Ordinary direction and clarification through contact_supervisor or the optional pi-intercom extension remain separate and are never permission-gated.

bash is always passed through by pi-subagents. Bash rules are rejected rather than parsed, gated, denied, or audited. Install and configure pi-guard when command-level bash policy is needed.

External CLI profiles are opaque processes, so native permissions cannot intercept their tools. A launch with effective ask or deny rules is rejected for an external CLI agent instead of claiming enforcement.

Direct commands

Use /run <agent> [task] [--bg] [--fork] for one child. Multi-agent orchestration is expressed through workflowScript in the subagent tool; the legacy /chain, /parallel, and /run-chain commands are not registered.

Profiles and provider model catalogs

Profiles are stored under:

~/.pi/agent/profiles/pi-subagents/

Provider model catalogs are cached under:

~/.pi/agent/profiles/pi-subagents/providers/

Use the profile workflow like this:

/subagents-refresh-provider-models openai-codex
/subagents-generate-profiles openai-codex
/subagents-load-profile openai-codex.quota

/subagents-refresh-provider-models writes a serialized provider model catalog with observed registry data, simple role-oriented classification, and live probe results from tiny one-shot pi -p --model ... --no-tools checks. The cache refreshes when missing or stale; use --force to ignore freshness and probe again immediately.

/subagents-generate-profiles uses the provider catalog to produce quota and quality profiles. /subagents-check-profile re-checks each assigned model in a saved profile against the current registry and a live probe so you can detect model removals, auth problems, or stale assignments.

WorkflowScript replacements

Use stable keys and ordinary JavaScript for sequence and parallelism:

subagent({ workflowScript: `
  const scan = await runs.run("scan", { agent: "scout", task: "Scan the codebase" });
  const reviews = await runs.all([
    { key: "correctness", agent: "reviewer", task: "Review correctness: " + scan.output },
    { key: "tests", agent: "reviewer", task: "Review tests: " + scan.output }
  ]);
  return reviews.map(result => result.output);
` });

Clarify and launch UI

Tool calls start background work by default. Set async: false when the current turn needs a foreground result, or clarify: true on single, parallel, or chain runs when you want to preview and edit the workflow before it runs; clarify stays foreground.

Common clarify keys:

  • Enter runs in the foreground, or in the background if background is toggled on
  • Esc cancels or backs out
  • ↑↓ or j/k moves between steps or tasks
  • e edits the task/template
  • m selects a model
  • t selects thinking level
  • s selects skills
  • b toggles background execution
  • w edits output/write behavior where supported
  • r edits reads where supported
  • p toggles 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.

Agents

Agents 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/
Installed package package.json pi-subagents.agents or pi.subagents.agents
User ~/.pi/agent/agents/**/*.md
Project Project config agents/**/*.md (.pi/agents/**/*.md in standard Pi)

Project discovery also reads legacy .agents/**/*.md files. Nested subdirectories are discovered recursively. .chain.md files do not define agents. Installed Pi packages can expose agent directories from either {"pi-subagents":{"agents":["./agents"]}} or {"pi":{"subagents":{"agents":["./agents"]}}} in their package manifest. Package agents load above builtins and below user/project agents. If both .agents/ and the project config agents directory define the same parsed runtime agent name, the project config directory 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.defaultModel or subagents.agentOverrides.<name>.model. oracle is an advisory reviewer that critiques direction and proposes an execution prompt without editing files; advisor is the same bundled role under the Claude Code-compatible name. worker is the implementation agent for normal tasks and approved oracle handoffs.

The researcher builtin uses web_search, fetch_content, and get_search_content; those require pi-web-access:

pi install npm:pi-web-access

Builtin overrides

You can override selected builtin fields without copying the whole agent. Overrides live in settings:

  • User: ~/.pi/agent/settings.json
  • Project: project config settings file (.pi/settings.json in standard Pi)

Example:

{
  "subagents": {
    "agentOverrides": {
      "reviewer": {
        "description": "Independent review tier",
        "inheritProjectContext": false
      }
    }
  }
}

Supported override fields are description, model, fallbackModels, thinking, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, acceptanceRole, disabled, skills, tools, and systemPrompt. description replaces the discovered description for builtin and custom agents, which lets list output show deployment-specific routing or model metadata. Use defaultContext: false or acceptanceRole: false to clear an inherited override. Project overrides beat user overrides.

Set subagents.defaultModel to give all subagents without an explicit model their own default model, separate from the parent session model. Per-agent model overrides and agent frontmatter still win.

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. You can also toggle a single agent without editing settings by hand: subagent({ action: "disable", agent: "reviewer" }) writes that override, and subagent({ action: "enable", agent: "reviewer" }) removes it.

Set subagents.disableThinking: true to clear bundled builtin thinking defaults globally for providers that do not support :low, :medium, :high, or similar model suffixes. A higher-precedence per-agent thinking override can opt one builtin back in.

Prompt assembly

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. delegate also uses append mode because its job is orchestration inside the parent workflow.

Agent frontmatter

A typical agent looks like this:

---
name: scout
# Optional: registers this as code-analysis.scout while preserving name: scout
package: code-analysis
description: Fast codebase recon
aliases: explorer, code-scout
tools: read, grep, find, ls, bash, mcp:chrome-devtools
extensions:
subagentOnlyExtensions: ./tools/child-only-search.ts
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, review-checklist
skillPath: ./skills, ../shared-skills
output: context.md
defaultReads: context.md
defaultProgress: true
async: true
timeoutMs: 900000
turnBudget: {"maxTurns":20,"graceTurns":2}
acceptance: {"level":"none","reason":"lightweight lookup"}
acceptanceRole: read-only
completionGuard: false
interactive: true
maxSubagentDepth: 1
---

Your system prompt goes here.

Simple-scalar list fields accept either the existing comma-separated form or a newline block list with one - item per line. This applies to tools, defaultReads, skill/skills, skillPath, fallbackModels, extensions, and subagentOnlyExtensions; for example:

tools:
  - read
  - mcp:github/search_repositories
fallbackModels:
  - openai/gpt-5-mini
  - anthropic/claude-sonnet-4

Important fields:

Field Notes
package Optional package identifier. A file with name: scout and package: code-analysis registers as code-analysis.scout; serialization keeps name and package separate.
aliases Optional comma-separated or block-list names that resolve to this agent for selection and explicit agent and task inputs. Runtime status, persistence, and config still use the canonical name; exact canonical names take precedence over aliases, and alias collisions between distinct canonical agents fail as ambiguous.
tools Strict child tool allowlist. Named extension tools must also have their provider loaded. mcp: entries select direct MCP tools when pi-mcp-adapter is installed.
extensions Omitted means normal extensions; empty means no extensions; list values allowlist specific extensions.
subagentOnlyExtensions Extension paths loaded only in spawned child sessions for this agent. Tools registered there are unavailable to the main agent unless also installed through normal Pi extension configuration.
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 or fork launch context default for this agent.
skills Selects specific skills for the child, regardless of inheritSkills.
skillPath Invocation-private skill files or discovery directories. Relative paths resolve from the agent definition file. Local matches take precedence, while unresolved or unreadable matches fall back to normal skill discovery. This field discovers candidates only; skills still selects what the child receives.
output Default single-agent output file.
defaultReads Files to read before running the agent.
defaultProgress Maintain progress.md.
async Default a single-agent launch to background (true) or foreground (false) when the call omits async. Explicit call values and forceTopLevelAsync win.
timeoutMs Positive integer default runtime deadline in milliseconds for single-agent launches. Foreground launches use 30 minutes when neither the call nor agent provides a timeout; explicit timeoutMs/maxRuntimeMs and agent defaults win.
turnBudget JSON object default such as {"maxTurns":20,"graceTurns":2} for single-agent launches. An explicit call value wins, followed by this agent default, then global turnBudget config.
acceptance Acceptance default for single-agent launches. Use a scalar level such as checked or an inline/block YAML map such as { level: "none", reason: "lightweight lookup" }. Explicit call values win; chain and parallel acceptance remains task/step configuration.
acceptanceRole Optional read-only or writer role for automatic acceptance inference. Explicit task mutation or no-edit intent wins; otherwise the declared role replaces agent-name guessing. This does not grant or revoke tools.
completionGuard Set false only for non-implementation agents that may mention implementation words while using mutation-capable tools such as bash.
interactive Parsed for compatibility but not enforced in v1.
maxSubagentDepth Tightens nested delegation for this agent's children.
memory Opt-in role-specific persistent memory. memory: { scope: "project" | "user", path: "<name>" } injects the first lines of a MEMORY.md from a dedicated agent-memory/ directory into the child system prompt. Agents with write tools (edit/write/bash) get a read-write block; read-only agents get a read-only fallback. Project scope resolves under <project>/.pi/agent-memory/, user scope under ~/.pi/agent/agent-memory/. Paths are validated against traversal and symlink escape.

Agent-local skillPath candidates never enter Pi's parent/global skills catalog. Pair inheritSkills: false with explicit skills and skillPath when a child should receive only its selected private skills.

Per-agent persistent memory

A recurring custom agent can opt into a durable, role-specific memory scope with the memory frontmatter field. This is independent of Pi's own parent/session/project memory system and writes nothing to it; memory lives under a dedicated agent-memory/ namespace so the two never collide.

memory:
  scope: project
  path: security-reviewer

On each run, the first 200 lines of MEMORY.md in the resolved memory directory are injected into the child system prompt so the agent can recall accumulated role notes such as threat-model entries, release gotchas, or verified commands. Agents that have write tools (edit, write, or bash, or no tools allowlist at all) are told they may append concise dated entries to the file. Agents without write tools receive a read-only memory block and are not instructed to edit it, so a read-only reviewer can still recall prior notes without being granted write capability. The memory directory is never created eagerly; the agent's own write tool creates it (and MEMORY.md) on the first persist. Memory paths are validated against ./.. traversal and symlink escape, and an unsafe or unresolvable scope is silently skipped rather than breaking the run.

Project-scoped memory resolves under <project>/.pi/agent-memory/<path> and travels with the repo. User-scoped memory resolves under ~/.pi/agent/agent-memory/<path> and is shared across projects for that agent.

Tool and extension selection

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; an empty tools: field emits --no-tools. An allowlisted name does not load the extension that registers it: load that provider through normal Pi extension discovery, extensions, subagentOnlyExtensions, or a path-like tools entry. mcp: entries are split out and forwarded as direct MCP selections without granting normal builtins unless those builtins are also listed. Path-like tools entries, such as extension paths or .ts/.js files, are treated as tool-extension paths rather than tool names. Internal runtime tools such as structured_output are added to an explicit allowlist only when their contract is active. Agents that declare only known read-only builtin tools skip the implementation completion guard, but bash, unknown tools, and MCP tools stay mutation-capable. Use completionGuard: false for bash-enabled validators or advisors that should never be judged as implementation agents.

Examples:

  • tools omitted and extensions omitted: normal builtins and normal extensions.
  • tools: mcp:chrome-devtools: only the resolved direct Chrome DevTools MCP tools.
  • tools: read, bash, mcp:chrome-devtools: only read and bash as builtins, plus direct Chrome DevTools MCP tools.
  • tools: subagent, read: a child-safe subagent tool is available inside that child so it can run explicitly assigned nested fanout.
  • tools: read, fixture_search plus subagentOnlyExtensions: ./tools/fixture-search.ts: the provider loads only in this agent's child process, and the registered fixture_search name survives the strict allowlist.

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. An mcp: entry named subagent does not authorize nested fanout; only the builtin subagent tool name does. If a resolved direct MCP name is missing from the child registry, pi-subagents keeps the launch failed under the strict allowlist and identifies the condition as a host/pi-mcp-adapter registration problem; verify that the adapter registers the selected tools before child startup.

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.ts

When extensions is present, normal discovered extensions are disabled; the listed extensions, path-like tools entries, required pi-subagents runtime extensions, and subagentOnlyExtensions still load.

Use subagentOnlyExtensions when a custom extension tool should exist only inside child sessions. It is scoped by agent config: every run of that agent receives those extension paths, while other agents do not unless they declare the same field. The current model does not have a separate named-subagent audience inside one agent definition.

To apply the same extensions allowlist to every agent that does not declare its own, set subagents.defaultExtensions in user or project settings. Omit it to preserve ambient extension discovery or set it to [] to disable ambient extensions by default; project settings win over user settings. Agents that explicitly define extensions keep their own value, including an empty extensions: field.

Before the first model turn, the child runtime compares every explicit tool name with Pi's final filtered registry. A missing provider now fails the run with the unavailable names and concrete subagentOnlyExtensions/extensions guidance instead of letting a direct or chained child silently continue without its requested tools.

Skills

Skills are SKILL.md files made available to an agent. The prompt includes skill metadata and the file location; the agent reads the full skill file only when the task matches.

Discovery uses project-first precedence:

  1. Project config skills/{name}/SKILL.md (.pi/skills/{name}/SKILL.md in standard Pi)
  2. Project packages and project settings packages via package.json -> pi.skills
  3. Current task cwd package via package.json -> pi.skills
  4. Project config settings.json -> skills
  5. ~/.pi/agent/skills/{name}/SKILL.md
  6. User packages and user settings packages via package.json -> pi.skills
  7. ~/.pi/agent/settings.json -> skills

Use agent defaults, override them at runtime, or disable them:

{ agent: "scout", task: "..." }
{ agent: "scout", task: "...", skill: "tmux, safe-bash" }
{ agent: "scout", 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.

Available skills use this shape:

The following configured skills are available to this subagent.
Use the read tool to load a skill's file when the task matches its description.
When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.

<available_skills>
  <skill>
    <name>safe-bash</name>
    <description>Run shell commands safely.</description>
    <location>/absolute/path/to/safe-bash/SKILL.md</location>
  </skill>
</available_skills>

If an agent has an explicit tools allowlist and resolved skills, read is added for that child run so the listed skill files can be loaded on demand.

Missing skills do not fail execution. The result summary shows a warning.

Bundled skill

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, review-loop, parallel research, parallel context-build, parallel handoff-plan, gather-context-and-clarify, 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 researchers
  • Safety boundaries: child agents must not run subagents unless their resolved builtin tools explicitly include subagent, must not invent intercom targets, and must escalate unapproved decisions
  • Intercom conventions: when to ask vs send, and how parent-side supervisor/result delivery works through the native channel
  • Control and diagnostics: attention signals, soft interrupts, status, and the doctor action

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.

Extension delegation API

Pi extensions can request configured foreground agents through the public event contract exported by pi-subagents/delegation.

Launch contract preflight

Use pi-subagents/preflight when an extension needs to inspect the resolved child launch contract before deciding whether to run anything:

import { resolveSubagentLaunchContract } from "pi-subagents/preflight";

const result = await resolveSubagentLaunchContract({
  agent: "reviewer",
  task: "Review the current diff.",
  context: "fresh",
  cwd: ctx.cwd,
  sessionRoot: "/tmp/my-extension-preflight-session-root",
  availableModels: ctx.modelRegistry.getAvailable(),
});

if (!result.ok) {
  // missing_agent, ambiguous_agent, missing_skill, denied_required_tool,
  // invalid_artifact_dir, invalid_cwd, or unsupported_mode
  throw new Error(result.message);
}

console.log(result.contract.digest, result.contract.tools.effectiveAllowlist);

Preflight covers ordinary single-agent launch resolution under public contract version 2: selected agent identity and shadowed candidates, a versioned parsed-definition digest (including system prompt and launch-affecting model, tool, skill, extension, output, and memory fields), fresh/fork context, effective model and thinking, skill and tool resolution, direct MCP selections, runtime/configured extensions, artifact/session paths, async lifecycle/status/result/event/process-terminal paths, package/lifecycle versions, capability-ceiling audit data, and stable digests. launchContractDigest is the canonical digest of the caller task, effective system prompt (including the resolved turnBudget prompt augmentation when supplied), model candidates, effective tools/extensions/MCP (including inherited capability ceilings), output binding, and structured-output schema that ordinary foreground and async execution report in results/status/events and metadata. Runtime acceptance prose and output-task annotations are intentionally excluded because side-effect-free preflight does not resolve those host/runtime augmentations; the contract version and task digest make that boundary explicit. Raw prompts are not exposed in public contract output. It is side-effect-free for launch state: it does not create child sessions, temp prompt files, structured-output runtimes, tool-diagnostic files, or run artifacts. Some host-owned facts, such as exact fork snapshots, nested async roots, and live model registries, can only be proven by the Pi host; those appear as host_required diagnostics instead of silently pretending to be exact.

Delegation v1

The compatibility v1 contract runs one configured foreground agent per request:

import {
  SUBAGENT_DELEGATION_REQUEST_EVENT,
  SUBAGENT_DELEGATION_RESPONSE_EVENT,
  type SubagentDelegationRequest,
  type SubagentDelegationResponse,
} from "pi-subagents/delegation";

const request: SubagentDelegationRequest = {
  version: 1,
  requestId: crypto.randomUUID(),
  agent: "reviewer",
  task: "Review the supplied evidence.",
  context: "fresh",
  cwd: ctx.cwd,
  timeoutMs: 120_000,
  toolBudget: { soft: 10, hard: 16, block: "*" },
};

const unsubscribe = pi.events.on(SUBAGENT_DELEGATION_RESPONSE_EVENT, (payload) => {
  const response = payload as SubagentDelegationResponse;
  if (response.requestId !== request.requestId) return;
  unsubscribe();
  // Inspect response.status and the metadata present for this run.
});
pi.events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, request);

The contract uses the established prompt-template:subagent:* event transport and the same executor as the subagent tool; it does not add another launcher. New integrations must send version: 1. Requests are strict and single-agent only. They can set fresh or fork context, model, cwd, timeout, turn and tool-call budgets, skills, output behavior, acceptance, and artifact capture. Unknown or malformed fields return invalid_request before execution.

Responses distinguish completion, failure, timeout, cancellation, interruption, turn or tool-budget exhaustion, explicit acceptance failure, invalid requests, and unavailable active context. Optional metadata is omitted when unavailable. Request IDs must be unique while active; duplicate active IDs are ignored so the original request keeps ownership of its terminal response. Emit SUBAGENT_DELEGATION_CANCEL_EVENT with the same version and request ID to cancel queued or active work.

Delegation v2

V2 is the owned-leaf contract for workflow supervisors. Independent requests can overlap through the delegated executor without weakening the ordinary model-facing tool's one-foreground-call-per-turn guard.

import {
  SUBAGENT_DELEGATION_REQUEST_EVENT,
  SUBAGENT_DELEGATION_RESPONSE_EVENT,
  type SubagentDelegationV2Request,
  type SubagentDelegationV2Response,
} from "pi-subagents/delegation";

const request: SubagentDelegationV2Request = {
  version: 2,
  requestId: crypto.randomUUID(),
  ownerRunId: workflowRunId,
  nodeId: "review-accuracy",
  agent: "reviewer",
  task: "Review the supplied evidence.",
  context: "fresh",
  cwd: ctx.cwd,
  thinking: "high",
  result: {
    kind: "structured",
    schema: {
      type: "object",
      properties: { verdict: { type: "string" } },
      required: ["verdict"],
      additionalProperties: false,
    },
  },
};

const unsubscribe = pi.events.on(SUBAGENT_DELEGATION_RESPONSE_EVENT, (payload) => {
  const response = payload as SubagentDelegationV2Response;
  if (response.version !== 2 || response.requestId !== request.requestId) return;
  if (response.ownerRunId !== request.ownerRunId || response.nodeId !== request.nodeId) return;
  unsubscribe();
  // Inspect response.status, response.result, response.usage, model, and thinking.
});
pi.events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, request);

ownerRunId plus nodeId is the active logical identity; requestId identifies one attempt. A second active attempt for the same logical node receives duplicate_node without disturbing the original. Started, update, response, and cancellation payloads carry the full tuple. Cancellation affects only an exact tuple, including cancel-before-start races. Each attempt emits at most one terminal response.

Result mode is explicit. Text remains literal even when it looks like JSON. Structured mode returns the separately captured, schema-validated JSON value. Terminal usage reports input, output, cache-read, cache-write, cost, turns, tool calls, and duration alongside the effective model and thinking level when known. Schemas are capped at 64 KiB; tasks and returned text/structured values are capped at 1 MiB, with smaller bounds on identity/configuration strings and a maximum v2 timeoutMs of 2,147,483,647. V2 alone accepts toolBudget: { hard: 0, block: "*" } to block the first tool call and run a zero-tool leaf; delegation v1 and ordinary model-facing/configured budgets keep their existing minimum of one. The foreground bridge retains up to 8,192 exact pending-cancellation and settled-attempt identities per extension context. If either history fills, it fails closed with unavailable_context for later v2 starts rather than evicting identity facts; lifecycle reset clears the bounded history.

Delegation requires an active extension context. Emit requests from a supported event callback or queued application step, not by recursively invoking the subagent tool inside another tool's tool_call hook. The caller selects a configured agent, but agent discovery and effective tools remain package-owned. A request cannot grant arbitrary tools, and tool restrictions are not an operating-system sandbox. The detached RPC remains async-only; this API is foreground-only.

Existing prompt-template payloads and delegation v1 continue over the same event family. V2 remains foreground-only and inherits the configured agent's current tools, skills, context, model policy, and workspace authority; it is not a sandbox or a durable task broker. pi-subagents/delegation is the canonical contract for extension integrations.

Capability ceilings

Parent extensions can enforce an out-of-band, session-scoped capability ceiling without adding a model-visible field to subagent:

import { registerSubagentCapabilityCeiling } from "pi-subagents/capability-ceiling";

const restriction = registerSubagentCapabilityCeiling({
  sessionId: ctx.sessionManager.getSessionId(),
  source: "plan-mode",
  ceiling: {
    allowedAgents: ["plan-scout", "plan-researcher", "plan-reviewer"],
    allowedTools: ["read", "grep", "find", "ls"],
    denyExtensions: true,
  },
});
// restriction.update(...) replaces this provider's policy atomically.
// restriction.dispose() removes only this provider's registration.

Active registrations intersect their allowedTools and allowedAgents sets and OR denyExtensions; an explicit empty list means no caller-facing tools or launchable agents for that field, while an omitted list does not restrict names. allowedAgents entries are canonical agent names and are case-sensitive. Launching a non-allowlisted agent fails before spawn, and { action: "list" } keeps restricted agents visible in a separate non-executable section instead of silently hiding them. The resolved snapshot is propagated monotonically to nested and async children and is retained for recovery. structured_output may remain as a package-owned internal protocol tool when an output schema requires it; it is not a caller capability. A denied lazy-skill read requirement fails before spawn rather than widening the ceiling.

denyExtensions suppresses ambient, configured, and MCP provider extensions while retaining the package runtime needed for child protocol enforcement. This is a same-process policy boundary, not a sandbox against malicious code already running in the parent process. Schedules created while a ceiling is active are rejected until durable schedule persistence is available; unrestricted schedules remain subject to any policy active when they fire. Public status exposes bounded audit counts and sources, never full extension paths.

Background-work provider API

Other Pi extensions can make their current-session jobs visible to subagent_wait through the versioned process-local provider contract:

import { registerBackgroundWorkProvider } from "pi-subagents/background-work";

const dispose = registerBackgroundWorkProvider({
  name: "my-background-extension",
  wakeChannels: ["my-extension:job-finished"],
  listActiveWork: () => jobs
    .filter((job) => job.status === "running")
    .map((job) => ({ id: job.id, sessionId: job.ownerSessionId })),
  reconcile: ({ sessionId, nowMs }) => reconcileJobs(sessionId, nowMs),
});

Each item needs a stable provider-local ID and the exact Pi session ID that owns it. subagent_wait captures those identities rather than a count, so one job finishing while another starts still satisfies first-completion waits without losing the replacement. It filters snapshots to the active session, fails closed if a provider disappears while its work is tracked, and surfaces malformed snapshots or provider errors with provider context. Wake channels only shorten polling; validated snapshots remain authoritative.

Providers share a registry through Symbol.for("pi-subagents.background-work.v1"), allowing independently loaded extension modules to meet in one Pi process. Registration is reload-safe: a new provider with the same name replaces the old callback, and the old disposer cannot remove the replacement. Call the disposer during extension shutdown when possible.

Child processes do not gain provider tools or extensions automatically. Add subagent_wait to the child agent's tools allowlist and load each provider through extensions or subagentOnlyExtensions. The parent's effective waitTool setting is serialized through foreground, async, resume, chain, parallel, and fanout launch paths; PI_SUBAGENT_WAIT_TOOL_ENABLED keeps precedence.

Programmatic tool usage

These are the parameters the LLM passes when it calls the subagent tool. Most users ask naturally or use slash commands instead.

Execution examples

// Single child
{ agent: "scout", task: "Analyze the auth flow", async: true }

// Sequential workflow
{ workflowScript: `
  const scan = await runs.run("scan", { agent: "scout", task: "Analyze auth" });
  return (await runs.run("plan", { agent: "planner", task: "Plan from: " + scan.output })).output;
` }

// Parallel workflow
{ workflowScript: `
  const results = await runs.all([
    { key: "backend", agent: "reviewer", task: "Review backend" },
    { key: "frontend", agent: "reviewer", task: "Review frontend" }
  ]);
  return results.map(result => result.output);
` }

Management actions

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: "scout" }
{ action: "models" }
{ action: "models", agent: "reviewer" }
{ action: "get", agent: "code-analysis.scout" }
{ action: "get", chainName: "review-pipeline" }

{ action: "create", config: {
  name: "Code Scout",
  package: "code-analysis",
  description: "Scans codebases for patterns and issues",
  scope: "user",
  systemPrompt: "You are a code scout...",
  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: "parallel-scout",
  thinking: "high",
  acceptance: { level: "none", reason: "lightweight lookup" },
  acceptanceRole: "read-only",
  output: "context.md",
  reads: "shared-context.md",
  progress: true
}}

{ action: "create", config: {
  name: "review-pipeline",
  description: "Scout then review",
  scope: "project",
  steps: [
    { agent: "scout", task: "Scan {task}", output: "context.md" },
    { agent: "reviewer", task: "Review {previous}", reads: ["context.md"] }
  ]
}}

{ action: "update", agent: "code-analysis.scout", config: { model: "openai/gpt-4o" } }
{ action: "update", agent: "code-analysis.scout", config: { acceptance: "" } } // clear the frontmatter default
{ action: "update", agent: "code-analysis.scout", config: { acceptanceRole: false } } // restore inferred name fallback
{ action: "update", chainName: "review-pipeline", config: { steps: [...] } }
{ action: "delete", agent: "scout" }
{ action: "delete", chainName: "review-pipeline" }

{ action: "eject", agent: "reviewer" }
{ action: "eject", agent: "reviewer", agentScope: "project" }
{ action: "disable", agent: "reviewer" }
{ action: "enable", agent: "reviewer", agentScope: "project" }
{ action: "reset", agent: "reviewer" }

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. config.aliases accepts a comma-separated string, string array, or false to clear aliases; aliases resolve to the canonical agent name for execution and are shown by list/get. 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 "".

eject copies a bundled builtin or package agent verbatim into the user or project agent dir (default user) as an editable custom file that shadows the original, so you can customize a builtin without hunting package files. disable writes a reversible agentOverrides.<name>.disabled: true entry to the user or project settings file (default user); the agent stays on disk but is hidden from runtime discovery and list. enable removes that disabled field while preserving any other override fields on the same entry. reset deletes the scope's custom agent file and/or settings override entry, restoring the bundled default; it refuses if no bundled default exists (use delete for purely custom agents). All four accept agentScope: "user" | "project" and operate in one scope at a time; project overrides still win over user ones, so a project-scope disable survives a user-scope enable until you target the project scope.

Parameter reference

Param Type Default Description
agent string - Agent name or alias for single mode, or target for management actions. Execution records use the canonical agent name.
task string - Task string for single mode.
action string - Agent, mission (mission.create/list/show/update/attach-run/close), Herdr inspector (inspector.open/status/close), status/control, schedule, watchdog, or doctor action.
chainName string - Chain name for management actions.
config object/string - Agent or existing durable chain config for management 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.
outputSchema object - Require schema-valid structured output for a direct single-agent run.
agentContract { version: 1 } - Opt into generic agent contract v1. Omit to keep the current/default contract.
context fresh | fork per-agent default or fresh Explicit fresh or fork overrides every child. When omitted, each agent uses its own defaultContext; fork creates real branched sessions from the parent leaf. Packaged planner, worker, oracle, and advisor default to fork.
missionId string - Attach a single-agent or workflow launch to an existing project mission.
mission object/false - Create-and-attach shortcut: { title, goal?, labels? }; pass false for an intentionally ephemeral launch with no mission record. Explicit mission persistence failures are strict.
handoffPath string - Aggregate handoff manifest required by action: "worktree.discard".
focus boolean true Focus the newly split pane for action: "inspector.open"; not a standalone action.
view fleet | transcript - Optional status view for the active fleet surface or transcript tail inspection.
lines number 80 Maximum transcript lines for action: "status", view: "transcript"; capped at 500.
clarify boolean false Show TUI preview/edit flow. Explicit clarify: true keeps the run foreground for the clarify UI.
agentScope user | project | both both Agent discovery scope. Project wins on collisions.
async boolean default-on Background execution. Scripted workflows always default to background and accept async:false as an explicit foreground escape hatch. clarify:true applies only to single-agent execution; workflowScript does not open clarify UI.
timeoutMs / maxRuntimeMs number 30 min foreground; none async Optional run-level max runtime in milliseconds. Foreground uses 30 minutes only when neither the call nor selected agent provides a timeout.
turnBudget object none Optional assistant-turn budget { maxTurns, graceTurns }. At maxTurns the child is warned to wrap up. After the grace window (default 1), termination occurs at the next assistant boundary; a response that starts tool work records termination-deferred until a later boundary. Partial output is returned on abort.
toolBudget object none Optional child tool-call budget { soft?, hard, block? }. At soft the child is nudged to finalize. After hard, configured tools are blocked; block defaults to read, grep, find, and ls, while "*" blocks every tool call. Final assistant text is never blocked.
usageBudget object none Optional root-only reported-usage budget { tokens?: { soft?, hard }, costUsd?: { soft?, hard } }. Soft limits are status-only. Hard limits prevent later child launches after reported usage is reconciled; already-running children are not stopped and no reservations are made.
cwd string runtime cwd Override working directory.
maxOutput object 200KB, 5000 lines Final output truncation limits.
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.
acceptance string/object/false inferred Configure evidence gates with "auto", "attested", "checked", "verified", or { level: "none", reason: "..." }. Independent review is orthogonal: use review: { required: true, agent?: "reviewer", focus?: "..." }. review-required means evidence passed but review is pending; reviewed is achieved only after a real independent result. Explicit "reviewed" remains schema-recognized solely for actionable preflight recovery. For reviewer/read-only calls, omit acceptance. false disables gates. With agentContract: { version: 1 }, omitted, "auto", and false mean no acceptance request for that run; explicit acceptance is reported separately from execution.

As a conservative orchestration policy, do not set turnBudget, a hard toolBudget, or a tight usageBudget on implementation workers, fix workers, reviewers with edit authority, or other mutation-capable children. A default tool budget blocks read/search tools rather than mutation tools, and reported usage has no reservation model, so neither assistant turns, tool-call counts, nor token/cost totals measure whether a delivery slice is buildable or safe to hand off. Hard caps remain appropriate for explicitly read-only scouts, reviewers, and validators.

Bound writer work with a narrow task and an outer timeoutMs or maxRuntimeMs that leaves enough margin for the slice. An elapsed timeout is not a mutation-safe boundary and may still signal a child during tool work. Before the deadline, use steer or an attention notice to request a checkpoint after the current tool returns, including changed files, build/test state, remaining work, and commit or PR state.

context: "fork" fails fast when the parent session is not persisted, the current leaf is missing, or the branched child session cannot be created. When the inherited transcript contains signed Anthropic thinking / redacted_thinking blocks, pi-subagents strips those provider-private blocks from the forked child session. It forces thinking off only when the child’s effective primary or fallback model resolves through the model registry to the Anthropic provider or anthropic-messages API; unresolved models are treated conservatively. The result reports every affected child, including on failed runs. Use context: "fresh" when an Anthropic child needs thinking. Forking never silently downgrades to fresh. In workflow runs that omit context, each runs.run child follows its own defaultContext, so a fresh-default scout can run fresh beside a fork-default worker. Pass explicit context: "fork" or context: "fresh" when you intentionally want one context for every child.

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 workflowScript, give each child an explicit output path when later script steps need a durable file reference. A child with only read-only tools does not need direct filesystem access for output: it returns the complete artifact in its final response and the runtime persists it. Children with mutation-capable tools retain the direct-write instruction.

Status and control actions:

subagent({ action: "status" })
subagent({ action: "status", view: "fleet" })
subagent({ action: "status", id: "<run-id>" })
subagent({ action: "status", id: "<run-id>", view: "transcript", index: 0, lines: 80 })
subagent({ action: "status", id: "<nested-run-id>" })
subagent({ action: "interrupt", id: "<run-id>" })
subagent({ action: "interrupt", id: "<nested-run-id>" })
subagent({ action: "stop", id: "<run-id>" })
subagent({ action: "resume", id: "<run-id>", message: "follow-up question after it pauses or finishes" })
subagent({ action: "resume", id: "<run-id>", index: 1, message: "follow-up for child 2" })
subagent({ action: "resume", id: "<nested-run-id>", message: "follow-up for a nested child" })
subagent({ action: "steer", id: "<run-id>", message: "guidance for the running child" })
subagent({ action: "steer", id: "<run-id>", index: 1, message: "guidance for child 2" })
subagent({ action: "append-step", id: "<run-id>", step: { agent: "worker", task: "Continue from {previous}" } })
subagent({ action: "approve-checkpoint", id: "<run-id>" })
subagent({ action: "reject-checkpoint", id: "<run-id>" })
subagent({ action: "doctor" })

status resolves exact foreground ids, top-level async ids, and nested run ids before falling back to prefix matching. view: "fleet" is an optional read-only active-run surface with transcript commands; it does not add steering or stop controls. view: "transcript" tails the selected run's live output-<index>.log or persisted session transcript, with lines capped at 500. Nested status shows the root/parent path, nested children, session/artifact paths when known, and nested control commands. Inside child-safe fanout mode, bare status requires an id when no local foreground run is active, so children cannot enumerate unrelated top-level async runs. Bare interrupt still targets only the visible top-level run; interrupting a nested run requires its explicit nested id.

resume revives a paused, completed, or failed async/foreground child by starting a new child from its stored session file; stopped runs remain non-resumable, and it does not interrupt a live top-level async child. Use steer for acknowledged live async guidance. Multi-child async runs and remembered foreground single, parallel, or chain runs can be revived by passing index to choose the child. Nested runs can be resumed by nested id when their live route or persisted nested session metadata is available. 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. Direct revival takes an exclusive cross-process lease on the canonical session file until the new child finishes. A concurrent attempt fails before Pi is spawned and identifies the owning revived run; dead-owner leases are reclaimed only when staleness can be proved.

stop ends a current-session top-level async run. It is deliberately stronger than interrupt: it is not a resumable pause, stopped runs should be restarted as new runs, foreground and nested targets are rejected, direct id calls execute immediately, and /subagents-stop without an id opens a selector with confirmation when a TUI is available. Use / or j/k to move through that selector. In non-TUI contexts the slash command prints exact subagent({ action: "stop", id }) and /subagents-stop <id> commands. Scheduled jobs can appear in the selector, but they are labeled as scheduled cancellations and route through schedule-cancel, not stop.

steer waits up to three seconds for a correlated child-Pi input acceptance and returns a request id with delivered, scheduled, pending, partial, recovered, or failed plus per-child states. Delivery means Pi accepted the user message, not model compliance. A pending indexed child returns scheduled. Only a top-level single run may interrupt after the acknowledgment deadline and recover after a further 15-second pause/revival bound; durable multi-child and nested runs never auto-interrupt. Recovery launches a replacement only after the source is confirmed paused, a valid persisted session exists, and deadline, turn, and tool budgets remain. It preserves the original child contract and remaining limits; otherwise the source stays paused with an explicit failure. Late acceptance is recorded but cannot cancel committed recovery. The persisted steering ledger retains 20 requests and replaces the old steerCount/lastSteerAt fields.

append-step accepts exactly one step object for an existing durable chain for a top-level async chain whose status is still running. The step is persisted in the run directory and becomes eligible only after the chain's already-queued steps finish; completed, failed, rejected, paused, foreground, single, and non-chain runs reject appends.

Durable missions

Missions are durable wrappers around runs. The noun map is:

  • Project/codebase — where work happens.
  • Mission — why delegated work exists and how to recover it later.
  • Run — one actual subagent execution.
  • Receipt — proof or a link for an external outcome, such as a PR, CI check, deployment, or release.

Ordinary task launches create a mission by default, with detailed JSON records under <cwd>/.pi-subagents/missions/ linking goals, run ids, lifecycle status, decisions, artifact paths, and delivery receipts. Automatic persistence failures do not block the run and are reported as details.missionWarning; explicit missionId and mission requests remain strict before launch. Human receipts end with Mission: <id> (<status>), while JSON/structured output text stays unchanged and details.missionId is authoritative. Pass mission: false for an intentionally ephemeral launch that should not leave a durable mission record. Set missions.enabled: false to disable automatic mission creation; explicit mission fields and actions still work.

const created = subagent({
  action: "mission.create",
  mission: { title: "Ship auth refresh", goal: "Implement and validate token refresh" }
})
subagent({ agent: "worker", task: "Implement the approved auth refresh plan", missionId: "<mission-id>" })

// Or create and attach in one launch
subagent({ agent: "worker", task: "Implement the approved plan", mission: { title: "Ship auth refresh" } })

Use mission.list, mission.show, mission.update, mission.attach-run, and mission.close for management. Use mission.update to record decisions, artifacts, labels, summaries, and delivery receipts while work runs; receipts are durable links for pull requests, CI, deployments, or releases, each with kind, status, title, url, and optional description. They record delivery state only; pi-subagents does not merge, poll CI, or deploy. Use mission.close with a terminal status and summary when a mission is done. After compaction or restart, resume from mission.list/mission.show first: mission.show refreshes linked async status where available, then use the linked run ids with normal status, steer, resume, or stop actions. mission.list with missionScope: "global" reads the user-local pointer index under the Pi agent directory; project records remain the source of truth, and missing records are reported as stale rather than hiding other projects.

For cross-project work, keep same-project tasks on ordinary subagents. Use an explicit cwd for small bounded work in another project. For substantial or long-running work in another project, open a project-owned Herdr pane with project.open and give that project Pi session a narrow mission/result contract. The project pane owns its own subagents; do not model it as ordinary child nesting or expect existing headless runs to move into the pane.

Worktree isolation

Scripted workflows can give each writing child a separate managed git worktree by setting worktree: true on each runs.run / runs.all item:

const [api, ui] = await runs.all([
  { key: "api", agent: "worker", task: "Implement the API", worktree: true },
  { key: "ui", agent: "worker", task: "Implement the UI", worktree: true }
]);
return { api: api.artifactPaths, ui: ui.artifactPaths };

Each child uses the existing worktree lifecycle: it branches from clean HEAD, journals ownership before launch, captures a patch and handoff manifest, then removes cleanly captured temporary worktrees and branches. The handoff manifest path remains available in the child's artifactPaths; return or emit it when the orchestrator needs to apply or inspect the patches. runs.ref stays concise and intentionally omits full paths.

A top-level { workflowScript, worktree: true } makes isolation the default for every workflow child. An individual child can override that default with worktree: false. Keep one writer when parallel writes are not intentionally isolated.

Configuration

pi-subagents reads optional JSON config from ~/.pi/agent/extensions/subagent/config.json.

toolDescriptionMode

{ "toolDescriptionMode": "compact" }

Controls the parent-facing subagent tool description registered at startup. full is the default. compact keeps the execution modes, async/subagent_wait guidance, child-safety boundary, management/action split, one-writer review guidance, and artifact/status essentials with less prompt bloat.

custom reads subagent-tool-description.md from the project config directory, then from ~/.pi/agent/subagent-tool-description.md. Missing, empty, unreadable, or oversized custom files fall back to the full description. Custom templates may use {{fullDescription}}, {{compactDescription}}, {{safetyGuidance}}, {{agentDir}}, and {{projectConfigDir}}; the safety guidance is always present so custom prose cannot remove the runtime guardrails. Restart Pi after changing the mode or custom file.

asyncByDefault

{ "asyncByDefault": false }

Ordinary top-level calls use background execution when the request omits async. Set asyncByDefault to false to restore foreground-by-default behavior. Callers can still force foreground with async: false unless forceTopLevelAsync is enabled; clarify: true remains foreground for its UI.

fleetView

{ "fleetView": false }

Controls the persistent, navigable FleetView. The default is true. Set it to false to hide FleetView without disabling status tracking, completion notifications, /subagents-fleet, or lifecycle events.

fleetViewPlacement

{ "fleetViewPlacement": "aboveEditor" }

Places the persistent FleetView either "belowEditor" or "aboveEditor". The default is "belowEditor"; invalid values fall back to "belowEditor".

asyncWidget

{ "asyncWidget": true }

Controls the legacy above-editor widget for background runs. It defaults to false while FleetView is enabled and true when FleetView is disabled. Set it explicitly to show both surfaces or hide the legacy widget entirely.

waitTool

{ "waitTool": { "enabled": false } }

Keeps the subagent_wait tool registered but makes direct calls return immediately instead of blocking on active subagent or provider work. The default is enabled. You can also set "waitTool": false; set PI_SUBAGENT_WAIT_TOOL_ENABLED=false (or 0, off, disabled) to override config for one process. The effective value is passed explicitly to child runtimes. Headless agent_end auto-drain remains a lifecycle safeguard even when direct wait calls are disabled. Invalid config or environment values fail instead of being coerced.

forceTopLevelAsync

{ "forceTopLevelAsync": true }

Forces depth-0 single, parallel, and chain runs into background mode and bypasses clarify UI by forcing clarify: false. Nested calls keep their own inherited settings.

globalConcurrencyLimit

{ "globalConcurrencyLimit": 20 }

Caps simultaneously running children inside existing durable legacy multi-child runs. New orchestration uses workflowScript and runs.all.

maxSubagentSpawnsPerSession

{ "maxSubagentSpawnsPerSession": 100 }

Optionally caps the total number of child subagent launches during one parent session, including completed and failed children, parallel task counts, static chain steps, and bounded dynamic fanout children. Sessions are unlimited by default. Set this value to 0 to disable a configured cap. PI_SUBAGENT_MAX_SPAWNS_PER_SESSION overrides the config for a process and follows the same positive-cap/zero-unlimited semantics.

subagent({ action: "status" }), fleet status, and subagent({ action: "doctor" }) expose used, effective limit, remaining capacity, grants, and the remaining grant allowance. Static chains and parallel calls fail before creating run artifacts or starting partial work when their declared capacity cannot fit. Later retries or unbounded dynamic work are not guaranteed by that preflight.

A user may explicitly call subagent({ action: "grant-spawn-budget", additional: 10 }) from the root interactive parent after all children settle and confirm the native prompt. Grants are additive: they never erase cumulative usage, are rejected for unlimited sessions and child/headless callers, and total granted capacity cannot exceed the original configured cap. Compaction remains part of the same logical parent session and does not reset usage or grants; starting a new parent session does.

scheduledRuns

{ "scheduledRuns": { "enabled": false, "maxPending": 20, "maxLatenessMs": 300000 } }

Configures one-shot scheduled subagent runs, which are enabled by default. subagent({ action: "schedule", agent, task?, schedule: "+10m" | "2030-01-01T09:00:00Z", scheduleName? }) defers a subagent launch until a future time. Absolute ISO timestamps must include a timezone (Z or an offset such as +05:30). The scheduled run launches as a normal tracked async run with fresh context once it fires, and joins the existing async widget, status, subagent_wait, and completion-notification paths. schedule-list, schedule-status, and schedule-cancel manage pending jobs. Schedules are persisted per session and restored after a Pi restart; a job missed by more than maxLatenessMs while Pi is unavailable is marked missed instead of firing late. maxPending caps the number of pending or running scheduled jobs per session (default 20). Set enabled to false to disable scheduling and remove it from the tool surface and prompt. Only schedule explicit delayed runs the user asked for.

parallel

{
  "parallel": {
    "maxTasks": 12,
    "concurrency": 6
  }
}

maxTasks defaults to 8; concurrency defaults to 4. Per-call concurrency takes precedence.

defaultSessionDir

{ "defaultSessionDir": "~/.pi/agent/sessions/subagent/" }

Session directory precedence is: params.sessionDir, then config.defaultSessionDir, then a directory derived from the parent session. Sessions are always enabled.

singleRunOutputBaseDir

{ "singleRunOutputBaseDir": "~/.pi/subagent-outputs" }

Routes relative output paths for single-agent /run calls under this directory. Absolute per-call or agent output paths are still used as-is. When unset, relative single-run outputs go under the run's output artifact directory instead of the project root.

maxSubagentDepth

{ "maxSubagentDepth": 1 }

Controls nested delegation when no inherited PI_SUBAGENT_MAX_DEPTH is already in effect. Per-agent maxSubagentDepth can tighten the limit for that agent’s child runs, but cannot relax an inherited stricter limit. This applies even to children that explicitly declare tools: subagent; at the cap, execution fanout is blocked instead of silently hiding nested work.

PI_SUBAGENT_PI_BINARY

export PI_SUBAGENT_PI_BINARY=/path/to/pi-or-wrapper

Overrides the command used to launch child Pi processes. Package wrappers can set this to their own pi/agent binary so subagents inherit wrapper flags, environment setup, and bundled resources without relying on PATH ordering. Empty or whitespace-only values are ignored.

intercomBridge

{
  "intercomBridge": {
    "mode": "always",
    "instructionFile": "./intercom-bridge.md",
    "resultDelivery": true
  }
}

Controls whether subagents receive runtime intercom coordination instructions and whether intercom and contact_supervisor are auto-added to their tool allowlist when needed.

Fields:

  • mode: default always; use fork-only to inject only for forked runs, or off to disable the bridge.
  • instructionFile: optional Markdown template replacing the default bridge instructions. {orchestratorTarget} is interpolated. Relative paths resolve from ~/.pi/agent/extensions/subagent/.
  • resultDelivery: default true; attempts acknowledged grouped completion delivery through an external subagent:result-intercom listener. Set false when native parent notifications own completion delivery. Supervisor asks/progress remain active, and genuine enabled-transport acknowledgement failures remain visible.

Bridge activation requires a targetable current parent session id, which pi-subagents passes to children automatically. It no longer depends on an external pi-intercom installation or per-agent extension allowlists.

The default injected guidance tells children to use contact_supervisor with reason: "need_decision" when blocked or needing a decision, reason: "progress_update" only for meaningful blocked/progress updates, generic intercom as fallback plumbing, and avoid routine completion handoffs.

worktreeBaseDir

{ "worktreeBaseDir": "/Users/matt/code/.worktrees/pi-subagents" }

Sets the base directory for worktree: true runs. Relative paths resolve from the repository root, ~/... expands to your home directory, and PI_SUBAGENTS_WORKTREE_DIR is used when config is unset. The default remains the system temp directory.

worktreeSetupHook

{
  "worktreeSetupHook": "./scripts/setup-worktree.mjs",
  "worktreeSetupHookTimeoutMs": 45000
}

The hook runs once per created worktree. Paths must be absolute, ~/..., or repo-relative; bare command names are rejected.

stdin is a JSON object with repoRoot, worktreePath, agentCwd, branch, index, runId, and baseCommit. stdout must be one JSON object, for example:

{ "syntheticPaths": [".venv", ".env.local"] }

syntheticPaths must be relative to the worktree root. They are removed before diff capture so helper files do not pollute patches. Tracked files are never excluded; marking a tracked path as synthetic fails setup. Default timeout is 30000 ms.

missions

{
  "missions": {
    "enabled": true,
    "directory": ".pi-subagents/missions",
    "globalIndex": true,
    "retainTerminal": 200
  }
}

Automatic missions are enabled by default for ordinary launches with a task. Use per-launch mission: false for intentionally ephemeral work, or set enabled: false to disable automatic creation globally; explicit mission actions and missionId/mission launch fields still work. directory may be absolute, ~/..., or project-relative. retainTerminal is a positive count (default 200); pruning removes only the oldest completed, failed, or cancelled records and their pointers, never planned, active, waiting, needs-decision, or corrupt records. The user-global index contains pointers only; missing-record pointers self-heal when globally listed. Set globalIndex: false to disable writes or globalIndexDir to redirect it.

authorityPolicy

{
  "authorityPolicy": {
    "discardWorktree": "confirm",
    "destructiveCleanup": "confirm",
    "spawnBudgetGrant": "confirm",
    "scheduleCreate": "auto",
    "stopRun": "auto",
    "steerRun": "auto"
  }
}

Each fixed action resolves to "auto", "confirm", or "forbid". This is intentionally a small action map, not a generic policy language. Confirm-required control actions fail closed without an interactive UI.

artifactDir

{
  "artifactDir": "session"
}

Controls where subagent artifact files (inputs, outputs, transcripts, metadata) are stored. Defaults to "project", which writes to <cwd>/.pi-subagents/artifacts/. Set to "session" to store artifacts under pi's session directory (~/.pi/agent/sessions/<session>/subagent-artifacts/), keeping the working directory clean. Set to "temp" to use the OS temp directory.

This preference also controls the default chain scratch directory. "project" uses <cwd>/.pi-subagents/chain-runs/, while "session" and "temp" use the user-scoped temp chain directory.

The "session" option uses the same directory that cleanupAllArtifactDirs already scans for age-based cleanup, so artifacts are still cleaned up automatically. Temporary chain directories are cleaned up separately after 24 hours.

completionBatch

{
  "completionBatch": {
    "enabled": true,
    "debounceMs": 150,
    "maxWaitMs": 1000,
    "stragglerDebounceMs": 75,
    "stragglerMaxWaitMs": 400,
    "stragglerWindowMs": 2000
  }
}

Controls smart batching of async-completion notifications. When several background subagents finish within a short window, their successful completions are held briefly and delivered as a single quiet grouped completion instead of separate completions. A hard maxWaitMs cap (measured from the first completion in a group) guarantees nothing is held indefinitely, and late-finishing siblings that arrive within stragglerWindowMs of a group emit join a shorter straggler group governed by stragglerDebounceMs and stragglerMaxWaitMs.

Failed and paused completions bypass batching and fire immediately, flushing any held successes first, so failure and needs-attention signals are never delayed. Set enabled to false to restore the original one-notification-per-completion behavior. Changes apply on the next session start.

Files, logs, and observability

Each chain run creates a scratch directory under its resolved chain root. With the default artifactDir: "project", that root is <cwd>/.pi-subagents/chain-runs/. With artifactDir: "session" or "temp", it is user-scoped temp storage:

<tmpdir>/pi-subagents-<scope>/chain-runs/{runId}/

A run directory may contain files such as context.md, plan.md, progress.md, and parallel-{stepIndex}/.../output.md. User-scoped temp chain directories older than 24 hours are cleaned up on extension startup; project-local and explicit persistent roots are not age-scanned.

Debug artifacts live under {sessionDir}/subagent-artifacts/, .pi-subagents/artifacts/ for project-scoped runs, or a user-scoped temp artifact directory. Single-run relative output files are saved under {artifactsDir}/outputs/{runId}/ unless singleRunOutputBaseDir is configured. 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, fallback attempt outcomes, and the resolved acceptance ledger with its parsed child report.

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 belong only to the originating session. The result watcher emits subagent:async-complete, and the extension consumes that event to record completion state. Successful sibling completions are held briefly and delivered as a quiet grouped completion when they finish within a short window (see completionBatch), avoiding unread markers on inactive tabs. Failed and paused completions remain visible and fire immediately.

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, including correlated subagent.steer.requested, scheduled, routed, delivered, failed, and recovered events plus failure/partial/recovery notices. Nested fanout status is stored as compact sidecar event/registry metadata and merged into parent status views and result/intercom payloads; full recursive status snapshots are not embedded in parent result files. output-<n>.log is a live human-readable tail. Fallback information is persisted so background runs are debuggable after completion.

Acceptance Gates

Every run resolves an effective acceptance policy. Callers may omit acceptance for the inferred default, or set it on single runs, top-level parallel task items, chain steps, static parallel tasks, and dynamic fanout templates.

{
  agent: "worker",
  task: "Implement the fix",
  acceptance: {
    level: "verified",
    criteria: ["Patch the bug without widening scope"],
    evidence: ["changed-files", "tests-added", "commands-run", "residual-risks", "no-staged-files"],
    verify: [{ id: "focused", command: "npm test", timeoutMs: 120000 }]
  }
}

Acceptance evidence levels are auto, none, attested, checked, and verified. acceptance: "auto" is the default. Review is a separate gate configured with acceptance.review; async, risky, and dynamic writer contexts infer checked evidence plus review: { agent: "reviewer", required: true }. Read-only tasks infer lightweight attestation, while normal writer tasks infer checked evidence without review. Agent frontmatter or subagents.agentOverrides may set acceptanceRole: "read-only" | "writer" for ambiguous tasks; explicit task mutation or no-edit intent wins over that role, while omitted metadata preserves the existing reviewer/scout/worker name heuristics. The role affects acceptance inference only and does not change tool access. The bare string "none" is rejected; use { level: "none", reason: "..." } instead. acceptance: false is accepted only as a deprecated shorthand for disabling gates.

For reviewer/read-only calls, omit acceptance. The explicit value "reviewed" is not a policy level: it remains schema-recognized only so semantic preflight can explain the mistake without spawning a child. To require review of a writer result, use acceptance: { level: "checked", review: { required: true, agent: "reviewer" } } and orchestrate the reviewer separately.

Acceptance provenance is stored separately from child prose. evidenceStatus preserves evidence progress when the overall status is waiting on or has completed review:

  • claimed: child finished but did not provide structured evidence.
  • attested: child returned a structured acceptance report.
  • checked: runtime structural checks passed, such as required evidence and no staged files.
  • verified: configured runtime verification commands passed. Child-reported command success does not count.
  • review-required: required evidence passed, but no independent reviewer result has been supplied.
  • reviewed: an independent reviewer result is present and has no blockers.
  • rejected: attestation, structural checks, verification, or review failed.

For attested or stricter levels, the child prompt includes a standardized acceptance section and asks for a fenced acceptance-report JSON block. The parser canonicalizes known enum synonyms, snake_case report keys and wrappers, underscore fence tags, unambiguous scalar arrays, string booleans, and criterion-id separators. Unknown or ambiguous keys and enum values fail with field-level diagnostics. Explicit empty changedFiles and testsAddedOrUpdated arrays are recorded as not applicable; missing fields and empty required command or validation evidence still fail.

Acceptance fences are removed from normal output artifacts, while the raw child transcript remains intact and per-child metadata stores the complete acceptance ledger and parsed report. Explicit failed gates fail the run. Inferred gates remain observable without failing the run.

Live progress

Foreground runs show compact live progress for single, chain, and parallel modes: current tool, recent output, token counts, aggregate cost, duration, activity freshness, current-tool duration, and chain graph metadata when available.

Press Pi's configured expand key (Ctrl+O by default) to expand the full streaming view with complete output per step.

Sequential chains show a flow line like done scout → running planner. Chains with parallel steps show per-step cards instead. Chain status uses label and phase metadata when present, while falling back to agent names for older chains.

Session sharing

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: "scout", 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.

Recursion guard

Subagents can call subagent only when their resolved builtin tools explicitly include subagent. That is meant for delegated fanout agents, not ordinary worker/reviewer children. 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. Nested runs appear in the parent status widget and status output as a tree, and status, interrupt, and resume can target a nested run by its id.

Configure the limit with:

  1. PI_SUBAGENT_MAX_DEPTH before starting Pi
  2. config.maxSubagentDepth
  3. maxSubagentDepth in 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=0

PI_SUBAGENT_DEPTH is internal and propagated automatically. Do not set it manually.

Events

Async events:

  • subagent:async-started
  • subagent:async-complete

The subagent:async-started payload includes task, the backwards-compatible first child task truncated to 50 characters, and goal, the workflow-level caller task truncated to 120 characters (falling back to the first child task). Companion UI extensions can combine goal, workflowGraph, and the live lifecycle artifacts under asyncDir without scraping terminal output.

Intercom delivery events:

  • subagent:control-intercom
  • subagent: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. Native supervisor requests are delivered only to the exact parent session that spawned the child.

Prompt-template integration

pi-subagents works standalone through natural language, the subagent tool, slash commands, and the packaged prompt shortcuts listed near the top of this README. It also includes a native prompt-workflow adapter for reusable subagent prompt templates, so you do not need pi-prompt-template-model for the common subagent workflow path.

Create a prompt in .pi/prompts/ or ~/.pi/agent/prompts/:

---
description: Take a screenshot
model: claude-sonnet-4-20250514
subagent: browser-screenshoter
cwd: /tmp/screenshots
---
Use url in the prompt to take screenshot: $@

Then run it through the native adapter:

/prompt-workflow take-screenshot https://example.com

The adapter delegates to the named subagent, applies model, skill, cwd, and fork/fresh context metadata, and supports runtime overrides such as --subagent reviewer, --fork, --fresh, and --bg.

Prompt templates with chain: frontmatter are translated into workflowScript and launched through /prompt-workflow; /chain-prompts is no longer registered.

Runtime files

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/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.
test/unit/ / test/integration/ / test/e2e/ Unit, loader-based integration, and real-session E2E tests.

About

Pi extension for async subagent delegation with truncation, artifacts, and session sharing

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages