omp packages

@nredd/pi-subagents0.22.0-nredd.7

A pi extension that brings Claude Code-like sub-agents and workflow orchestration to pi — parallel execution, live widget, fleet view, custom agent types, mid-run steering, dynamic workflows, Claude Code compatibility, look and feel.

GitHub Actions Published 4 days ago MIT ~2.8K downloads/mo

Untested on omp

This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.

Did it work for you?

Result

One report per person. A new report replaces your old one.

Reports 0

No reports yet.

Readme

@nredd/pi-subagents

@nredd/pi-subagents is maintained in nredd/pi-harness. It was imported with history from nredd/pi-subagents commit 526fa2df76ba85df31bd14a843fae370e8efe78a, tagged v0.22.0-nredd.5, which is rebased on tintinweb/pi-subagents master commit ca93a5247e72e7053fcd10bea7125d6c4205f949. Original authorship and the MIT LICENSE are retained; subsequent local changes are maintained by nredd.

A pi extension that brings Claude Code-style autonomous sub-agents and workflow orchestration to pi. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level. Run them in the background (the default) or block on them, steer them mid-run, resume completed sessions, and define your own custom agent types. When the orchestration shouldn't be improvised, hand a deterministic JavaScript script to the SubagentWorkflow tool — agent(), parallel(), pipeline() — and scripts written for Claude Code's Workflow tool run here unchanged.

pi-subagents screenshot

https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543

pi-color-badges-white

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 10) and completion notifications that reach the parent at its next boundary, several at once when they finish together
  • Live widget UI — persistent above-editor widget with animated spinners, live tool activity, token counts, and colored status icons. Configurable via /agents → Settings → Widget: all (every agent), background (default — hides foreground runs, which already render inline as the Agent tool result), or off
  • 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 viewer — select any agent in /agents to open a live-scrolling overlay of its full conversation (auto-follows new content, scroll up to pause). In pi's fullscreen mode it is a borderless, full-terminal observer with mouse support. Steer a running agent inline by pressing Enter to open a composer, typing, then Enter to send (Esc or an empty submit returns) — the message appears as a user message and redirects the agent after its current tool. Press b on a running foreground Agent call to hand that run to the background (see Sending a foreground agent to the background). Stop a still-running agent by pressing x (then x again to confirm) — steer and stop both work for background agents too. Assistant text renders as Markdown; m cycles that between off, assistant-only and everything (see Viewer markdown)
  • 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, and Claude Code-compatible colored name badges
  • Package-shipped agents — a pi package can declare its own subagents (and workflow scripts) with "pi": { "subagents": { "agents": ["./agents"] } } in its package.json, so pi install is the whole setup: no copying files into .pi/agents/ on every machine or container start. Only packages pi has actually installed are read (never node_modules), package definitions rank below every agent you wrote, and /agents shows them read-only with Eject and Disable. Narrow or switch it off with packageAgents / packageWorkflows. Full guide
  • 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
  • Agent mentions — subagents are first-class: type @explore also check the RPC path at the prompt and it goes to that agent instead of the main model, without a word of it entering the chat. One syntax covers the whole lifecycle — message it while it runs, resume it once it has finished, reopen its session from disk long after that, or start it if it never ran. Mentioning an agent that isn't running spawns it through an off-screen clone of the conversation, so it gets Claude Code's context-written prompt and a real Agent tool call without a word of it reaching the chat; direct mode starts it here from your text instead, with no model call at all. The orchestrator can name an agent so you address it as @auth-audit, and handles work in steer_subagent/get_subagent_result too. @ completes live agents, resumable ones, and startable types alongside pi's file completion; @main forces text back to the main model. Toggle via /agents → Settings → Agent mentions
  • Scripted workflows — a SubagentWorkflow tool that runs a deterministic JavaScript script orchestrating many subagents: agent(), parallel(), pipeline(), phase(), log() and args, with a pure-literal meta block declaring the phases. pipeline() has no barrier between stages, so one item can be in a later stage while another is still in the first — unlike parallel(), which idles every fast agent until the slowest finishes. Runs in the background with a live card, inspectable via /agents → Workflows or by selecting the run in FleetView. agent() also takes gate: "npm test" to verify a child by running a command (inside its worktree, when isolated) rather than asking another model, and resume: "<label>" to continue a child instead of re-paying its context. Scripts run in a node:vm sandbox on a worker thread where Date.now(), Math.random() and eval throw. On by default, but it stands down for company: if another extension already provides a Workflow or SubagentWorkflow tool, this one warns and disables itself for the session rather than offering the model two orchestrators. Pin it either way with "workflowsEnabled" in subagents.json or /agents → Settings → Workflows. A script written for Claude Code's Workflow tool runs here unchanged: same globals, schema returns a validated object exactly as it does there, budget is present and always reports no token target (pi has no such directive) so its budget.total-guarded patterns still take the branch they were written for, and nested workflow() composes saved workflows one level deep. Full guide
  • Mid-run steering — inject messages into running agents to redirect their work without restarting
  • Session resume — pick up where an agent left off, preserving full conversation context. Resumes detached by default and notifies you on completion, just like a fresh spawn; pass run_in_background: false to block and get the result inline
  • 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. A type that doesn't resolve to exactly one enabled agent — unknown, disabled, or ambiguous between two agents differing only by case — falls back to general-purpose with a note, or is refused outright under fallbackSubagent: none
  • Fuzzy model selection — specify models by name ("haiku", "sonnet") instead of full IDs, with automatic filtering to only available/configured models
  • Context inheritance — optionally fork the parent conversation into a sub-agent so it knows what's been discussed
  • 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
  • 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) and a per-message spend ledger (subagents:usage) emitted via pi.events, enabling other extensions to react to sub-agent activity
  • Cross-extension RPC — other pi extensions can spawn, stop, steer, and join subagents via the pi.events event bus (subagents:rpc:ping, subagents:rpc:spawn, subagents:rpc:stop, subagents:rpc:consume, subagents:rpc:steer). Standardized reply envelopes with protocol versioning. Emits subagents:ready on session start. Full reference
  • Schedule subagents — pass schedule to the Agent tool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results arrive as the same completion notification as a manual background agent; manage via /agents → Scheduled jobs
  • 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

