@signalridge/pi-subagents

Signalridge's managed subagent runtime with workflow-owned orchestration RPC.

Packages

Package details

extension

Install @signalridge/pi-subagents from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@signalridge/pi-subagents
Package
@signalridge/pi-subagents
Version
1.15.0
Published
Oct 18, 2026
Downloads
808/mo · 217/wk
Author
signalridge
License
MIT
Types
extension
Size
1.3 MB
Dependencies
6 dependencies · 4 peers
Pi manifest JSON
{
  "extensions": [
    "./src/index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

@signalridge/pi-subagents

Signalridge's managed subagent package for Pi, including protocol-v4 spawning, lifecycle isolation, Fleet UI, persistence, and recovery.

A pi extension that brings Claude Code-style autonomous sub-agents to pi. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level. Run them in foreground or background, steer them mid-run, resume completed sessions, and define your own custom agent types.

Features

  • Claude Code look & feel — same tool names, calling conventions, and UI patterns (Agent, get_subagent_result, steer_subagent) — feels native
  • Parallel background agents — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 4) and smart group join (consolidated notifications)
  • Live FleetView UI — one navigable list below the editor with a status mark per row, live tool activity, token counts, and text status labels. Enter opens the selected agent's conversation. Toggle via /agents → Settings → Fleet view
  • FleetView — Claude Code-style navigable list of main + every running subagent rendered below the editor (earliest-launched first). Press ↓ (or ←) at an empty prompt to jump in, ↑/↓ to move the selection, Enter to open the selected agent's live, auto-updating conversation, Esc to return. Finished agents linger briefly before dropping out, and a viewer stays open through completion so you can read the final output. Toggle via /agents → Settings → Fleet view
  • Conversation observer — select a retained agent with a session in /agents to open a live observer. Public fullscreen hosts use the terminal-sized view; regular hosts keep the floating bordered view. Scroll up to pause following, use Home for the first available history and End/Ctrl+End for the latest. Bounded windows preserve older-message navigation; ] pages available message text and [ returns to its first text page. Press Enter to compose a steering message; native rejection retains the draft. Controls stay bound to the selected session and complete managed attempt, never a replacement that reuses its ID.
  • Foreground handoff — Ctrl+B at an empty, focused main prompt hands off one eligible foreground agent without restarting it. In its observer, b arms handoff and y confirms it; x similarly arms stop, y confirms, and Esc cancels. The ID, session, transcript and worktree remain the same; managed, nested, resumed and internal generation tasks are excluded. A full background pool refuses the transfer rather than exceeding its capacity.
  • Custom agent types — define agents in .pi/agents/<name>.md or .agents/agents/<name>.md (project) or globally, with YAML frontmatter: custom system prompts, model selection, thinking levels, tool restrictions
  • Nested subagents — opt-in, default-off delegation: a custom agent that sets allowed_subagents gets its own ownership-scoped Agent, get_subagent_result, and steer_subagent tools, depth-capped from the main session (default 2). It can control only its own children, they are stopped when it finishes, and their transcripts and token spend roll up to it. The allowlist is a privilege boundary — a child runs with its own tools, so pick it as carefully as tools: itself
  • Mid-run chat — inject messages into running or queued agents to redirect their work without restarting. From the main prompt, use @handle message (with @ autocomplete); configure this under /agents → Settings → Agent mentions.
  • Session resume — pick up where an agent left off, preserving full conversation context
  • Graceful turn limits — agents get a "wrap up" warning before hard abort, producing clean partial results instead of cut-off output
  • Case-insensitive agent types — "explore", "Explore", "EXPLORE" all work. Unknown types fall back to general-purpose with a note
  • Fuzzy model selection — specify models by name ("haiku", "sonnet") instead of full IDs, with automatic filtering to only available/configured models
  • Context boundary — raw inherit_context: true is refused because Pi does not expose the parent's post-hook request or atomically pin the child's physical endpoint; pass a reviewed, sanitized summary in the task instead
  • Persistent agent memory — three scopes (project, local, user) with automatic read-only fallback for agents without write tools
  • Git worktree isolation — run agents in isolated repo copies; changes auto-committed to branches on completion, with an explicit prompt guard keeping the base checkout off-limits
  • Skill preloading — inject named skills into agent system prompts, discovered from .pi/skills/, .agents/skills/, and global locations (Pi-standard <name>/SKILL.md directory layout supported)
  • Tool denylist — block specific tools via disallowed_tools frontmatter
  • Styled completion notifications — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show full output. Group completions render each agent individually
  • Event bus — lifecycle events (subagents:created, started, completed, failed, steered, compacted) emitted via pi.events, enabling other extensions to react to sub-agent activity
  • Cross-extension RPC — other Pi extensions can spawn and stop subagents via the pi.events event bus (subagents:rpc:ping, subagents:rpc:spawn, subagents:rpc:stop). Protocol v4 adds managed spawning, a request-level Agent tier, toolset/denylist/thread/worktree hints, owner-scoped stop/quiescence, a published Agent-tier routing policy, and standardized reply envelopes; pi-subagents remains the final policy owner. Pre-schema-v2 managed tombstones are quarantined rather than replayed. Emits subagents:ready on session start
  • Schedule subagents — pass schedule to the Agent tool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results land via the same subagent-notification followUp path as manual background completions; manage via /agents → Scheduled jobs
  • Model tiers — name a (model, thinking) pair once and let the orchestrator pick it by name; the Agent tool exposes tier and never model/thinking, so which model runs stays a config decision. Manage the catalogue in /agents → Model tiers, pick the default in /agents → Settings → Default tier, or set a plain defaultModel when one line beats a catalogue
  • Model scope enforcement — opt-in validation that subagent model choices stay within your pi enabledModels allowlist (sourced from /scoped-models, with both global and project-local pi settings honored). Caller-supplied out-of-scope → hard error to orchestrator; frontmatter-pinned out-of-scope → warning + runs anyway (frontmatter authoritative). Toggle via /agents → Settings → Scope models
  • Resilient agent files — malformed custom .md files are skipped with a path-qualified warning so one bad file cannot prevent startup; enable /agents → Settings → Strict agent files when startup should fail closed instead

Install

pi install npm:@signalridge/pi-subagents

Use from this checkout

From the repository root:

pi -e ./packages/pi-subagents

Verified host baseline: Pi 1.0.4. Offline resource-ownership, child tool-policy, and native Fleet input barriers are additionally exercised on Pi 1.1.0; that focused coverage is not complete host qualification. Wildcard peer dependency ranges and retained capability fallbacks do not establish support for older host versions.

Upstream and local divergence

Derived from tintinweb/pi-subagents; the upstream MIT license is preserved. The FleetView editor-focus fix from commit 7da6bef828d7582ee33386ece9379c170e7742c4 (2026-10-09, #374; reported by @BarrMan) is selectively ported: callable getText/setText identifies non-inheriting custom editors without taking dialog or selector keys. No clone-parent-history behavior is imported; raw inherit_context: true remains refused. This fork also excludes pi-background from child resource loading until asynchronous child continuation has manager-owned lifecycle accounting.

The container-first FleetView and bounded drill-down patterns are independently adapted from tintinweb/pi-subagents at 221df02323c6b6350ea8474579e0ee0f6cc84cea and fd8d81618d80ae83a3faf40cb87dae99cca33264. Collision-only child-label discriminators are inspired by nicobailon/pi-subagents 69673c7fbf3351e54a5b0786a7e9d9fff625a749. The workflow engine and phase inspector remain owned by pi-workflows, not copied into this package. A separate optional, validated UI protocol supplies authoritative run names, statuses and observed phases and opens that owner's inspector. Without that peer, groups retain the observation-only child list. No workflow source imports, inferred DAG state or second queue/model policy are introduced. The managed protocol remains v4.

Resume-budget and shutdown parity fixes selectively adapt upstream work at d7c9ec0, ca93a52 (unreleased after the 0.22 tag), and 83be963 (released in 0.22). They retain this fork's manager-owned sessions, resource budgets, activity accounting, and protocol v4; the integrated upstream engine is not imported.

The opt-in parent clarification broker is locally implemented. It uses the existing main session, not an upstream LLM arbitrator, cloned parent history, private UI automation, or an additional Agent per question. Permission gates remain human-owned.

Lint policy

Formatter checks remain disabled for this package, while legacy noExplicitAny, control-character regex, and empty-interface rules remain off. Signalridge additions follow the package AGENTS rules and the strict --error-on-warnings lint gate.

Quick start

The parent agent spawns sub-agents using the Agent tool:

Agent({
  subagent_type: "Explore",
  prompt: "Find all files that handle authentication",
  description: "Find auth files",
  run_in_background: true,
})

Foreground agents block until complete and return results inline. Background agents return an ID immediately and notify you on completion.

At a TUI prompt, @explore find it starts one background Explore agent. In model mention mode, a hidden, throwaway turn uses only the current mention text to draft the task prompt; it does not receive the parent's conversation, system prompt, other tools, compaction summaries, or context hooks. This prevents raw parent history from reaching a separate provider before request-local privacy hooks can redact it. The resulting Agent spawn is still attributed to the parent; raw inherit_context: true is refused before the child starts (see below). If the hidden turn cannot start the agent, the extension starts it directly unless the parent runtime itself is unavailable. A pending mention is discarded if you switch sessions or branches before it starts; the off-screen provider turn is cancelled if the switch happens during streaming. An already-started agent is not started twice if a later UI or event update fails.

Pi runtime requirement: On hosts where child sessions require the parent's ModelRuntime, Pi currently exposes it to extensions only through the registry's private .runtime property. If that bridge is missing or incompatible, Agent, mentions, and managed spawns refuse before allocating an ID or making a provider request; they do not silently construct a fresh runtime that would lose custom providers, credentials, or virtual routes. Existing managed idempotency keys remain readable. An explicit child model is validated against the parent runtime after final model selection, so a stale ctx.model alone does not block a valid physical child model. The error advises updating to a host that exposes the parent runtime, or disabling dispatch until one does.

Opt-in parent clarification (background only)

By default, contact_supervisor still asks the human. Explicitly opt a fresh, non-isolated top-level background child into ordinary main-agent clarification:

Agent({
  subagent_type: "Explore",
  prompt: "Investigate the schema. Ask only ordinary task clarifications, never approval.",
  description: "Investigate schema",
  run_in_background: true,
  supervisor_questions: "parent",
})

The child awaits an in-process answer promise. Only its explicit bounded question reaches the main model: sanitized request-local context contains a one-use questionId, original agentId, and numbered choice tokens. The main-only answer_subagent_question({ questionId, answer }) accepts free text, an offered option:N token, or an empty answer to decline. Display labels are separate from the original choice data returned to the child; duplicate identical answers never resume it twice. Question text is untrusted data, not new instructions or authorization.

The broker coalesces at most 16 pending questions (one per child), issues at most 64 IDs per session/branch generation, and expires unanswered questions after two minutes. Each question is displayed in hidden request-local context at most once. It requests one normal prepared extension turn only when the root is idle, has no queued messages, has no observed input/preparation in progress, and is not waiting on a dialog; otherwise questions join the next ordinary model context or wake after final agent_settled, never at retry-prone agent_end. No question means no extra model request. It does not edit genuine user text or images, transfer parent history/credentials into the child, poll a model, persist a separate question history, or automatically retry unanswered questions.

Idle wakes are factory-local authenticated control tokens, not a claim on a familiar sentence. Recognition requires input.source === "extension" and an exact namespace/nonce/MAC; an interactive/RPC literal, foreign-namespace extension literal, or invalid MAC remains ordinary input. Admission replaces only the extension-owned input with a distinct frame token. While that admission remains unambiguous, preparation adds an authenticated empty-content native custom message as prompt-local frame evidence, never persistent model guidance. Native message_start identifies the executing frame; the promptless agent_start and old branch messages cannot identify it. Superseding genuine input invalidates wake authorization without forgetting accepted control identity. Request-local context removes only proven control pairs; verbatim user quotations remain unchanged. Historical proof recognition uses a fixed-size secret and constant recognition state, not an accumulating ledger. At most 16 unresolved or ambiguous admissions are retained independently of the flight; an exhausted admission budget refuses further wakes rather than forgetting unresolved frames.

before_agent_start exposes no input source or delivery ID: token text alone cannot prove identity. If genuine INPUT copies an admitted token before its preparation reaches this package, both matching frames become ambiguous. That admission is retained inert within the same cap, neither frame receives a control proof or question context, neither is aborted or stripped by token text, and the flight's unannounced questions receive structured no-answer. The extension does not alter literal text or images. Native SDK image normalization can append a hint and resize an attachment; question suppression still requires the first text's authenticated token boundary and its retained ambiguous admission, never generic hint prose. This evidence cannot authorize a control proof or abort. The package cannot atomically cancel the unidentifiable control request before native persistence/provider dispatch. Ambiguous preparations cannot release a genuine input's readiness; idle wakes remain conservatively fenced until session retirement, but later ordinary inputs can still receive new questions normally. An already-prepared wake retains its positive native proof and cannot donate it to a later literal quotation.

An owned request-local context delivery is the ACK. Each question has one wake attempt: no ACK within 30 seconds clears only that flight and returns structured no-answer for its still-unannounced questions. Async native authentication rejection and earlier input interception cannot occupy the flight indefinitely or retry acknowledged questions. A superseding genuine prompt retains its own preparation readiness; except for the explicit copied-token ambiguity above, its questions join that prompt or expire normally. Executing-frame ownership separately captures the public ctx.signal and its matching native agent_end. A competing preparation's busy-failure agent_settled cannot clear that frame while the core operation still owns a public signal; only a proven ended and released owner permits cleanup. Native preparation can record inert control tokens and empty frame proofs in ordinary session history; question text and answers are not stored as separate broker entries.

Public-hook limits: handlers run in registration/load order. Observed genuine INPUT retires idle readiness synchronously, before later awaits. This package's mention interception releases only its own INPUT-scoped preparation on a native handled result; a thrown handler is not handled (Pi reports it and continues), and another concurrently preparing input is never released by that mention. Pi exposes neither an earlier handler's in-progress input nor a generic input-handled/auth-rejection completion notification. External handling/rejection after observation can therefore retain preparation readiness until session retirement; no timer guesses that a genuine prompt is inactive. Unresolved accepted wake preparations also retain their bounded admission slots until preparation arrives or the activation is replaced.

Pi has no cancellable post-preparation/pre-persistence boundary. Already-prepared competing prompts cannot be atomically serialized or cancelled before native persistence through public hooks. Cancellation can leave an inert native control record; a proven invalidated control frame is never reclassified as ordinary instructions or given question context. Abort is issued only after an authenticated native message boundary proves the currently executing owned frame, never from a global accepted-token guess or to stop a genuine/other run. SDK 1.0.4 coverage includes later-preparation release orders, user/proof message-start and message-end awaits, copied unprepared tokens with and without images, default AUTO resizing of real 4096x4096 PNGs in both release orders while another child's question stays pending, and guard-disabled controls. Native concurrent prompts can still reject with “already processing”; the package preserves observed frame identity but does not guarantee native concurrent-prompt acceptance or atomic pre-persistence cancellation. This is not a private SDK/renderer/history patch.

Answers are not human approval or permission grants. ask_tools, createAskGate, project trust, external permission dialogs, and human rejections are unchanged. A decision outside existing user task authorization must be declined and left to the human. Cancellation (including an aborted main turn), child stop/removal, timeout, navigation, or reload returns structured no-answer/assumption guidance, never approval. Pending IDs capture the public root session-manager identity, session ID, and complete native branch-entry prefix (including non-model metadata) at child channel construction and at every question invocation from the live root context, plus the current child attempt. A terminal child's background resume keeps its channel guard but cannot reuse that older prefix as the origin of a new question. Admission, context reads, and answer dispatch validate both live ancestries, not only generation events or model-text projections. Ordinary appends retain ownership; metadata-distinct divergent ancestry retires it even if an earlier asynchronous committed-tree handler has delayed this extension's notification. Old IDs cannot answer a replacement run.

Foreground parent-mode calls are rejected before spawn: a main model blocked waiting for a child cannot execute another answering tool. Schedules and changing question mode on resume are also unsupported. An existing parent-mode child can resume only in background with supervisor_questions omitted; it retains its mode. Do not call get_subagent_result(wait: true) while an ordinary clarification is pending: that wait blocks the answering model until cancellation or the question timeout. Nested children cannot opt in or call the root answering tool. Managed protocol-v4 requests do not carry this new mode and remain unchanged; this MVP supports direct Agent calls and in-process SpawnOptions.supervisorQuestions: "parent" only when the root manager has issued the channel. No new global configuration is added. The existing Supervisor questions setting must remain enabled; supervisor_questions: "off" withholds contact for one fresh call.

Install the changed package and reload/restart Pi to use this feature; an already running older installed package does not acquire new tools or parameters from checkout edits.

Root-owned operation approvals

When a root approval service is present at child creation, admitted canonical edit, write, and bash tools use @signalridge/pi-approval-core reviewed SDK definitions. Each operation awaits the root human service with its exact final inputs, file diff or command, working directory, and a distinct child:<agent-id> source. Ordinary parent clarification answers never authorize operations. No service means existing behavior is unchanged; malformed or duplicate services refuse before child allocation. A captured service cannot reconnect after retirement or replacement.

Tool admission, denylist, ask_tools, project trust, and isolation still apply first: approval adds no tools, including to internal zero-tool generation. The gate survives in-memory resume, steering, and nested ctx.executeTool calls. Noncanonical command/mutation-shaped tools require exactly one live cooperative adapter matching the effective schema identity. Known command or path-plus-edit/content schema properties are classified before invocation even when optional. Admission is also rechecked after each downstream public tool_call handler against the native mutable arguments and the originally captured execution name/schema; a normalizer cannot add a command or rename the event to bypass it. These post-handler checks never repeat ask_tools human prompts and retain the same shared execution deadline. Conflicting canonical extension tools are refused rather than silently replacing the reviewed binding. While root review is pending, only human waiting time is excluded from the child's default tool timeout; execution retains its remaining budget and native bash timeouts still apply after approval. Cancellation and divergent root/child branch ownership deny execution.

Inherited pi-approvals resources are excluded before factory loading when the root service is captured, including package aliases. Explicit selection is rejected with guidance to integrate approvals in the root session. Children do not load another approval extension or forward their UI to impersonate a human; no UI/model mode is enabled automatically. Known-shape admission does not interpret arbitrary extension semantics or unknown schemas; a timed-out hook can still perform its own side effects outside the executor. This is an in-process trusted extension integration, not an OS sandbox for arbitrary extension code. Managed protocol and journal formats remain v4.

Scheduling

Add a schedule field to register the agent to fire later instead of running now:

Agent({
  subagent_type: "Explore",
  prompt: "Look at recent commits and summarize what changed since last week",
  description: "Weekly commit review",
  schedule: "0 0 9 * * 1",   // 9am every Monday (6-field cron)
})

Schedule formats:

  • Cron — 6-field (second minute hour day-of-month month day-of-week), e.g. "0 0 9 * * 1" for 9am every Monday, "0 */15 * * * *" for every 15 minutes.
  • Interval — "5m", "1h", "30s", "2d". Fires repeatedly at that interval.
  • One-shot relative — "+10m", "+2h", "+1d". Fires once at that future time.
  • One-shot absolute — full ISO timestamp, e.g. "2026-12-25T09:00:00.000Z".

When a schedule fires, the spawn runs in background and its completion notification arrives in the conversation through the same subagent-notification followUp path as a manually-spawned background agent — your parent agent reasons about the result the same way. The fire also draws itself into the FleetView list, so a scheduled run is visible while it works and not only when it reports.

Schedules are session-scoped: they reset on /new and restore on /resume. List and cancel via /agents → Scheduled jobs (creation is the Agent tool's job — there is no parallel manual-create wizard). Storage at <cwd>/.pi/subagent-schedules/<sessionId>.json with PID-based file locking for cross-instance safety.

Disable the feature entirely: /agents → Settings → Scheduling → disabled removes schedule from the Agent tool spec (no LLM-context cost), hides the menu entry, and stops any active scheduler. The schema-level removal takes effect on the next pi session; the runtime kill is immediate. Re-enable from the same menu.

Restrictions:

  • schedule cannot be combined with inherit_context (no parent conversation exists at fire time) or resume (schedules create fresh agents).
  • Scheduled agents always run in the background; explicitly setting run_in_background: false is rejected rather than silently changed.
  • Scheduled fires bypass the maxConcurrent queue so a 5-minute interval cannot be deferred behind long-running manual agents.
  • Headless pi -p doesn't wait for scheduled subagents.

UI

Running agents are drawn in exactly one place: the FleetView list below the editor (see below). Earlier versions also rendered a widget above the editor and repeated the same summary in the footer status; a single surface under the prompt replaces all three, and the widgetMode setting that configured the above-editor widget is gone.

Runs that enter through the scheduler or a cross-extension spawn appear in the list too, alongside Agent-tool runs.

Every string these surfaces take from a child run — its description, activity preview, error, the Agent tool's own result and completion notification, and the conversation overlay's messages, tool results and command output — is text the extension did not author, so it is neutralized before rendering. Fleet, conversation labels and scheduled-job selectors/confirmations/notifications use @signalridge/pi-ui display encoding (controls shown as \\uXXXX). Transcript previews drop terminal escape sequences, name remaining unsafe code points as [U+XXXX], and replace binary content with a placeholder. These are display copies, not changes to stored prompts, paths, or messages. An agent file's display_name and description get the same treatment as they are loaded, since a cloned repository supplies them. A subagent that reads a poisoned file or fetches a hostile page cannot repaint or reorder the parent's terminal.

The token field is annotated with two optional signals inside parens:

  • NN% — context-window utilization (color-coded: <70% dim, 70–85% warning, >=85% error). Omitted when the model has no declared contextWindow, or briefly right after compaction.
  • compactions N — number of times the session has compacted, when greater than 0. Stays dim; the percent's color carries urgency.

FleetView

While subagents are running, a Claude Code-style navigable list renders below the editor:

  Esc interrupt · ← agents · ↓ manage

  ● main · current
  ● general-purpose · Sleep then report 1 · running            11s · 13.1k tokens
  ● general-purpose · Sleep then report 2 · completed          11s · 13.1k tokens
  ✗ general-purpose · Sleep then report 3 · failed              4s · 2.1k tokens
  3 more below

Each row leads with a single-column status mark colored by outcome — ● while a run is live or finished cleanly, ✗ for a failure, ⊘ for one that was stopped or aborted — so the column scans vertically without reading the labels. The mark is a geometric shape rather than an emoji on purpose: an emoji invites font fallback, and a fallback glyph is usually double-width, which would push the rest of the row out of alignment on some terminals and not others.

The list is ordered earliest-launched first, and only shows agents you can actually open (pending/queued agents with no session yet appear once they start). At an empty prompt, press down or left to move focus from the prompt into the list. Up and down move the selection, Enter opens the selected agent's live conversation overlay (it auto-updates as the agent works), and Esc (or up above main) returns to the prompt. Selecting main returns to the normal view. Regular-mode observers use a complete ╭─╮ / │ / ╰─╯ border; public fullscreen hosts use a terminal-sized observer. Press Enter to chat with a running agent — type a message and Enter to send it (Esc or an empty submit returns). For a queued agent that has no session yet, type @handle message in the main prompt; the message is delivered when it starts. Press x and follow the displayed explicit confirmation to stop a running agent; holding one key must not confirm it. A viewer stays open when its agent finishes so you can read the final output, and finished agents linger in the list for a few seconds before dropping out. Typing anything at a non-empty prompt behaves normally — the list only captures navigation keys when the prompt is empty. Disable it entirely via /agents → Settings → Fleet view.

Workflow containers appear above ordinary agents. With the optional pi-workflows UI peer, a row shows the actual workflow name/status, observed current phase, call counts and available token totals; even a running workflow with no child yet can appear. Enter opens the workflow owner's inspector, where pause/resume/stop stay with that owner. Paused and interrupted childless runs remain available for explicit resume.

Without the peer, the fallback is Workflow <runId> · N retained children. That count describes retained manager records, not total workflow calls or authoritative status. Opening it gives a terminal-height-bounded child list with stable-ID selection, Up/Down, PageUp/PageDown, Home/End and Enter to view a conversation; Esc returns from the conversation to the child list, then to FleetView. Sessionless queued children remain visible with an explicit unavailable-session message. Completed owned children remain inspectable while retained, independent of the ordinary four-second linger. Duplicate descriptions receive an ID discriminator only when needed. Detached records are excluded, and controls opened on an old thread attempt cannot steer/stop a replacement attempt.

Steering rejection (including a child finishing while the composer is open) preserves the draft and shows an error; Esc explicitly discards it. Completed conversation headers fall back to the retained lifetime usage and compaction count when live activity has been removed.

Individual agent results use restrained text status labels:

State Example
Running ● running · turns 3 of 30 · tools 3 · 12.4k tokens (8%) / ⠹ searching, reading 3 files...
Completed ● completed · turns 8 · tools 5 · 33.8k tokens (62%) · 12.3s / Done
Wrapped up ● wrapped up · turn limit · turns 50 of 50 · tools 50 · 89.1k tokens (84% · compactions 2) · 45.2s / Wrapped up at the turn limit
Stopped ⊘ stopped · turns 3 · tools 3 · 12.4k tokens (8%) / Stopped before completion
Error ✗ failed · turns 3 · tools 3 · 12.4k tokens (8%) / Error: timeout
Aborted aborted · turns 55 of 50 · tools 55 · 102.3k tokens (95% · compactions 3) / Aborted at the turn limit

Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.

By default, foreground and background agents each stream their full conversation to a per-subagent transcript — a JSON-lines file at <os-tmpdir>/pi-subagents-<uid>/<cwd>/<session>/tasks/<agent-id>.output (owner-only 0700, cleared on reboot). Set output_transcript: false on a custom agent to write no transcript path or file for it, or set outputTranscript: false in subagents.json to make transcripts opt-in for the whole project (frontmatter overrides the project default). This governs only the transcript: it is independent of persist_session (the pi session on disk), and it does not affect isolation: worktree (which commits the agent's work to a git branch) or memory: (durable files) — set those accordingly if the goal is to keep a run off disk entirely. Background agent completion notifications render as styled text:

Find auth files · completed
  turns 3 · tools 3 · 12.4k tokens · 4.1s
  Found 5 files related to authentication...
  transcript: .pi/output/agent-abc123.jsonl

Group completions render each agent as a separate block. The LLM receives structured <task-notification> XML for parsing, while the user sees the themed visual.

Default agent types

Type Tools Model Prompt Mode Description
general-purpose all 7 inherit append (parent twin) Inherits the parent's full system prompt — same rules, CLAUDE.md, project conventions
Explore read, bash, grep, find, ls haiku (falls back to inherit) replace (standalone) Fast codebase exploration (read-only)
Plan read, bash, grep, find, ls inherit replace (standalone) Software architect for implementation planning (read-only)

The general-purpose agent is a parent twin — it receives the parent's entire system prompt plus a sub-agent context bridge, so it follows the same rules the parent does. Explore and Plan use standalone prompts tailored to their read-only roles.

Default agents can be ejected (/agents → select agent → Eject) to export them as .md files for customization, overridden by creating a .md file with the same name (e.g. .pi/agents/general-purpose.md), or disabled per-project with enabled: false frontmatter.

Custom agents

Define custom agent types by creating .md files. The filename becomes the agent type name. Any name is allowed — using a default agent's name overrides it.

Agents are discovered from three locations (higher priority wins):

Priority Location Scope
1 (highest) .pi/agents/<name>.md Project — pi's config dir; authoritative, and where /agents writes
2 .agents/agents/<name>.md Project — the shared cross-tool .agents workspace (same convention as .agents/skills/)
3 $PI_CODING_AGENT_DIR/agents/<name>.md (default ~/.pi/agent/agents/<name>.md) Global — available everywhere

Project-level agents override global ones with the same name, so you can customize a global agent for a specific project. If both project locations define the same name, .pi/agents/ wins — .pi stays the project authority; .agents/agents/ is an additional read location for projects that keep their agent assets in the .agents workspace. The global location follows Pi's PI_CODING_AGENT_DIR setting and can relocate all agent state.

Malformed or unreadable files are skipped with a path-qualified warning by default. If a malformed project file shadows a valid lower-priority definition with the same name, the loader warns which definition survived; set strictAgentFiles: true in subagents.json to fail closed during the first session_start instead. Strict validation and discovery use that session's ctx.cwd, not the process cwd, and a failed validation registers no root tools, manager, or RPC responder. Reloads after startup remain lenient; an accidental edit cannot terminate an active session. Unchanged warning keys are suppressed while their discovery root remains in the bounded 64-root cache; if that root is evicted, a later unchanged reload may warn again. Interactive agent-file edits are compare-and-commit operations: replacements are written to a flushed temporary file, new files honor the process umask, and all mutations use a package-local, non-expiring atomic lock directory with bounded acquisition retries. Orphaned locks are never stolen; the error names the lock path to inspect/remove. Only writers using this protocol coordinate with these operations. Non-cooperating editors are detected when a content comparison observes their change, but portable Node has no universal filesystem CAS, so this is not a claim of an arbitrary-editor lock.

Example: .pi/agents/auditor.md

---
description: Security Code Reviewer
tools: read, grep, find, bash
model: anthropic/claude-opus-4-6
thinking: high
max_turns: 30
---

You are a security auditor. Review code for vulnerabilities including:
- Injection flaws (SQL, command, XSS)
- Authentication and authorization issues
- Sensitive data exposure
- Insecure configurations

Report findings with file paths, line numbers, severity, and remediation advice.

Then spawn it like any built-in type:

Agent({ subagent_type: "auditor", prompt: "Review the auth module", description: "Security audit" })

Frontmatter fields

All fields are optional — sensible defaults for everything.

Field Default Description
description filename Agent description shown in tool listings
display_name — Display name for UI (e.g. the agent list, the conversation overlay)
tools all 7 Which tools the agent can call. Built-in names (read, grep, …), * / all (all built-ins), none, and ext:<extension> / ext:<extension>/<tool> selectors for extension tools. See Tool & extension scoping below
extensions true Which extensions to load for the agent. true (all child-safe defaults), false (none), or an explicit list: [mcp, "/abs/path.ts", "*"]. pi-background is excluded; intentional selection is refused by name. See Tool & extension scoping below
exclude_extensions — Extension denylist applied after extensions: — exclude wins. Plain names only (case-insensitive), no paths or *. Useful with extensions: true to drop one extension (e.g. pi-notify)
skills true true uses Pi's discovered skills, including skills supplied by loaded extensions; false exposes none; a comma-separated list preloads only those named skills into the system prompt (see Skill Preloading for discovery locations)
memory — Persistent agent memory scope: project, local, or user. Auto-detects read-only agents
disallowed_tools — Comma-separated tools to deny even if extensions provide them
ask_tools — Comma-separated tools whose first call requires human approval. Approval permits later calls to that tool across resumed turns of the current in-memory child. It is not persisted: reopening an evicted child from disk or restarting Pi asks again. Without an interactive approver, calls are blocked
isolation — Set to worktree to run in an isolated git worktree
tier none This agent's default model tier, by name, from agentTiers.profiles. A tier passed at the call site overrides it. When set, it wins over model/thinking below — see Model tiers
model — Removed. An agent no longer chooses its own model; use tier. A file that still has it loads normally, with a warning naming it — the line simply has no effect
thinking — Removed, same as model above
max_turns unlimited Max agentic turns before graceful shutdown. 0 or omit for unlimited
persist_session false Persist this subagent as a normal pi session instead of keeping the session in memory only. The subagent's .output transcript is still written either way unless output_transcript: false
output_transcript true (or subagents.json outputTranscript) Write this subagent's .output transcript; when set, overrides the subagents.json outputTranscript default. Set false to write no transcript file or path. Governs only the transcript — independent of persist_session, isolation: worktree, and memory:
session_dir pi default Optional session directory when persist_session: true; omitted uses pi's normal session location, and relative paths resolve from the agent cwd
allowed_subagents none Opt in to scoped nested Agent, get_subagent_result, and steer_subagent tools. Omitted / empty / none / false = no nesting; all (or "*" / true) = any enabled agent; comma-separated list = only those agent types
prompt_mode replace replace: body is the full system prompt (no AGENTS.md / CLAUDE.md inheritance). append: body appended to parent's prompt (agent acts as a "parent twin" — inherits parent's AGENTS.md / CLAUDE.md)
inherit_context false true is refused: the host cannot safely transfer post-hook parent context or pin the child's physical provider endpoint
run_in_background false Run in background by default
isolated false Hermetic specialist mode: forces extensions: false + skills: false + drops ext: selectors. Only built-in tools. Distinct from isolation: worktree (filesystem)
enabled true Set to false to disable an agent (useful for hiding a default agent per-project)

Frontmatter is authoritative. If an agent file sets model, thinking, max_turns, inherit_context, run_in_background, isolated, or isolation, those values are locked for that agent. Agent tool parameters only fill fields the agent config leaves unspecified.

inherit_context: true is rejected before child creation, for top-level, nested, mention-started, and managed agents. A parent's session projection is pre-hook: Pi does not expose the post-context / context_with_system result to this extension, and a child cannot atomically pin the same physical endpoint across virtual/provider routes. A raw_for confirmation would not fix that request-time auth/routing race. Pass a deliberately sanitized summary in the Agent task prompt instead; if context must be edited in the parent session, persist a context_edit first and still pass only the summary you intend to disclose. Normal calls with inherit_context: false are unchanged. Older persisted child sessions containing the inherited-history prompt cannot resume or switch models safely; start a new child session with a sanitized task rather than reopening them.

Forgiving model: resolution. A model: pin is matched against pi's model registry tolerantly, so cosmetic id variations don't silently drop the agent back to the parent's model: . and - are treated as equivalent in version numbers (claude-haiku-4.5 ≡ claude-haiku-4-5), a trailing -YYYYMMDD date stamp is optional (anthropic/claude-haiku-4-5-20251001 matches an undated registry id and vice-versa), and a provider/modelId whose named provider doesn't carry that model retries the bare id against every provider. Precedence is exact → fuzzy under the named provider → same model under any provider → unavailable, so an exact match always wins and dated snapshots aren't conflated. If nothing resolves, the pin can't run and the agent inherits the parent model — /agents → Agent types flags this case as (unavailable, fallback: inherit) and shows the resolved target (→ provider/id) when resolution lands on a different provider or version than configured. (This is distinct from Model Scope enforcement, which resolves pi's configured enabledModels patterns.)

Nested subagents

Nested delegation is default-off. Set allowed_subagents only on a non-isolated custom agent that owns a real fan-out responsibility:

---
tools: read, grep, find
extensions: false
allowed_subagents: support-file-finder, support-callsite-tracer   # or `all`
---

The allowlist is a privilege boundary, not just a routing hint. A child runs with its own tools:, extensions:, and isolated: — the parent's restrictions are not inherited — so delegation grants the parent the union of what the listed agents can do. The read-only agent above can write and run commands through any listed agent that can, and all reaches every enabled agent including general-purpose. Choose the list as carefully as you would choose tools: itself; that is the main reason this is default-off.

allowed_subagents is runtime-enforced. A comma-separated list restricts nesting to those types; all (or "*" / true, matching how extensions: and skills: take booleans) allows any enabled agent; omitted, empty, none, or false means no nested tools are injected at all. Unknown, disabled, and out-of-list types are rejected rather than falling back — regardless of the project's fallback agent setting, so a configured fallback can never hand a nested caller an agent outside its allowlist — and a nested model: is validated against Model Scope exactly like a top-level spawn. Result, resume, and steering operations are ownership-scoped, so a parent can control only its own children. Nested records remain internal to that parent and do not appear in top-level tools, lifecycle events, or agent UI — so when a parent finishes, is stopped, or ends a resumed turn, its nested children are stopped with it. They do write their own .output transcript (subject to the same output_transcript gate), filed under the root session's directory alongside their ancestors', so a nested run can still be inspected after the fact. Their token usage is folded into every ancestor's totals up to the top-level agent (lifecycle events, completion notifications, /agents), so nested spend stays attributable at any depth even though the children themselves stay hidden. A nested result that ends stopped, aborted, or steered is labelled as partial, the same guarantee top-level results carry.

The hard cap is depth 2 by default: main session (0) → subagent (1) → nested child (2). Change it project-wide with maxSubagentDepth in subagents.json (or /agents → Settings → Nested depth); 0 or 1 turns nesting off everywhere. An agent already at the cap gets no nested tools at all — not even get_subagent_result, since it can never own a child. A child must independently set allowed_subagents to delegate again; isolated agents never receive nested tools.

Nested children don't occupy maxConcurrent slots — their parent already holds one, and queueing them behind it would deadlock a parent waiting on its own child. The depth cap bounds how deep nesting goes, not how wide: a parent's only limit on concurrent children is that each spawn costs it a turn. Pair allowed_subagents with a max_turns on that agent if you want a hard ceiling on its fan-out.

Because a subagent session never activates this extension (that is what keeps a child from building a second agent manager, and it is why nested tools are injected directly instead), a subagent also gets none of the extension's other surfaces: no /agents command, no cross-extension RPC handlers, no subagents:ready event.

Child project trust

Root startup and reload also gate this package's project subagents.json, .pi/agents, .agents/agents, and custom tool description on the public project-trust decision. Unknown or throwing trust checks skip project configuration before parsing; global definitions/settings and embedded defaults remain available. Global aliases into denied roots are not treated as global authority. The runner rejects a denied project-owned definition (including the actual fallback definition) before package resolution, even when it requests an extension outside the project or in a global package cache.

On Pi 1.0.4, children capture the parent's public isProjectTrusted() decision and configuration root before resolving child resources. Missing or failed trust checks mean denial; children never prompt for or grant project trust. The child passes its derived trust explicitly to SettingsManager, governing project settings merging, resource/package discovery, and project MCP configuration. Denied project settings and MCP configuration are not merged. Before the SDK reads native global settings.json or mcp.json, the child checks their lexical and canonical sources against every retained denied root. A global file or agent-directory alias into a denied root fails child admission, even with extensions disabled; genuine global files and aliases into an approved parent remain usable.

Trust belongs to the configuration root, not the tool execution directory. Execution in a worktree with configCwd pinned to the trusted parent retains that parent's resources (including MCP configuration), also for nested children. A different configuration root, including a newly copied worktree, is not implicitly trusted. Resource checks retain all denied ancestor roots, including the captured parent root whose trust was denied and the child's untrusted configCwd. Lexical and canonical path checks apply to explicit, configured, and discovered extension paths before their factories execute, and to discovered/preloaded skills before instruction injection. Changing cwd/configCwd, naming an explicit parent extension, or using a symlink/package alias cannot re-admit the denied source. Named skill preloading also checks global-root ancestor aliases against all retained denied roots.

Genuine user/global resources outside denied roots remain available subject to the child's normal policy. This is a resource-source and instruction-loading boundary, not an OS sandbox: arbitrary allowed global extension code can perform its own I/O or register MCP servers; project trust does not contain that code's behavior. isolated, extensions: false, named extension selection, and skill restrictions still narrow access independently. Project context files and appended prompts remain suppressed in child resource loading; append-mode agents receive only the parent's already-built system prompt.

Tool & extension scoping

extensions: decides which extensions load, tools: decides which tools surface to the LLM. They compose:

# Default (both omitted): child-safe extensions load, all 7 built-ins surface

tools: read, grep, find           # narrow to listed built-ins; extensions still load
tools: "*"                        # all 7 built-ins (alias: `all`)
tools: none                       # zero built-ins (alias: `""`)
tools: "*, ext:mcp/search"        # built-ins plus one extension tool

extensions: false                 # no extensions load
extensions: [mcp]                 # only mcp loads
extensions: ["*", "/abs/foo.ts"]  # all defaults plus one path-loaded extension

exclude_extensions: pi-notify     # everything except pi-notify (with extensions: true)

# Specialist: load one extension, expose only one of its tools, keep built-ins
extensions: [mcp]
tools: "*, ext:mcp/search"

isolated: true                    # hermetic: built-ins only, no extensions/skills/context

A few rules the examples don't make obvious:

  • extensions: is the loading authority, subject to child lifecycle safety. pi-background is excluded before its factory loads, including under default inheritance and "*"; an extension-error:… warning names it. Its jobs can wake a retained child after session.prompt() finishes, when the manager has marked the record terminal, released its concurrency slot, and removed its worktree. Until manager-owned asynchronous continuation exists, explicitly selecting pi-background (by name, alias, package path, or entry path) is refused by name rather than silently dropped. ext:pi-background cannot override this boundary and warns as an unloaded selector. Ordinary extensions remain available; parent sessions are unaffected. This does not add or change another extension's settings.
  • ext:foo in tools: narrows what surfaces; it can't load foo on its own. Mismatches fire extension-error:… warnings.
  • Any ext: entry flips extension tools to an explicit allowlist — unnamed extensions still load (handlers fire) but expose no tools. So tools: "*, ext:mcp/search" exposes only search from mcp, nothing from any other extension.
  • Extension names match case-insensitively ([Mcp] = [mcp]); tool names in ext:foo/bar stay case-sensitive.
  • Extensions that register tools lazily work too. MCP-backed extensions typically can't enumerate their tools until their servers connect, so they register from session_start or before_agent_start rather than at load. Subagent scoping is re-derived as tools appear, including under ext: selectors. Scoping does not override Pi's own exposure: deferred/codemode tools and defaultActive: false tools are not activated for the model, and tools deliberately deactivated during a session stay inactive on later turns. Pi may still allow nested ctx.executeTool() calls to deferred/codemode tools, but the child's selector and ask_tools: approval gate both model-issued and nested calls.
  • A configured defaultToolTimeoutMs bounds the public tool_call policy and each selected extension handler using one deadline per call, including nested calls and handlers registered later with pi.on. On expiry the call is blocked; a Promise race returns control but cannot cancel side effects of a misbehaving hook that keeps running. Interactive ask_tools: confirmation is human decision time, not tool execution time: the deadline starts after approval. The separate execution timeout can still abort a tool whose execute() hangs.
  • When the SDK exports public codemode, tool-search, and MCP factories (as on the verified Pi 1.0.4 baseline), they are included as built-in extensions in children. extensions: false omits them; extensions: [mcp] selects MCP; exclude_extensions: mcp removes it; and tools: ext:codemode, ext:tool-search, or ext:mcp/<tool> narrows their tools like any other extension. Capability checks omit factories that the host does not export; this fallback is not an older-host support guarantee. Whenever extensions are enabled, the child uses Pi's public package resolver to select discovered paths before loading factories. Thus an excluded extension cannot register a conflicting /mcp command and displace a selected replaceable built-in before filtering. Resolution can perform configured package installs, as Pi's normal loader does, and a selected extension that fails to load is reported rather than silently dropped.
  • Explicit sources are validated separately before canonical deduplication, so a real file plus its file alias, or two package-directory aliases, load once without falsely reporting a missing source. Ownership names are unioned for admission/exclusion, but whole-package priority uses each resource's declaring package; direct entries use the canonical declaring owner. A package that aliases another package's entry does not lose its unrelated entries when the target is explicitly selected. Directory-backed modules are matched to Node's supported module entry without evaluating factories; undeclared siblings are not attributed to that package.
  • An installed package extension matches by its package short name (@scope/pi-subagents → [pi-subagents]), in addition to its path-derived name (a package whose entry is src/index.ts also answers to [src]). Prefer the package name — the path-derived one is incidental.
  • Plain tools: typos fail loudly: tools: reed, grep fires tools-error:… instead of silently producing an under-tooled agent.
  • exclude_extensions: wins over extensions: and over ext: selectors — an excluded extension never loads and a tools: ext: entry can't pull it back. Plain names only (no paths, no *); a name matching nothing fires an extension-error:… warning.
  • exclude_extensions: is not a sandbox: preselection prevents excluded discovered factories from loading into the child, but resolving configured packages can still install code. Allowed extensions run with the Pi process's permissions. Don't rely on extension selection to contain untrusted packages.
  • Array and string forms are equivalent: [a, b] == "a, b".

Tools

Agent

Launch a sub-agent.

Parameter Type Required Description
prompt string yes The task for the agent
description string yes Short 3-5 word summary (shown in UI)
subagent_type string yes Agent type (built-in or custom)
tier string no Model tier for this spawn, by name. Overrides the agent's own default tier. Unknown tiers are rejected, not substituted — see Model tiers
max_turns number no Max agentic turns. Omit for unlimited (default)
run_in_background boolean no Run without blocking
supervisor_questions "human" / "parent" / "off" no Default human; parent routes only ordinary clarification from fresh non-isolated background calls. No schedules or mode-changing resumes
resume string no Agent ID to resume a previous session
isolated boolean no No extension/MCP tools
isolation "worktree" no Run in an isolated git worktree
inherit_context boolean no Defaults to false; true is refused before child dispatch. Put a sanitized summary in prompt instead

get_subagent_result

Check status and retrieve results from a background agent.

Parameter Type Required Description
agent_id string yes Agent ID to check
wait boolean no Wait for completion
verbose boolean no Include full conversation log

Cancelling a wait: true call (for example, with Esc) stops only the wait. The background agent keeps running, and its completion notification still arrives normally.

steer_subagent

Send a chat message to a running or queued agent. A running agent receives it after the current tool execution; a queued agent receives it when its session starts.

Parameter Type Required Description
agent_id string yes Agent ID to steer
message string yes Message to inject into agent conversation

answer_subagent_question (main only)

Answer a current opted-in background child's ordinary clarification: questionId is the bounded one-use ID in extension context; answer is at most 2,000 characters, an offered option:N token for choices, or empty to decline. Forged, expired, cancelled, or stale IDs and unoffered choices are rejected. The tool is never inherited or injected into children and cannot grant permission or override a human rejection.

Commands

Command Description
/agents Interactive agent management menu

The /agents command opens an interactive menu:

Agent runs (2) · 1 running · 0 queued · 1 completed · 0 wrapped up · 0 stopped · 0 aborted · 0 failed
Agent types (6) · unified list of defaults and custom agents
Model tiers (3) · the (model, thinking) catalogue
Create new agent
Settings
  • Agent runs — select any retained run with a session to open its conversation viewer. Queued runs without a session remain listed but are not openable until they start; selecting one offers a stop confirmation, and @handle message queues a chat message. The summary reports running, queued, completed, wrapped up, stopped, aborted, and failed buckets separately. While a run is still active, press Enter to open the chat composer, then Enter again to send a message that redirects the agent (same mechanism as the steer_subagent tool; Esc or an empty submit returns), or press x and follow the displayed explicit confirmation to stop/abort it — including background agents, which a global Esc can't unambiguously target (Esc still stops a blocking foreground Agent call). A stopped agent reports its partial output flagged as incomplete, not as a completion.
  • Agent types — unified list with textual source labels (project, global, and disabled). Each row shows the agent's model, and the highlighted agent's full description appears below the list. The model column flags (unavailable, fallback: inherit) when a configured model can't be resolved (it would silently inherit the parent model), and shows (resolved: provider/id) when it resolves to a different provider or version than configured. Select an agent to manage it:
    • Default agents (no override): Eject (export as .md), Disable
    • Default agents (ejected/overridden): Edit, Disable, Reset to default, Delete
    • Custom agents: Edit, Disable, Delete
    • Disabled agents: Enable, Edit, Delete
  • Eject — writes the embedded default config as a .md file to project or personal location, so you can customize it
  • Disable/Enable — toggle agent availability. Disabled agents stay visible in the list with a disabled label and can be re-enabled
  • Create new agent — choose project/personal location, then manual wizard (step-by-step prompts for name, tools, model, thinking, system prompt) or AI-generated (describe what the agent should do; a sub-agent writes a unique same-directory staging file, and the parent parses and compare-commits it to the target). Any name is allowed, including default agent names (overrides them)
  • Model tiers — create, edit and delete the tier profiles. Each row shows what the tier resolves to on this machine (small — claude-haiku-4-5 · thinking max), and a tier dropped as malformed is listed as blocked so it can be fixed rather than staying invisible. The model picker offers this machine's available models (narrowed to your scope when Scope models is on) plus inherit and a typed escape hatch; the thinking picker offers only the levels the chosen model actually supports, since the rest would be silently clamped at spawn. Deleting the tier that defaultTier names clears the default in the same step
  • Settings — configure max concurrency, default max turns, grace turns, default model, default tier, and join mode at runtime

Graceful max turns

Instead of hard-aborting at the turn limit, agents get a graceful shutdown:

  1. At max_turns — steering message: "Wrap up immediately — provide your final answer now."
  2. Up to 5 grace turns to finish cleanly
  3. Hard abort only after the grace period

Each resumed prompt gets its own counters, not an unlimited continuation or a deduction from prior turns. This applies to foreground/background resumes, nested children, mentions, and reused managed threads. Agent frontmatter remains authoritative; without a per-resume override, the original invocation's turn limit is inherited, falling back to configured defaults. Resume overrides use the same non-negative safe-integer validation as spawning; caller zero cannot disable an inherited/configured cap. An authoritative frontmatter zero remains unlimited, even after an earlier finite invocation. Nested resumes reload the same branch-local definitions as nested spawns, never the root registry; an internal resume without scoped policy retains its admitted invocation budget. A retained session's admitted turn policy, including unlimited, survives later project-default changes; the manager supplies that same effective cap to queued and running progress trackers. The existing token/tool-call caps and tool timeout also apply to resumed prompts. Budgets are event-based: an already-running provider/tool can overshoot within one event or ignore abort; these are not process-isolation guarantees.

Confirmed shutdown retires grouped completion, batch-finalization, held-nudge timers, and invocation-owned foreground spinners before awaiting child teardown. Captured delivery callbacks cannot wake the main model or publish foreground progress afterward, even while a provider ignores abort. Refused or failed resume admissions retire their own UI callbacks; an admitted run owns cleanup until settlement. Mention continuations cannot publish notifications after their root/runtime retires. Managed lifecycle/usage receipts remain available during cooperative shutdown; vetoable navigation attempts do not dispose these resources.

Status Meaning UI treatment
completed Finished naturally completed
steered Wrapped up at the limit; output may be partial wrapped up · turn limit in warning color
aborted Grace period exceeded aborted in warning color
stopped User-initiated abort stopped

Concurrency

Background agents are subject to a configurable concurrency limit (default: 4). Excess agents are automatically queued and start as running agents complete. Queued agents appear in the list once they start.

Foreground agents bypass the queue — they block the parent anyway.

Join strategies

When background agents complete, they notify the main agent. The join mode controls how these notifications are delivered. It applies only to background agents.

Mode Behavior
smart (default) 2+ background agents spawned in the same turn are auto-grouped into a single consolidated notification. Solo agents notify individually.
async Each agent sends its own notification on completion (original behavior). Best when results need incremental processing.
group Force grouping even when spawning a single agent. Useful when you know more agents will follow.

Timeout behavior: When agents are grouped, a 30-second timeout starts after the first agent completes. If not all agents finish in time, a partial notification is sent with completed results and remaining agents continue with a shorter 15-second re-batch window for stragglers.

Configuration:

  • Configure join mode in /agents → Settings → Join mode

Model tiers

An Agent tier is one name for a (model, thinking) pair. The host agent picks a tier by name and nothing else: the Agent tool exposes tier and does not expose model or thinking, so which model runs is decided by the Agent-tier catalogue rather than by the orchestrator improvising per call.

Names are yours. The names below are only an example — research, cheap, nightly are equally valid keys. Replace the illustrative provider/model values with models available in your environment.

{
  "agentTiers": {
    "defaultTier": "medium",
    "profiles": {
      "low":      { "description": "Fast, cheap exploration",       "model": "provider/fast-model", "thinking": "max" },
      "medium":   { "description": "Ordinary planning and review",  "model": "provider/reasoning-model", "thinking": "max" },
      "high":     { "description": "Architecture and risky review", "model": "provider/architecture-model", "thinking": "xhigh" },
      "research": { "description": "Long-context research",         "model": "provider/long-context-model", "thinking": "max" }
    }
  }
}

A profile is all-or-nothing: both model and thinking are required, and either may be the literal "inherit" to keep the parent's. description is optional and defaults to the key; it is what the host reads when choosing between Agent tiers.

One catalogue, including for workflows

A managed pi-workflows call arrives naming a key from this same agentTiers catalogue. There is no second tier catalogue and no second resolver: this package still owns every model, every thinking level, and the only resolveAgentTier().

What a workflow script writes is not that key. A script names a strength — low, medium, high, pi-workflows' own word for how much effort a step deserves — and a strengths table on that side chooses which of your tiers it runs on:

// in a workflow script
await agent("summarize this diff", { strength: "low" })
await agent("design the migration", { strength: "high" })
// pi-workflows' own settings, edited with `/workflows strength`
{ "strengths": { "low": "cheap-search", "high": "deep" } }

The indirection exists so that re-pricing workflow work does not re-price everything else. Workflow fan-outs ask for cheap work by the dozen, and so does the Explore agent and every spawn that names no tier of its own; if the workflow side reached your catalogue directly, making a 26-agent fan-out affordable would mean editing the tier all of them share. Pointing a strength elsewhere leaves your tiers alone.

Nothing about that reaches this side. By the time a request arrives it carries one tier key, and this package cannot tell a mapped call apart from a spawn that named the key itself. Nor is it a second policy: a strengths value is a key in this catalogue and never carries a model or thinking of its own — that is the line between it and the retired workflow.tiers key, which did.

The tier is resolved by the same resolveAgentTier() path an ordinary Agent spawn uses — same precedence, same model lookup, same thinking clamping, same availability checks, same immutable resolution snapshot. A tier the host does not define is rejected before dispatch, naming the tiers it does define.

Model and thinking are deliberately absent from the managed request. A tier is the only model policy a workflow can express, so there is no second selector that could silently win or be silently ignored.

Fresh installs ship an effort ladder: low, medium, high. Every shipped profile inherits its model, so a new machine gets a working vocabulary without this package ever choosing a vendor for you. Those names are also what pi-workflows' shipped default table maps its strengths onto — identity, and only where you define the name — so a stock machine runs workflows at the strengths their scripts asked for. Rename or remove them and that default simply yields nothing: a managed call that names no tier uses the agent's own tier, then agentTiers.defaultTier, and finally falls back to medium.

medium inherits its model, so on an unconfigured machine that fallback runs on the parent session's model. What it buys is a call with a named policy, a durable resolution snapshot and a scope check — not cheaper work. If you want managed work to run somewhere cheaper, set a defaultTier whose profile pins a model.

That last fallback is scoped to managed calls. It is deliberately not the catalogue's defaultTier: a shipped default that applied to every ordinary spawn would silence defaultModel and pin a thinking level on machines that configured neither.

So Default tier has three settings, not two, and the menu offers all three:

Setting Ordinary spawn Managed workflow call
a tier name that tier that tier
unset defaultModel, then the parent session the shipped medium
none defaultModel, then the parent session rejected

none is a policy statement, recorded as noDefaultTier; unset is the absence of one. Deleting the profiles has the same effect on managed calls as none.

How the host discovers tiers

The catalogue is rendered into the Agent tool description at registration, so the host knows the vocabulary before its first call — there is no lookup tool to remember. It sees:

Available agent tiers:

- low: Fast, cheap exploration
  model: provider/fast-model
  thinking: max
...
Default tier: medium

The caller may pass only a tier key. Do not pass model or thinking directly.

A custom tool description can place it with {{tierList}}, {{compactTierList}} or {{defaultTier}}. Tier changes apply on the next pi session, since the description is built once at registration.

Precedence

  1. tier passed to the Agent call
  2. tier: in the agent's frontmatter
  3. agentTiers.defaultTier
  4. defaultModel — a model with no tier attached, for workspaces that want one setting rather than a catalogue
  5. the parent session's model and thinking

An agent cannot pin its own model at any step. model:/thinking: in frontmatter are read only to warn that they are stale, and the built-in agents pin nothing either. With nothing configured at all, a subagent runs on the parent session's model.

A managed workflow call cannot take steps 4 and 5 — it has no parent session to inherit from — so it gets one extra step between 3 and the end: the shipped medium fallback. That step exists only for callers that would otherwise fail closed, which is why it does not displace defaultModel for everyone else.

defaultModel

Steps 1–3 are a catalogue; step 4 is one line. Set it when the whole point is "subagents run on the cheap model" and there is no second policy to name:

{ "defaultModel": "anthropic/claude-haiku-4-5" }

It only decides the model — thinking still comes from the parent, because a level nobody chose for a specific model is exactly what a tier exists to express. Any tier that applies overrides it outright.

Set it from /agents → Settings → Default model (Enter opens the picker), or by hand. It accepts the same references a tier's model does, plus the literal "inherit", which is how a project cancels a global defaultModel — omitting the key inherits whatever the global file set.

Unlike a tier, an unresolvable defaultModel does not fail the spawn: it falls back to the parent model, and the Settings row shows (unavailable, fallback: inherit). A tier is refused because someone named that policy at the call site; defaultModel is the value nobody named, so one unauthed provider must not take every spawn on the machine down with it.

Editing tiers

/agents → Model tiers manages the catalogue — new tier, change a tier's model, thinking or description, delete one. defaultTier lives with the other defaults in /agents → Settings → Default tier. Both write the project file; the global file is never written from the menu.

Two things the menu knows that a hand-edited file does not: the thinking picker offers only levels the chosen model supports (the rest get clamped at spawn anyway), and a tier dropped as malformed still appears in the list, marked blocked, so redefining it is one selection rather than an archaeology expedition through subagents.json.

The Agent tool description is built once at registration, so a tier edit reaches the model on the next pi session. Resolution itself is live — a spawn right after the edit already uses the new profile.

One thing the file format cannot express: a project deleting a tier that only the global file defines. The menu writes the merged catalogue back to the project file, so deleting one of several works, but deleting the last one — or clearing a defaultTier that only global sets — leaves no agentTiers key behind, and the global value is inherited again on the next start. Remove it from ~/.pi/agent/subagents.json instead.

Refusals

A defaultTier, or an agent's tier:, that names no defined profile is reported at startup, listing the available keys — a typo there would otherwise sit quiet until the first spawn that needed it, possibly minutes into a session.

These fail before the spawn, with the tier key and where it came from named. None of them silently substitutes another model:

  • a tier key nobody defined (from the call, the agent file, or defaultTier)
  • a profile dropped as malformed during settings load
  • a profile whose model is not available on this machine
  • a syntactically invalid key (blank, whitespace, over 64 characters)

Merging global and project settings

~/.pi/agent/subagents.json supplies the catalogue; <cwd>/.pi/subagents.json edits it. A project profile replaces its global namesake whole — never field by field, which would let a project change a model while inheriting a thinking level nobody chose for that pair. A project profile that fails validation blocks its global namesake rather than reviving it, and defaultTier is a simple project-over-global override.

Migrating from model:/thinking:

Define the Agent-tier profiles once, then replace each agent's model:/thinking: with tier: <name>. Files that still carry the old fields load and run — the fields are ignored, with a warning naming the file — so the migration can be done one agent at a time. Until an agent names a tier it uses agentTiers.defaultTier, then defaultModel, or the parent's model when none is set.

Programmatic callers and the legacy RPC may still pass model/thinking directly. That is the escape hatch for code, not a way to configure an agent.

Model scope

Opt-in: off by default. Enable via /agents → Settings → Scope models.

When on, each subagent spawn's effective model is validated against pi's own enabledModels list (configured via pi's /scoped-models UI). pi-subagents reads that list; it doesn't manage it. Both of pi's settings files are honored: global ~/.pi/agent/settings.json and project-local <cwd>/.pi/settings.json. Project overrides global — mirrors pi's SettingsManager deep-merge, so a tighter per-project scope (hand-edited into the project settings) is respected.

Out-of-scope handling depends on source:

Model source Out-of-scope behavior
Caller-supplied programmatic model (only when no Agent tier applies) Hard error returned to the orchestrator, listing allowed models
Pinned in agent frontmatter Warning toast + the pinned model runs (frontmatter is authoritative)
Parent-inherited (neither set) Warning toast + parent's model runs

Design: scopeModels is a guardrail against unexpected runtime model choices, not a hard policy against user-level config. An applicable Agent tier is checked after its single final resolution; compatibility model inputs are checked only on the no-tier path and cannot override a tier.

Nested spawns (nested subagents) apply the same table against the parent's config root. The hard-error case is identical; the warning cases proceed silently, since a subagent session has no UI to toast to.

Pattern format: pi's enabledModels entries may use exact provider/modelId references, bare IDs or partial names, case-insensitive globs such as anthropic/* and *sonnet*, and optional :thinking suffixes. Non-glob partial names match model IDs or display names, not provider-qualified provider/partial references; a glob's thinking suffix is removed before matching, so custom/*:high also includes colon-suffixed model IDs. This guard checks only model membership, not the configured thinking level. Malformed non-string entries are ignored without erasing valid entries or disabling a configured project scope. If a configured list resolves to no available models, caller-supplied model choices are refused rather than letting the scope check silently pass; pinned and inherited models retain the warning-and-proceed behavior above.

No-op safety: if enabledModels is missing or empty in pi's settings, scope check skips entirely — no false positives, no spurious errors.

Persistent settings

Runtime tuning values set via /agents → Settings (max concurrency, default max turns, grace turns, nested depth, fallback agent, default model, default tier, default join mode, scheduling on/off, scope models on/off, strict agent files on/off, disable defaults on/off, output transcript on/off, tool description full/compact/custom, fleet view on/off) persist across pi restarts. Two files, merged on load:

  • Global: ~/.pi/agent/subagents.json — your machine-wide defaults. Edit by hand; the /agents menu never writes here.
  • Project: <cwd>/.pi/subagents.json — per-project overrides. Written by /agents → Settings.

Precedence: project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency 4, default max turns unlimited, grace turns 5, nested depth 2, join mode smart, defaults enabled).

The workflow settings key is retired; a file that still has one is ignored with a warning naming the key. This file holds no workflow routing of its own: a managed pi-workflows call arrives naming a key from agentTiers, and which key that is was decided on the pi-workflows side by its own strengths table — a table of keys into this catalogue, never a second catalogue and never its own model/thinking. agentTiers.defaultTier replaces what workflow.defaultTier used to do. See One catalogue, including for workflows.

Default model (defaultModel, unset): the model a non-tiered ordinary subagent runs — see defaultModel for where it sits in precedence, why an unresolvable value falls back instead of failing, and how "inherit" lets a project cancel a global default. Default tier (agentTiers.defaultTier, unset) is the tier applied when neither the caller nor the agent names one; the profiles it selects from live under agentTiers. It has three settings — a tier name, unset, and none — which the menu offers separately because the last two behave differently for managed workflow calls; see Model tiers for the table.

Strict agent files (strictAgentFiles, default false): normal startup skips unreadable or malformed agent definitions with a warning that includes the file path. Enable it to fail closed during the first session_start, using that session's ctx.cwd, with the path in the error instead of silently running a surviving lower-priority override. A failed validation leaves no root manager or RPC responder behind. Reloads after startup remain lenient, so an accidental edit cannot terminate an active session; the setting applies on the next pi session.

Nested depth (maxSubagentDepth, default 2): the hard ceiling on nested delegation, counted from the main session (main = 0, its subagents = 1). 0 or 1 disables nesting project-wide regardless of any agent's allowed_subagents. Read when a subagent session is built, so a change applies to agents started after it.

Fallback agent (fallbackSubagent, default general-purpose): the agent used when a caller-supplied subagent_type doesn't resolve to exactly one enabled agent — unknown, disabled, or ambiguous because two agents differ only by case. Name any enabled agent to route those calls there instead, or set none for strict, fail-closed dispatch: the call is refused with an error listing the available types, and nothing spawns. Strict mode matters most for background and scheduled calls, which would otherwise start executing a substituted agent before the caller learns anything. Also settable from /agents → Settings → Fallback agent. The boolean false is accepted as a spelling of none, because it would otherwise be dropped as the wrong type and silently leave the permissive default in place. Every other value is read as an agent name, so a mistaken off fails loudly at dispatch rather than meaning one thing in the settings file and another in the resolver. A fallback agent that is itself unknown or disabled is a misconfiguration and is reported rather than quietly replaced. Note the default is unchanged and stays permissive by design: with disableDefaultAgents and no general-purpose of your own, an unresolvable type still resolves to a built-in config carrying all tools — set none (or name one of your own agents) to close that.

Disable defaults (disableDefaultAgents, default false): when on, the three built-in agents (general-purpose, Explore, Plan) are not registered — only your project/global custom agents are advertised and spawnable. User-defined agents are unaffected, including ones that override a default by name. The Agent tool's type list updates on the next pi session (the tool schema is registered at startup).

Output transcript (outputTranscript, default true): the project/global default for writing each subagent's .output transcript. Toggle via /agents → Settings → Output transcript, or set false in subagents.json to make transcripts opt-in project-wide — useful when run transcripts shouldn't sit on disk for backup or DLP tooling to pick up. A custom agent's output_transcript frontmatter overrides this per agent. Applied live at spawn time. Governs only the transcript, not persist_session, worktree commits, or memory files.

Tool description (toolDescriptionMode, default "full"): which Agent tool description the LLM sees. "full" is the rich Claude Code-style prompt (~1,400 tokens with the default agents); "compact" is ~75% smaller — one-line agent type list, terse usage notes — for small/local models where tool-spec tokens are expensive. Per-option details stay in the parameter descriptions in every mode (the parameter schema is never customizable). Applies on the next pi session.

"custom" registers your own description from <cwd>/.pi/agent-tool-description.md (project) or <agentDir>/agent-tool-description.md (global; project wins). The file is read once at tool registration, so edits also apply on the next pi session. Dynamic parts stay live via placeholders — a static agent list would go stale the moment you add a custom agent:

Launch an autonomous agent. Available types:
{{typeList}}

Custom agents live in .pi/agents/ or {{agentDir}}/agents/.

Placeholders: {{typeList}} (full per-agent descriptions), {{compactTypeList}} (first sentence each), {{agentDir}}, {{scheduleGuideline}} (expands with its own leading newline + - bullet when scheduling is on — place it directly after your last rule line; empty when scheduling is off). Unknown placeholders are left verbatim with a stderr warning; a missing or empty file falls back to "full" with a warning. Note the usual trust umbrella: a project-level file shapes the orchestrator's prompt, same as project agents and extensions do.

Starting point: copy examples/agent-tool-description.md — it reproduces the default full description exactly (a CI test keeps it in sync), so you can trim from a known-good baseline instead of writing from scratch.

Example — global defaults for a beefy machine:

mkdir -p ~/.pi/agent
cat > ~/.pi/agent/subagents.json <<'EOF'
{
  "maxConcurrent": 16,
  "graceTurns": 10
}
EOF

Every project now starts with concurrency 16 and grace 10, without ever touching the menu. Individual projects can still override via /agents → Settings.

Failure behavior: missing file is silent; malformed JSON logs a [pi-subagents] Ignoring malformed settings at … warning to stderr; invalid/out-of-range field values are dropped per-field; write failures downgrade the /agents toast to a warning with (session only; failed to persist).

Events

Agent lifecycle events are emitted via pi.events.emit() so other extensions can react:

Event When Key fields
subagents:created Background agent registered id, type, description, isBackground, optional owner
subagents:started Agent transitions to running (including queued→running) id, type, description, optional owner
subagents:completed Agent finished successfully (background and foreground) id, type, durationMs, tokens (lifetime { input, output, total }), toolUses, result, optional outputFile/owner
subagents:failed Agent errored, stopped, or aborted (background and foreground) same as completed + error, status, optional outputFile/owner
subagents:steered Steering message sent id, message
subagents:compacted Agent's session successfully compacted id, type, description, reason ("manual" / "threshold" / "overflow"), tokensBefore, compactionCount, optional owner
subagents:scheduled Schedule lifecycle change { type: "added" | "removed" | "updated" | "fired" | "error", … } (job/agentId/error fields per type)
subagents:scheduler_ready Scheduler bound to session, enabled jobs armed sessionId, jobCount
subagents:ready RPC handlers registered and armed — fired on session start; not emitted in a session that excludes pi-subagents version: 4, capabilities (managedSpawn, lifecycleOwner, ownedStop, ownedQuiescence, childContext, agentTiers, managedPolicy — all required), routingPolicy (Agent-tier catalogue + fingerprint)
subagents:settings_loaded Persisted settings applied at extension init settings (merged global + project)
subagents:settings_changed /agents → Settings mutation was applied settings, persisted (boolean — false on write failure)

tokens.total = input + output + cacheWrite. cacheRead is excluded — each turn's cacheRead is the cumulative cached prefix re-read on that one API call, so summing per-message would over-count it. Use contextUsage.percent (surfaced as (NN%) in the agent list) for current context size.

Cross-Extension RPC

Other pi extensions can spawn and stop subagents programmatically via the pi.events event bus, without importing this package directly.

All RPC replies use a standardized envelope: { success: true, data?: T } on success, { success: false, error: string } on failure.

Discovery

Listen for subagents:ready to know when RPC handlers are available:

pi.events.on("subagents:ready", () => {
  // RPC handlers are registered — safe to call ping/spawn/stop
});

subagents:ready fires only when pi-subagents is actually loaded and bound in the current session. A session that excludes it (via an agent's extensions:) emits no subagents:ready and does not answer the RPC channels — exactly as if pi-subagents were not installed. Treat "no subagents:ready" as "not available here" and give discovery a timeout rather than waiting indefinitely.

Ping

Check if the subagents extension is loaded and get the protocol version and current routing-policy fingerprint:

const requestId = crypto.randomUUID();
const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, (reply) => {
  unsub();
  if (reply.success) console.log("Protocol version:", reply.data.version, "routing policy:", reply.data.routingPolicy.fingerprint);
});
pi.events.emit("subagents:rpc:ping", { requestId });

The reply always carries version, capabilities, and routingPolicy. Anything beyond those is requested by name through an optional include array, and is sent only to a caller that asked:

pi.events.emit("subagents:rpc:ping", { requestId, include: ["maxConcurrent"] });

include accepts up to 8 names; unknown ones are ignored. Today the only one is maxConcurrent, the live background-agent pool size — the number of background slots a spawn competes for, which /subagents can change mid-session, so read it per operation rather than caching it at session start.

Opt-in rather than volunteered, because the envelope is validated with rejectUnknownKeys: a field added unconditionally would make every already-published caller reject the handshake and lose the peer entirely. Asking keeps both directions working without a version bump — an older peer answers without the field, and a caller that never asks gets the same envelope it has always parsed. Treat an absent field as "this peer does not publish it" and fall back to your own default; it is not an error.

Managed spawn (protocol v4)

Workflow-owned orchestration uses the subagents:rpc:spawn-managed channel. Its request may include the core identity fields plus an optional Agent tier, toolset, excludeTools, thread, and isolation: "worktree". There is no per-call model or thinking — the wire validator rejects them:

{
  "requestId": "request-1",
  "spawnKey": "run-id/node-id/attempt-1",
  "type": "Explore",
  "prompt": "Find the relevant files",
  "description": "Find relevant files",
  "tier": "low",
  "excludeTools": ["workflow", "workflow_control"],
  "isolation": "worktree",
  "owner": { "extension": "pi-workflows", "runId": "run-id", "nodeId": "node-id", "attemptId": "run-id/node-id/attempt-1" }
}

The manager validates and resolves the tier, agent configuration, queue, tool, session, and worktree policy against its own Agent-tier catalogue and model scope. The resolved tier and its snapshot are retained on the managed invocation/tombstone. spawnKey is idempotent within a root manager; the same normalized request returns the existing agent id and a conflicting request is rejected. A named managed thread re-enters one sequential session only while its effective model, thinking, toolset, denylist, isolation, and agent policy fingerprint remain unchanged — including the model and thinking its tier currently resolves to, so switching the session model interrupts a thread whose tier inherits it; a policy change or concurrent call is rejected rather than silently reusing the old session. Managed agents use the normal Agent execution path, queue, FleetView, activity, transcript, compaction, and lifecycle events. Only the automatic main-session completion nudge is suppressed for an owner-scoped record. Managed requests must carry an attempt-scoped owner, and stop-owned/quiesce-owned fail closed when exact node/generation metadata is missing. During branch replacement, timed-out records are detached and late callbacks are suppressed. Pre-tree bus notifications carry the host navigation attempt's signal so workflow consumers can distinguish retries after a veto; the event payload retains a fallback for an absent signal, without establishing support for older hosts. Active or unsettled child work vetoes a tree-navigation attempt rather than being aborted by that attempt. Pi has no post-veto, pre-leaf commit hook, so work started after preflight during asynchronous summarization can be quarantined at commit but cannot be guaranteed to finish.

Spawn

Spawn a subagent and receive its ID:

const requestId = crypto.randomUUID();
const unsub = pi.events.on(`subagents:rpc:spawn:reply:${requestId}`, (reply) => {
  unsub();
  if (!reply.success) {
    console.error("Spawn failed:", reply.error);
  } else {
    console.log("Agent ID:", reply.data.id);
  }
});
pi.events.emit("subagents:rpc:spawn", {
  requestId,
  type: "general-purpose",
  prompt: "Do something useful",
  options: { description: "My task", run_in_background: true },
});

options.model accepts either a Model object (e.g. ctx.model) or a "provider/modelId" string — strings are resolved against ctx.modelRegistry at the RPC boundary, so cross-extension callers can forward serializable values without losing auth context.

options.cwd (absolute path to an existing directory — anything else returns an error envelope; null means unset) runs the agent in a different working directory than the parent session. Its tools operate there and the prompt's environment block describes it, but .pi config still loads from the parent session's project — the target directory's .pi extensions never execute, and its agents/skills/settings are not picked up. Combined with isolation: "worktree", the worktree is created from the target directory's repo, the agent works at the equivalent subdirectory inside the copy (a monorepo-package cwd stays scoped to that package), and the resulting pi-agent-* branch lands in that repo — the completion message names it. On session end, worktree registrations are pruned in every repo that received one; only a hard crash can leave a stale entry (then: git worktree prune in the target repo). Agents with memory: keep reading/writing the parent project's memory.

Stop

Stop a running agent by ID:

const requestId = crypto.randomUUID();
const unsub = pi.events.on(`subagents:rpc:stop:reply:${requestId}`, (reply) => {
  unsub();
  if (!reply.success) console.error("Stop failed:", reply.error);
});
pi.events.emit("subagents:rpc:stop", { requestId, agentId: "agent-id-here" });

Optional workflow UI bridge

@signalridge/pi-subagents-protocol declares a separate version-1 UI contract: pi-workflows:ui:query, pi-workflows:ui:changed, pi-workflows:ui:open, and subagents:ui:open-owned. These are not new required managed capabilities and do not grow the strict v4 ping envelope. Query/change payloads are bounded display projections, not DAG or journal storage. An open request is an explicit user UI action, not an agent dispatch or approval grant.

The conversation opener requires the exact agentId plus { extension, runId, nodeId, attemptId }, an active root TUI and a retained session. Admission is acknowledged before the dialog closes; unavailable, foreign, sessionless and stale attempts are refused. Reload, tree replacement and shutdown retire pending UI work. Other peers can remain absent without disabling managed execution.

Owned lifecycle events may additionally carry managedTokens, the current attempt's usage. Existing tokens remains lifetime usage; Fleet never resets cumulative counters on thread reuse. Managed terminal tokenCount describes that attempt, including cache writes, so sequential thread calls do not bill earlier turns twice. Older stored terminal facts are not rewritten.

Reply channels are scoped per requestId, so concurrent requests don't interfere. Workflow callers should pass a foreground signal only to their own wait. Managed child cancellation is explicit through the owner-scoped lifecycle RPC; aborting an RPC wait does not abort the child.

Persistent agent memory

Agents can have persistent memory across sessions. Set memory in frontmatter to enable:

---
memory: project   # project | local | user
---
Scope Location Use case
project .pi/agent-memory/<name>/ Shared across the team (committed)
local .pi/agent-memory-local/<name>/ Machine-specific (gitignored)
user <agentDir>/agent-memory/<name>/ (default ~/.pi/agent/agent-memory/, honors PI_CODING_AGENT_DIR) Global personal memory

The user scope previously hardcoded ~/.pi/agent-memory/. If that legacy directory exists for an agent and the new location doesn't, it keeps being used — existing memories aren't orphaned.

Memory uses a MEMORY.md index file and individual memory files with frontmatter. Agents with write tools get full read-write access. Read-only agents (no write/edit tools) automatically get read-only memory — they can consume memories written by other agents but cannot modify them. This prevents unintended tool escalation.

The disallowed_tools field is respected when determining write capability — an agent with tools: write + disallowed_tools: write correctly gets read-only memory.

Worktree isolation

Set isolation: worktree to run an agent in a temporary git worktree:

Agent({ subagent_type: "refactor", prompt: "...", isolation: "worktree" })

The agent gets a full, isolated copy of the repository. On completion:

  • No changes: worktree is cleaned up automatically
  • Changes made: changes are committed to a new branch (pi-agent-<id>) and returned in the result
  • Agent committed its own work: the branch is created at the agent's HEAD, preserving its commits (uncommitted leftovers are committed on top first)

The automatic preservation commit uses --no-verify, so local pre-commit hooks can't block it — the commit is local-only and never pushed, and pre-push/server-side hooks still apply.

If the worktree cannot be created (not a git repo, no commits, or git worktree add fails), the Agent tool returns a clear error instead of running unisolated — isolation: "worktree" is a strict guarantee, not a hint. Initialize git and commit at least once, or omit isolation. If cleanup cannot be confirmed, the result keeps the worktree path and includes a bounded diagnostic plus recovery commands. Cleanup first tries git worktree remove --force; when that fails, pi-subagents only uses direct filesystem removal for a verified package-created temporary path, then prunes and verifies Git's worktree registration. It never recursively removes an arbitrary caller-supplied path automatically. At session shutdown, failed removals are retried deepest-first without blocking siblings or ancestors; late provider continuations are quarantined, and remaining failures are reported as immutable path- and repository-specific diagnostics via getWorktreeCleanupFailures().

The child prompt also states that the temporary worktree is the only writable checkout and names the original base repository as off-limits. This prevents inherited parent instructions from sending the agent back into the shared tree.

Skill preloading

Skills can be preloaded by name and injected into the agent's system prompt:

---
skills: api-conventions, error-handling
---

Discovery roots (checked in this order, first match wins). Both project roots are skipped when the child's configuration project is untrusted or trust is unknown; global named skills still preload unless their lexical or canonical source belongs to either the captured denied parent root or the untrusted child configuration root. Skill names are not explicit paths and cannot bypass that decision:

Scope Path Source
Project <cwd>/.pi/skills/ Pi-standard
Project <cwd>/.agents/skills/ Agent Skills spec
User $PI_CODING_AGENT_DIR/skills/ (default ~/.pi/agent/skills/) Pi-standard
User ~/.agents/skills/ Agent Skills spec
User ~/.pi/skills/ Legacy (pre-Pi)

Per root, a skill named foo resolves to the first of:

  • <root>/foo.md — flat file at the top level
  • <root>/foo/SKILL.md — directory skill (top-level)
  • <root>/*/.../foo/SKILL.md — directory skill, found by recursive descent

Recursion skips dotfile directories and node_modules. A directory that itself contains a SKILL.md is treated as a single skill — we don't descend into it. Traversal is byte-order sorted for deterministic resolution across filesystems.

Security: symlinks are rejected at every layer (root, flat file, skill directory, SKILL.md inside a skill directory) — intentional deviation from Pi, which follows symlinks. Skill names with path-traversal characters (.., /, \, spaces, leading dot, >128 chars) are rejected.

Tool denylist

Block specific tools from an agent even if extensions provide them:

---
tools: read, bash, grep, write
disallowed_tools: write, edit
---

This is useful for creating agents that inherit extension tools but should not have write access.

Architecture

src/
  index.ts            # Extension entry: tool/command registration, rendering
  types.ts            # Type definitions (AgentConfig, AgentRecord, etc.)
  default-agents.ts   # Embedded default agent configs (general-purpose, Explore, Plan)
  agent-types.ts      # Unified agent registry (defaults + user), tool name resolution
  agent-runner.ts     # Session creation, execution, graceful max_turns, steer/resume
  agent-manager.ts    # Agent lifecycle, concurrency queue, completion notifications
  cross-extension-rpc.ts # RPC handlers for cross-extension spawn/ping via pi.events
  group-join.ts       # Group join manager: batched completion notifications with timeout
  custom-agents.ts    # Load user-defined agents from .pi/agents/, .agents/agents/, and global agents
  agent-file-toggle.ts # YAML-safe enable/disable, wizard serialization, and agent-file lookup
  memory.ts           # Persistent agent memory (resolve, read, build prompt blocks)
  skill-loader.ts     # Preload skills (Pi-standard + Agent Skills spec layouts)
  output-file.ts      # Streaming output file transcripts for agent sessions
  worktree.ts         # Git worktree isolation (create, cleanup, prune)
  prompts.ts          # Config-driven system prompt builder
  context.ts          # Pure parent-context renderer (not used for child dispatch)
  context-boundary.ts # Raw inheritance and persisted-session guards
  parent-questions.ts # Opt-in clarification broker, authenticated bounded outbox, public preparation/ACK gates
  question-origin.ts # Public manager identity and complete native branch-prefix ownership
  model-runtime-bridge.ts # Checked parent runtime compatibility bridge
  env.ts              # Environment detection (git, platform)
  ui/
    agent-display.ts      # Shared agent formatting: spinner frames, activity, stats, names
    conversation-viewer.ts # Live conversation overlay for viewing agent sessions
    owner-group.ts        # Bounded retained workflow-child inspector (no engine coupling)
    select-item.ts         # Numbered identity-safe selectors for duplicate labels

License

MIT; see LICENSE for the complete notice.