Install

pi install npm:@nredd/pi-subagents

Or load directly for development:

pi -e ./src/index.ts

Requires pi 1.1.0 or newer: pi 0.86 and 0.87 moved the system prompt and conversation history into the session transcript, and the @handle conversation clone and the agent transcript files are built on that model. The peerDependencies range declares it, so npm flags an older pi at install time.

In-process factory and clock

src/index.ts exports createSubagentsExtension(options: SubagentsExtensionOptions = {}): ExtensionFactory. The default export is createSubagentsExtension() and keeps native time and timer reference behavior. For an in-process host, pass createSubagentsExtension({ clock }) as an inline extension. No setting or process-global clock patch is needed. The same clock object can be shared with session-kit and its now method with the subscription router:

import { createSubagentsExtension, type Clock } from "@nredd/pi-subagents/src/index.ts";

// Host-provided clock; use () => clock.now() when passing `now` separately.
declare const clock: Clock;
const extension = createSubagentsExtension({ clock });

SubagentsExtensionOptions has one optional field, clock?: Clock. The exported clock contract matches session-kit's scheduling boundary:

interface TimerHandle { cancel(): void; unref?(): void }
interface Clock {
  now(): number;
  setTimeout(callback: () => void, ms: number): TimerHandle;
  setInterval(callback: () => void, ms: number): TimerHandle;
}

Cancellation wraps native clearTimeout / clearInterval; the supplied clock owns whether its timers are referenced or unreferenced. The default clock does not add or remove unref() calls. Clock callbacks must be deferred, not invoked synchronously by setTimeout / setInterval.

The clock controls relative schedule parsing, ISO validation, one-shot/interval dispatch, quota re-parking, the Agent admission timeout, manager retention and lifecycle timestamps, model-call activity/stall ages, UI repaint tickers and the 200ms idle completion hold. Cron remains owned by Croner, including validation and next-run calculation; this seam deliberately leaves its existing behavior untouched. SubagentScheduler also accepts { clock } directly, and CompletionOutbox accepts it as its third constructor argument. Static detectSchedule(s, now?) and parseRelativeTime(s, now?) accept epoch milliseconds; omitted now uses Date.now().

This is not a whole-extension virtual clock: cron, scheduled re-admission's RPC timeout, group-join holds, workflow execution timers, child-shutdown watchdogs, detached-spool filenames, core scrollbar auto-hide and schedule-store lock retries retain native time. Queued result waits use the manager's start gate when present; legacy queued resumes retain the clock-driven fallback.

npm run check runs Biome, TypeScript, the full Vitest suite and test:bun. The four session scenario files (test/{session-scenarios,fleet-session,viewer-session,worktree-isolation-e2e}.test.ts) run under both Vitest and Bun 1.3.14 on core v1.3.0-nredd.6 release tarballs. They use exported createHarness and createInteractiveHarness/VirtualTerminal, real child sessions and git, one injected extension clock, promise-held tools/streams and raw SGR mouse input. Homes, settings and provider credentials are fixtures; no installed pi, tmux or real-clock sleeps are needed. Existing old-host strict-mode coverage explicitly removes the optional SDK export; the shared faux runtime uses the host's real withBeforeProviderRequest facade. The full older suite remains Vitest-only.

Other hosts

This extension is developed and tested against pi.

Third-party adapters report running it elsewhere. These are maintained independently of this project: not tested here, not covered by our CI, and compatibility may break with any release.

  • DeepSeek Harness (dsh) — via an adapter that maps pi's host API onto native DSH agents. Details and reports: #258

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,
})

Agents run in the background by default: the call returns an ID immediately and notifies you on completion, carrying a preview of the result (use get_subagent_result for the full text). Pass run_in_background: false to block until the agent finishes and get its full output inline; while it blocks, ctrl+b hands it to the background (see below).

Sending a foreground agent to the background

Press ctrl+b while a foreground Agent call is running to hand it to the background, as in Claude Code. The same run keeps going — same session, in-flight turn, transcript and worktree — and the blocked call returns the agent ID with the same result a run_in_background: true spawn gets. Completion then arrives as the normal background notification, naming the original tool call. After 2 seconds the running call's row shows (ctrl+b to run in background).

  • ctrl+b hands off every running foreground Agent call at once. To hand off just one, press b in its conversation viewer (FleetView or /agents → Running agents).
  • The key is claimed only while such a call is running and the prompt editor has focus; the rest of the time — and in dialogs or the viewer's steering composer — ctrl+b is still pi's cursor-left. With FleetView disabled the extension cannot see which component has focus, so it claims the key whenever a call is running.
  • One hint shows per batch of parallel calls, on the earliest one still running.
  • Esc in the parent no longer reaches a handed-off agent. Stop it from /agents → Running agents with x.
  • It is one-way: there is no way back to foreground.
  • Other tool calls in the same assistant message still hold the model's next turn until they finish; pi runs a message's tool calls as one batch.
  • Not offered for a foreground call still queued for a slot, a foreground resume, a workflow's agents or nested children. There is no automatic timeout.

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 the same way as a manually-spawned background agent's — your parent agent reasons about the result the same way. The job stores the resolved thinking level when you create it: an explicit Agent({ thinking }) wins over the agent definition then, while an omitted value keeps the definition's default. Editing the agent file later does not replace that stored level.

Session switches. /new, /resume, /fork and /reload do not stop running background agents. They keep showing in the fleet list of the reloaded session while they run (open, stop and steer work from there), finish on their own, and the session that spawned them gets the completion notification when you return to it (immediately if it is already open). Held notices survive the switch too: unread results are transferred to the session spool and re-enter the same boundary outbox on return, without repeating their history entries. The preview is in the notification; the full output is in the transcript file it names, since get_subagent_result cannot reach an agent from a previous session. Quitting pi still aborts everything.

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).
  • run_in_background: false is refused — scheduled jobs always run in the background. Omitting it, or passing true, is fine.
  • 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.

Quota admission

With a subscription router installed (e.g. pi-subscription-router), a fresh Agent dispatch first asks it whether the model is out of included quota, over pi.events: router:rpc:admit { requestId, model: "provider/id" }, replied on router:rpc:admit:reply:<requestId> as { success, data: { ok, resolvedModel?, resetAt?, wait? } } (resolvedModel is ignored; wait: false is the router's exhaustion policy saying fail now, absent means wait).

  • Admitted, or no router answers within 100ms: the agent starts as usual
  • Refused, background, reset known, wait not false, scheduling on: the dispatch is parked as a one-shot scheduled job a minute after the reset, persisted like any schedule. When it fires it asks again and re-parks at the new reset while still blocked; if the refusal can no longer park (wait: false, no reset, or a reset already past) the job is not started and ends with lastStatus: "error" and an error event. Only an explicit admission, or no router answering, starts it. If pi was closed through the reset, it asks again 10s after the session starts, once the router has warmed its usage cache
  • Refused otherwise (foreground, inherit_context, scheduling off, unknown reset, or wait: false): the call fails with the reset time and no agent starts

Resumes, cron and interval jobs, and RPC or workflow spawns are not gated. A router/* virtual model still routes each request inside the child.

UI

The extension renders a persistent widget above the editor showing active agents. By default it shows background runs only (widgetMode: background) — foreground agents already render inline as the Agent tool result, so the widget would otherwise double-render them. Switch to all (every agent) or off (hide the widget) via /agents → Settings → Widget:

● Agents
├─ ⠹ Agent  Refactor auth module · ↻5≤30 · 5 tool uses · 33.8k token (62%) · 12.3s
│    ⎿  editing 2 files…
├─ ⠹ Explore  Find auth files · ↻3 · 3 tool uses · 12.4k token (8%) · 4.1s
│    ⎿  searching…
├─ ⠹ Agent  Long-running task · ↻42 · 38 tool uses · 91.0k token (84% · ⇊2) · 2m17s
│    ⎿  reading…
└─ 2 queued

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.
  • ⇊N — number of times the session has compacted, when > 0. Stays dim; the percent's color carries urgency.

The Agent tool row

pi 1.0 folds every collapsed tool row to <first line of the call> · <first line of the result> and draws the ▸/▾ marker itself, so the Agent row's call and result each put what matters on their first line and draw no marker:

Plan  design the cache · ⠹ thinking · 2m41s · 12s · 3.2k token      collapsed, running
Plan  design the cache · ✓ done · 41s · 33.8k token                 collapsed, finished

The status, elapsed time and tokens are read from the agent record on every render (and the host is asked to repaint once a second while it runs), so the row keeps moving after a background launch instead of freezing on Running in background. Expanded, the row shows the prompt, the model and thinking tags (↳ opus 4.6 · thinking: high), the transcript link, then the result.

FleetView

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

  esc to interrupt · ← for agents · ↓ to manage

  ● main
  ○ workflow         audit-src                    12/40 agents · 32s · ↓ 26.4k tokens
  ○ general-purpose  Sleep then report 1                                11s · ↓ 13.1k tokens
  ○ general-purpose  Sleep then report 2                                11s · ↓ 13.1k tokens
                                                                                   ↓ 3 more

Running workflows appear as a single workflow row above the agents, carrying their agent counts in place of a description. Enter on one opens the same two-pane inspector /agents → Workflows does, rather than a conversation overlay. A run's own agents are not listed separately — they belong to the run, which reports for them, so they are filtered out of the fleet list, the above-editor widget, the /agents menus and @handle resolution exactly as nested children are. They are also outside the maxConcurrent pool: the run has its own concurrency cap, and routing a fan-out through the session pool as well would let one workflow starve everything else. The agents are ordered earliest-launched first, and only agents you can actually open are shown (pending/queued agents with no session yet appear once they start). At an empty prompt, press ↓ (or ←) to move focus from the prompt into the list — the selected row is marked ●, the rest ○. The row block stays at five lines once the roster reaches five rows; overflowing lists show three rows plus ↑ N more / ↓ N more indicators (blank at an edge), without moving the content below it. The selected row renders in the theme's primary text color rather than the muted/dim treatment of the others; an agent with a configured color shows its badge there too, bolded. ↑/↓ move the selection, Enter opens the selected agent's live conversation overlay (it auto-updates as the agent works), and Esc (or ↑ above main) returns to the prompt. Selecting main returns to the normal view. Inside the overlay, press Enter to steer the running agent — type a message and Enter to send it (Esc or an empty submit returns), and it redirects the agent the same way the steer_subagent tool does. On a running foreground Agent call, press b to hand that run to the background; the hint only shows where the handoff is available. 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 arrow keys when the prompt is empty. Disable it entirely via /agents → Settings → Fleet view. With showModel on, each row names the model and thinking level it runs at (sonnet 4.6 · thinking: high · 11s · ↓ 13.1k tokens), and a running row says what its in-flight model call is doing (see Live activity). On a virtual model such as router/default the row reads router default (routing) until the first response, then the physical model that answered. The thinking level shown is the session's selected level; a router chain entry's own thinking override is not visible to it.

Fullscreen conversation observer

In fullscreen mode (pi's default), opening an agent from FleetView, /agents, or the workflow inspector fills the terminal. Regular mode keeps the centered floating viewer and keyboard controls. Neither mode replaces the main session or pauses either agent. Escape returns to the previous view; while composing, the first Escape cancels the draft.

In pi's /settings → TUI mode → fullscreen, the observer supports:

  • Mouse-wheel scrolling. Scrolling up pauses automatic following; reaching the bottom resumes it.
  • A single-column scrollbar using pi's │ track, ┃ thumb, and theme colors. The thumb becomes █ while hovered or dragged. Track clicks page; holding and moving also drags.
  • pi's /settings → Fullscreen scrollbar preference applies to the observer: auto appears on scrolling or hovering and hides after one second of inactivity; always reserves a column and stays visible even when the conversation fits; hidden removes the rail and its mouse target. The observer reads the main session's settings when opened, without changing them.
  • Clickable footer controls for steering, stopping, Markdown mode, and closing. Stopping still takes two presses, as with x.
  • Keyboard navigation and steering as before. Mouse scrolling also works while composing a steering message.
  • The bottom divider stays pinned above the footer (and steering composer when open). While scrolled away, a centered [ ↓ N new messages · Ctrl+End ] indicator within the divider counts new conversation messages, not streaming updates. Before any arrive, it reads [ ↓ Jump to latest message · Ctrl+End ]. Click it or press Ctrl+End to resume following and clear the count, without losing an open steering draft.

Observer mouse events are consumed, including at scroll limits, so they do not scroll, select, or click the main view underneath. Closing restores focus without editing the main prompt. The main conversation can still advance as its agent runs.

This is an initial implementation. Mouse text selection, copy-on-select, link activation, and switching agents without leaving the observer are not implemented. pi's global shortcuts remain host-owned: its fullscreen search searches the main transcript, not the observer. The scrollbar, jump indicator, and mouse controls apply only to fullscreen mode; regular mode retains the original viewer.

Live activity

While a model call is in flight, the widget, the fleet row and the conversation viewer say what it is doing instead of thinking… for as long as it takes:

Shown Meaning
thinking · 2m41s the call has been thinking (no text yet) for 2m41s
writing · 3.2k tok it is writing text or tool-call arguments; the figure is an estimate (characters / 4) until the provider reports usage
stalled? · 2m41s no stream event of any kind for 2 minutes (the time is the silence, not the call)

Tools running take precedence (reading…, running command…). Activity is tracked for spawns made through the Agent tool, RPC and mentions, and for foreground and background resumes. Each resume gets fresh text/model-call tracking and turn counts; physical-model labels follow the assistant messages.

Conversation viewer input

The fullscreen observer uses pi's wheel delta, draggable scrollbar and clickable footer controls (see above). Regular mode keeps the centered floating box and keyboard controls. Both stream the reply being written before it lands in the transcript, and repaint once a second while the agent runs so activity timers count.

Agent mentions

Subagents are addressable. Every agent has a typeable handle — the agent type, lowercased, numbered when instances collide (explore, explore-2) — and @handle <message> at the prompt talks to that agent, whatever state it happens to be in. Type @ to pick one:

❯ @
  @auth-audit     send message · Explore · running · audit the auth flow
  @explore-2      send message · running · find flaky tests
  @code-review    resume · code-review · check the diff
  @plan           start agent · Software architect agent for designing implementation plans.
  index.ts        src/index.ts                        ← pi's own file rows, still there
  index.d.ts      dist/index.d.ts

The handle names the agent, not one process, so a single syntax covers its whole lifecycle:

State @explore fix the flaky test does
running or queued sends the message into its conversation, exactly as steer_subagent would
finished resumes it in the background from its existing session, continuing where it left off
finished long ago, record gone reopens its session from disk and continues there
never started starts it — by default, through a clone of this conversation (below)

No turn is ever spent in the main conversation, and nothing about the mention enters the chat. The answer comes back as the ordinary background-completion notification, so the main model can relay it.

Starting a new agent

Claude Code does not start a mentioned agent itself. @agent-<type> becomes an attachment appending a <system-reminder> to your prompt — "the user has expressed a desire to invoke the agent X; please invoke the agent appropriately, passing in the required context to it" — and the main model makes the tool call. There is no tool forcing and no allowed-tools narrowing: the mention constrains which agent, not what it is told. So the model writes the agent's prompt, giving it the conversation context a cold spawn lacks.

The cost is a visible turn — the model's reasoning and its tool block, narrating a decision you already made by typing the handle. This extension keeps the mechanism and moves it off-screen. The conversation is copied into a throwaway in-memory session, that clone takes the turn holding only the Agent tool, and what it starts is an ordinary top-level agent:

@cyan whats your favorite color        →  (nothing in the chat)
  └─ clone of this conversation, off-screen
       └─ Agent(subagent_type: "cyan", prompt: …)
            ▸ Cyan Agent   favorite color        ← widget, fleet row, handle

It is a literal clone — the session's own entries and the same system prompt, not inherit_context's text rendering of them — taken from memory and compaction-aware, so what the copy reads is what the main model is working from. The clone gets one tool and one job; it cannot read, write or run anything, because an invisible turn with the full toolset could do invisible work. The agent it starts is attributed to the real session, so its transcript and rootSessionId land where they would have anyway, and it carries no tool-use-id — the main conversation never issued one.

Mode @plan sketch the migration, with no Plan agent running
model (default) a clone of this conversation takes the turn off-screen and calls Agent, so the agent starts with a prompt written from the conversation. Nothing reaches the chat but a Prompting @plan… toast — the wording marks the wait for that turn, where direct's Started @plan means it is already running
direct the agent starts here, immediately, with your message verbatim as its prompt. No model call at all, so no latency before it begins
off @ means only "attach a file" again

Either way the started agent honours its own frontmatter — model:, thinking:, max_turns: all apply, since neither path passes them and the agent's config wins. Mentioning something as the very first thing in a session works: there is simply no history to carry, and the clone still runs on your model and system prompt. If it cannot deliver at all — a model can always answer in prose instead of calling the tool — the agent is started directly with your text and the toast says so, rather than leaving you with nothing running.

model is also the only mode that works outside the TUI: pi -p '@plan the migration' clones, spawns, and reports through the normal completion path, where a direct start would have detached the agent and printed nothing. Messaging and resuming stay TUI-only for that reason, in both modes.

Two things to weigh against direct: the clone re-sends the whole conversation, and the agent does not start until that turn finishes.

Named agents. The Agent tool takes an optional name, so the orchestrator can call one auth-audit instead of leaving you to tell @explore-2 from @explore-3. A name is additive: the type-derived handle is still assigned, so @explore keeps reaching that agent rather than starting a second one beside it. Both names share one namespace — an alias can never shadow a live handle or the reverse — and the popup shows one row per agent, under its alias, with the type moved into the description. steer_subagent and get_subagent_result accept a handle too, so you and the model address agents the same way.

Resuming much later. Because subagent sessions are persisted by default (rememberAgents), a handle keeps working after the agent's in-memory record is evicted: @explore anything else? reopens the conversation from disk. Only the definition is re-resolved, so a continuation runs under the agent type's current frontmatter, not the one the first run used. If the type has since been deleted or disabled, the resume is refused rather than falling back to another agent — re-enable it and the handle works again. Names from an evicted agent stay reserved, so a later Explore becomes explore-2 rather than shadowing something you can still reach; the 100 most recent are kept, and all of them are forgotten on /new and session switch. A resumed agent takes those names back, so @explore keeps meaning the same conversation. An agent whose session was only ever in memory leaves nothing to reopen, and the mention starts a fresh one instead; if the session file has since been deleted, the mention says so and frees the handle rather than silently sending your message to a new agent.

The grammar mirrors Claude Code's, and is deliberately narrow so nothing gets swallowed by accident:

Input Goes to
@explore fix the flaky test the explore agent
@agent-explore fix the flaky test the same agent — Claude Code's manual spelling, accepted as a synonym
@main @explore is not a mention the main model, with @main stripped — the escape hatch
@explore (no message) the main model — a bare handle is never a send
hey @explore look at this the main model — only a leading mention is routed
@src/index.ts summarize this the main model, with pi's normal file attachment
@nosuchagent hello the main model, verbatim — no agent, no type, no interception

While an agent is live its handle addresses it, so @explore never starts a second Explore alongside a running one — use the Agent tool for deliberate parallelism. @<agent-id> works too. main is reserved and can never be an agent's handle (a type slugging to it gets main-2); handles are capped at 64 characters. A handle written as typed always wins over the @agent- form, so an agent genuinely called agent-explore stays reachable. Nested subagents are not addressable — they are hidden from every top-level surface and only their owner may steer them, so a handle that would name one starts a fresh top-level agent instead of reaching through that boundary. Suggestions list live agents first, then resumable ones, then startable types — and then pi's own file rows, in the same popup: @ stays the file picker it always was, and the handles are added to it rather than replacing it. Disable the whole thing via /agents → Settings → Agent mentions.

A direct-mode start takes the non-tool spawn path shared with the scheduler and cross-extension RPC, so — like those — it writes no .output transcript. That is the trade for skipping the model call: a model-mode start goes through the real Agent tool and keeps everything. Live tool activity and the turn counter are not part of that trade — a direct start renders them like any other agent. A mention-resumed agent goes through the full resume wiring and keeps both in either mode.

Individual agent results render Claude Code-style in the conversation:

State Example
Running ⠹ ↻3≤30 · 3 tool uses · 12.4k token (8%) / ⎿ searching, reading 3 files…
Completed ✓ ↻8 · 5 tool uses · 33.8k token (62%) · 12.3s / ⎿ Done
Wrapped up ✓ ↻50≤50 · 50 tool uses · 89.1k token (84% · ⇊2) · 45.2s / ⎿ Wrapped up (turn limit)
Stopped ■ ↻3 · 3 tool uses · 12.4k token (8%) / ⎿ Stopped
Error ✗ ↻3 · 3 tool uses · 12.4k token (8%) / ⎿ Error: timeout
Aborted ✗ ↻55≤50 · 55 tool uses · 102.3k token (95% · ⇊3) / ⎿ Aborted (max turns exceeded)

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 collapse to one line, with pi 1.0 drawing the ▸/▾ marker and toggling on click (the renderers draw none of their own):

✓ Find auth files · 3 tools · 12.4k token · ~$0.0042 · 4.1s          collapsed
3 agents finished · ✓ a, ✓ b, ✗ c                                   collapsed, grouped

Expanded, a notice adds ↻3 · 3 tools · 12.4k token · 4.1s, the result preview, and the transcript as an OSC 8 hyperlink labelled transcript ↗ (the path follows it as plain text). A group expands to one block per agent, plus a total when showCost is on. 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 frontmatter name: is the subagent_type and dispatch identity, falling back to the filename when absent; display_name only changes the UI label. Claiming a default agent's name overrides it.

Agents are discovered from four 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
4 (lowest) Paths declared by an installed pi package Package — see Shipping agents in a pi package

Project agents need project trust. .pi/agents/ and .agents/agents/ are project-supplied, so they load only when pi trusts the project (/trust, a saved decision, defaultProjectTrust: "always", or --approve). In an untrusted project (including --no-approve) both roots are skipped unread: they cannot override or disable a global or package agent, and a malformed file there neither warns nor aborts a strictAgentFiles startup. Global agents and packages configured globally stay available; packages configured only by the project's .pi/settings.json are gated by the same decision. The decision is pi's canonical one, ctx.isProjectTrusted() at session_start (so --approve/--no-approve and /trust override a saved trust.json), which this extension never reads itself. Activation runs before that answer exists, so it loads global and globally configured package agents only; the first session_start loads the project roots and re-registers the Agent tool so its description and subagent_type list match, before any provider turn. This gates reading project agent definitions (and a project agent-tool-description.md); the extension still reads operational project settings such as .pi/subagents.json before trust is known. A nested subagent applies its parent's decision, including after a spawn with a custom cwd: the decision for the parent's config root travels with that root through every later generation, and never extends to the custom working directory. The /agents menu writes new project agents to .pi/agents/, which an untrusted project does not list. Trusted projects keep the order above.

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 the upstream PI_CODING_AGENT_DIR env var — set it to relocate all pi-coding-agent state (agents, skills, settings) to a custom directory. An agent's name is its frontmatter name:, falling back to the filename, so two files can now claim the same one — the later load wins, and the warning below names the file that took over.

An unreadable or unparseable agent file is skipped, not fatal — a warning names the file and the error. If it was overriding a same-named agent, a second line names the file that loads instead. Set strictAgentFiles: true in subagents.json (or /agents → Settings → Strict agent files) to fail startup on a broken file instead; mid-session reloads still only warn. Strict validation runs in two phases. At activation, a malformed global agent file fails the factory as an ordinary load error; a malformed file from a package (even one configured globally) only warns, since it is a third party's file and not the user's to fix. At the first canonical session_start, once trust is known, the trusted project roots are loaded and validated, and a malformed file there throws the host's ExtensionStartupError (an untrusted project's files are never read, so they never trip it). The host class is needed for the second phase: on an older host with strictAgentFiles: true the extension refuses at activation with an upgrade message, before any agent file is read, rather than degrading to warnings. Non-strict use works on any host in the peer range.

Example: .pi/agents/auditor.md

---
color: red
description: Security Code Reviewer
tools: read, grep, find, bash
model: 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
name filename The agent's type — what subagent_type and @handle address. Claude Code's rule: the filename doesn't have to match, so blubb.md with name: code-review dispatches as code-review. Omit it and the filename is used. Any value works except one containing :, which Claude Code reserves for plugin-scoped identifiers — such a file is skipped with a warning. Two files may declare the same name; the later load wins, as a filename clash always did
display_name the type Label shown in the UI (widget, agent list, badges) — cosmetic only, and independent of name. Claude Code has no equivalent; a file that sets only name badges as its type, unchanged
color — Background color for the agent name badge in the Agent tool header, widget, FleetView, and conversation viewer. Supports Claude Code's red, blue, green, yellow, purple, orange, pink, cyan (the values its own default theme uses); quoted six-digit hex such as "#8B5CF6"; and Agency Agents aliases (amber, teal, indigo, gold, neon-green, neon-cyan, metallic-blue, violet, rose, lime, gray/grey, fuchsia, slate, navy). Badge text is black or white, whichever clears 4.5:1 against the rendered background — Claude Code uses one inverse color for every badge. Invalid values render no badge and preserve each surface's existing theme foreground
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 defaults), false (none), or an explicit list: [mcp, "/abs/path.ts", "*"]. 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 inherits the parent's skills; false inherits none. A comma-separated list preloads only those skills into the system prompt and does not inherit the rest (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
isolation — Set to worktree to run in an isolated git worktree, or off to refuse one even when the caller passes isolation: "worktree" (frontmatter is authoritative). Workflow scripts are the exception: as in Claude Code, only their agent() option decides. none, no, and false are accepted spellings of off
model inherit parent Default model — provider/modelId, machine-local alias, or fuzzy name ("haiku", "sonnet"). An explicit Agent({ model }) selection overrides this default. Resolved tolerantly (./- and a trailing date stamp are interchangeable) and falls back to the same model under another provider if the named one doesn't have it
thinking inherit Default thinking level: off, minimal, low, medium, high, xhigh, max — an explicit Agent({ thinking }) overrides it for that run. Actual availability depends on your pi version and model; pi clamps unsupported levels down
max_turns unlimited Max agentic turns before graceful shutdown. 0 or omit for unlimited
persist_session subagents.json rememberAgents (default true) Persist this subagent as a normal pi session instead of keeping the session in memory only; overrides the rememberAgents project default in both directions. It records its spawning session as parent, so it nests under it in /resume. 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. A session outside the parent's session directory (this override, or isolation: worktree) is listed separately, so it shows as a root instead of nesting
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 Fork parent conversation into agent
run_in_background — Pin this agent to background (true) or foreground (false). Omit to follow backgroundByDefault
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 for max_turns, inherit_context, run_in_background, isolated, and isolation. An agent file's model and thinking are portable defaults: an explicit Agent({ model, thinking }) selection overrides either for that run. With no explicit thinking level, the definition wins, then the parent session supplies the fallback; model selection additionally follows the agentModels precedence. Other Agent tool parameters only fill fields the agent config leaves unspecified. Other spawn paths differ per field:

  • isolated holds on every spawn path, including @handle mentions, workflows and cross-extension RPC.
  • isolation holds on every path except workflows: as in Claude Code, only the script's agent() option decides there.
  • inherit_context applies only to Agent tool and nested spawns, including default-mode @handle mentions, which spawn through the Agent tool. As in Claude Code, where only a fork inherits the conversation, every other spawn starts fresh unless its caller asks: workflow children and scheduled runs never inherit, and an RPC caller opts in with inheritContext.

Fuzzy names pick the newest model. A bare family name such as "opus" matches several ids; exact ids and aliases still win first, otherwise the candidates are narrowed to pi's enabledModels (when any candidate is enabled), then ranked by parsed version, newest first, then by match score. Versions are read from claude-<family>-<n>[-<n>] (claude-opus-4-6, claude-opus-5; a trailing -YYYYMMDD is a date, not a minor) and gpt-<n>[.<n>]-<variant> (gpt-6.1-sol); ids with no recognised scheme rank after every versioned one. Previously the shortest id won, so "opus" resolved to claude-opus-5 even with claude-opus-4-6 enabled and claude-opus-5 not.

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 matches the enabledModels allowlist by exact entry.)

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.

No background work inside a subagent. A nested Agent call always blocks and returns the child's result inline. run_in_background: true returns an error (a frontmatter run_in_background: true on the nested type is ignored and the child runs in the foreground), and the workflow / Workflow / SubagentWorkflow tools, including another extension's, return an error explaining why when a subagent calls them. A child gets a refusing SubagentWorkflow stand-in for this (one short tool definition), since the real tool is excluded and core would otherwise answer "Tool SubagentWorkflow not found" before the refusal could run; an isolated or extensions: false child gets no workflow tool at all. Both would deliver their completion to a session that ends the moment the subagent returns its answer, and a detached child is aborted with its owner anyway.

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 occupy no slot in either per-session pool — their parent already holds one, and queueing them behind it would deadlock a parent waiting on its own child. They do take a lease from the process budget when it is on, which reserves slots for them so that cannot deadlock. 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.

Depth-2 review delegation. The default depth already allows main session (0) → implementation agent (1) → reviewer (2). Give the implementation agent an explicit allowlist naming only the reviewer, and leave the reviewer without allowed_subagents, so it is a leaf:

---
# ~/.pi/agent/agents/implementer.md
allowed_subagents: code-reviewer
---

The implementer then calls Agent({ subagent_type: "code-reviewer", ... }) in the foreground and gets the review inline. With the process budget on, its slot stays held while the reviewer runs, and the reserve keeps reviewers startable when every top-level slot is an implementer waiting on one.

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.

Tool & extension scoping

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

# Default (both omitted): all 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: [builtin:mcp, builtin:codemode]  # pi's built-in MCP + codemode
extensions: ["*", "/abs/foo.ts"]  # all defaults plus one path-loaded extension

exclude_extensions: pi-notify     # everything except pi-notify (with extensions: true)
exclude_extensions: builtin:mcp   # everything except pi's built-in MCP

# 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 sole loading authority. 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.
  • pi's built-in extensions (builtin:mcp, builtin:codemode, builtin:tool-search) load in subagents as they do in the main session, so with extensions: true every subagent starts its own set of mcp.json servers, which stay up until the subagent's session is disposed (on quit, or ~10 minutes after it finishes). Address them by the full builtin: name — bare [mcp] matches a third-party extension named mcp, not the built-in — and narrow their tools with ext:builtin:mcp/<tool>. MCP tools are reached through codemode (or tool_search for deferred servers), so an ext: allowlist must name that bridge too, as pi's --tools read,codemode,'mcp__*' does: tools: "read, ext:builtin:codemode, ext:builtin:mcp/mcp__radius__search". Opt out per agent with exclude_extensions: builtin:mcp, or everywhere with -builtin:mcp in pi's extensions setting; starting pi with --no-mcp keeps MCP out of subagents too, and --no-extensions keeps all three built-ins out. MCP tools stay undeclared until codemode or tool_search reaches them, as in the main session, and a codemode script can only call tools inside the agent's scope. Out-of-scope tools are blocked but can still appear in codemode's searchTools() and tool_search results.
  • Subagents inherit the parent session's project trust: in a project you did not trust, a subagent loads none of its .pi extensions or settings and never starts the servers in its .pi/mcp.json. A subagent spawned into anot