@nredd/pi-subagents 0.22.0-nredd.8 → 0.22.0-nredd.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -55
- package/package.json +14 -11
- package/src/abortable.ts +1 -1
- package/src/agent-file-toggle.ts +0 -1
- package/src/agent-manager.ts +360 -103
- package/src/agent-runner.ts +121 -63
- package/src/child-context.ts +19 -1
- package/src/completion-outbox.ts +22 -3
- package/src/cross-extension-rpc.ts +10 -6
- package/src/custom-agents.ts +2 -1
- package/src/default-agents.ts +19 -17
- package/src/detached-results.ts +18 -2
- package/src/enterprise-local.ts +2 -1
- package/src/error-message.ts +11 -0
- package/src/git-path.ts +66 -0
- package/src/group-join.ts +4 -10
- package/src/index.ts +731 -231
- package/src/invocation-config.ts +1 -5
- package/src/mention-clone.ts +4 -3
- package/src/model-resolver.ts +51 -14
- package/src/model-selection.ts +17 -6
- package/src/model-tiers.ts +5 -0
- package/src/nested-tools.ts +80 -21
- package/src/notification-result.ts +118 -0
- package/src/plan-mode-children.ts +176 -0
- package/src/prompts.ts +7 -2
- package/src/quota-admission.ts +16 -6
- package/src/resource-loader.ts +20 -6
- package/src/schedule.ts +65 -19
- package/src/settings.ts +21 -1
- package/src/skill-loader.ts +244 -94
- package/src/status-note.ts +28 -5
- package/src/types.ts +52 -2
- package/src/ui/agent-mention.ts +5 -2
- package/src/ui/agent-widget.ts +41 -22
- package/src/ui/conversation-viewer.ts +4 -2
- package/src/ui/fallback-flag.ts +28 -4
- package/src/ui/fleet-list.ts +89 -54
- package/src/ui/notification-view.ts +4 -1
- package/src/ui/workflow-card.ts +1 -8
- package/src/ui/workflow-dialog.ts +31 -18
- package/src/workflow/host.ts +4 -5
- package/src/workflow/meta.ts +7 -4
- package/src/workflow/progress.ts +4 -3
- package/src/workflow/runtime.ts +23 -7
- package/src/workflow/saved.ts +2 -4
- package/src/workflow/task.ts +12 -7
- package/src/workflow/tool-description.ts +2 -2
- package/src/workflow/worker-source.ts +6 -2
- package/src/worktree.ts +309 -19
package/README.md
CHANGED
|
@@ -14,14 +14,14 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
|
|
|
14
14
|
|
|
15
15
|
## Features
|
|
16
16
|
|
|
17
|
-
- **Claude Code look & feel** — same tool names, calling conventions, and UI patterns (`Agent`, `get_subagent_result`, `steer_subagent`) — feels native
|
|
17
|
+
- **Claude Code look & feel** — same tool names, calling conventions, and UI patterns (`Agent`, `get_subagent_result`, `steer_subagent`, `stop_subagent`) — feels native
|
|
18
18
|
- **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
|
|
19
19
|
- **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`
|
|
20
|
-
- **FleetView** — one-line summary below the editor by default; `ctrl+shift+a` expands the Claude Code-style navigable list of `main` + every running subagent (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`
|
|
20
|
+
- **FleetView** — one-line summary below the editor by default; `ctrl+shift+a` expands the Claude Code-style navigable list of `main` + every running subagent (earliest-launched first). `ctrl+shift+k` twice within 3s stops every running agent and workflow. 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`
|
|
21
21
|
- **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](#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](#persistent-settings))
|
|
22
22
|
- **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
|
|
23
23
|
- **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](https://github.com/tintinweb/pi-subagents/blob/master/docs/packages.md)**
|
|
24
|
-
- **Nested subagents** — opt-in, default-off delegation: a custom agent that sets `allowed_subagents` gets its own ownership-scoped `Agent`, `get_subagent_result`, and `
|
|
24
|
+
- **Nested subagents** — opt-in, default-off delegation: a custom agent that sets `allowed_subagents` gets its own ownership-scoped `Agent`, `get_subagent_result`, `steer_subagent`, and `stop_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
|
|
25
25
|
- **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`
|
|
26
26
|
- **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](https://github.com/tintinweb/pi-subagents/blob/master/docs/workflows.md)**
|
|
27
27
|
- **Mid-run steering** — inject messages into running agents to redirect their work without restarting
|
|
@@ -32,8 +32,9 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
|
|
|
32
32
|
- **Context inheritance** — optionally fork the parent conversation into a sub-agent so it knows what's been discussed
|
|
33
33
|
- **Persistent agent memory** — three scopes (project, local, user) with automatic read-only fallback for agents without write tools
|
|
34
34
|
- **Git worktree isolation** — run agents in isolated repo copies; changes auto-committed to branches on completion
|
|
35
|
-
- **Skill preloading** — inject named skills into agent system prompts,
|
|
35
|
+
- **Skill preloading** — inject named skills into agent system prompts through Pi's configured resource discovery, including installed package skills; project resources participate only after project trust and package/user precedence stays Pi-native
|
|
36
36
|
- **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
|
|
37
|
+
- **Stops are reported** — a run that settles while the session's own agents are still running or queued shows the count and what is armed to wake the parent, e.g. `Run ended with 3 agents running or queued, armed: 2 agent completion notices wake the parent; 1 foreground or RPC-spawned agent will not`. Background agents (`Agent` with `run_in_background`, `@handle` mentions, scheduled jobs) wake it in every join mode; foreground agents and RPC spawns without `isBackground` notify nobody, and when only those are pending the notice is a warning with `armed: no agent completion notice wakes the parent`. It speaks for agents only, beside the monitor and todo notices. Two edges: after a session switch the notice is spooled and lands when that session is live again, and a held notice can be dropped when `pi -p` tears down. Nested and workflow children are not counted
|
|
37
38
|
- **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
|
|
38
39
|
- **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
|
|
39
40
|
- **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](https://github.com/tintinweb/pi-subagents/blob/master/docs/rpc.md)**
|
|
@@ -103,14 +104,26 @@ holds, workflow execution timers, child-shutdown watchdogs, detached-spool filen
|
|
|
103
104
|
auto-hide and schedule-store lock retries retain native time. Queued result waits use the manager's
|
|
104
105
|
start gate when present; legacy queued resumes retain the clock-driven fallback.
|
|
105
106
|
|
|
106
|
-
`npm
|
|
107
|
+
`npm test` and `test:coverage` launch Vitest through the shared
|
|
108
|
+
`scripts/run-vitest.ts` launcher. It gives the child a temporary `HOME`, `USERPROFILE`,
|
|
109
|
+
`XDG_CONFIG_HOME`, `PI_CODING_AGENT_DIR` and `PI_SUBAGENTS_CONFIG_DIR` before imports,
|
|
110
|
+
then removes that home on exit. Operator `agentModels`/model-tier policy must not affect
|
|
111
|
+
fixture dispatch; runtime tier defaults and operator files remain unchanged. The command-level
|
|
112
|
+
regression runs the real `npm test` against a hostile fixture home with an unavailable
|
|
113
|
+
`@default` tier and checks the existing Agent boundary assertions. `test:watch` and
|
|
114
|
+
`test:e2e` retain their operator environment. The explicit live smoke command
|
|
115
|
+
`PI_E2E_LIVE=1 npm run test:e2e` needs the operator's configured Pi login; it is not a
|
|
116
|
+
hermetic verification gate. Without that opt-in, `test:e2e` runs the scripted faux suite.
|
|
117
|
+
|
|
118
|
+
`npm run check` runs Biome, TypeScript, the full Vitest suite and `test:bun`, the same full suite on Bun 1.4.4 through
|
|
119
|
+
`scripts/run-vitest.ts bun` (no native `bun test` subset). The four session scenario
|
|
107
120
|
files (`test/{session-scenarios,fleet-session,viewer-session,worktree-isolation-e2e}.test.ts`) run under
|
|
108
|
-
both
|
|
121
|
+
both engines on core `v1.3.0-nredd.10` release tarballs. They use exported `createHarness`
|
|
109
122
|
and `createInteractiveHarness`/`VirtualTerminal`, real child sessions and git, one injected extension
|
|
110
123
|
clock, promise-held tools/streams and raw SGR mouse input. Homes, settings and provider credentials
|
|
111
124
|
are fixtures; no installed pi, tmux or real-clock sleeps are needed. Existing old-host strict-mode
|
|
112
125
|
coverage explicitly removes the optional SDK export; the shared faux runtime uses the host's real
|
|
113
|
-
`withBeforeProviderRequest` facade.
|
|
126
|
+
`withBeforeProviderRequest` facade. Maintainer guidance on public index callback
|
|
114
127
|
coverage and retained safety guards is [here](../../docs/subagents-index-continuation.md).
|
|
115
128
|
|
|
116
129
|
### Other hosts
|
|
@@ -134,7 +147,7 @@ Agent({
|
|
|
134
147
|
})
|
|
135
148
|
```
|
|
136
149
|
|
|
137
|
-
Agents run in the background by default: the call returns an ID immediately and notifies you on completion, carrying
|
|
150
|
+
Agents run in the background by default: the call returns an ID immediately and notifies you on completion, carrying the whole result up to [`notificationResultChars`](#completion-notice-results) (16k chars by default; past that the head and tail, with `get_subagent_result` for the rest). 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).
|
|
138
151
|
|
|
139
152
|
### Sending a foreground agent to the background
|
|
140
153
|
|
|
@@ -170,7 +183,7 @@ Schedule formats:
|
|
|
170
183
|
|
|
171
184
|
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. Model selection runs at fire time with the manual-dispatch precedence: stored `model` request (including a captured configured tier), current `agentModels[type]`, `agentModels["*"]`, definition, parent. A stored explicit model that no longer resolves or is out of scope fails the job instead of being replaced. Tier jobs retain the `@tier` request, including a configured tier, and re-resolve its binding at fire time. Other quota-parked jobs retain the original explicit input, not a snapshot of the configured or parent model. 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.
|
|
172
185
|
|
|
173
|
-
**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
|
|
186
|
+
**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 notification carries the result as a live one would, up to [`notificationResultChars`](#completion-notice-results); past that the full output is in the transcript file it names, since `get_subagent_result` cannot reach an agent that finished in a previous session. While a survivor is still running, `get_subagent_result` and `steer_subagent` reach it by id, handle or name from the reloaded session. Quitting pi still aborts everything. `router/*` routing is unaffected by the switch. A step that routes through another extension's virtual model bound to the old session is re-prompted up to three times with backoff; if it never recovers the agent ends `error` with `interrupted by a session reload` and a bounded summary of its latest tool activity, not `No output`. Earlier prose is labeled as progress if tools ran after it. The activity summary names only built-in tools (other names become `other tool`) and completion status, never tool arguments or output; inspect the transcript to recover artifacts. Usage and compaction observers bound to the old session are dropped rather than failing the run (any other observer error is logged once and also kept from failing it), and the structured-output retry and forced final report are not attempted on a runtime that is still stale, so `subagents:usage` and `subagents:compacted` stop for an agent detached by a switch.
|
|
174
187
|
|
|
175
188
|
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.
|
|
176
189
|
|
|
@@ -241,6 +254,8 @@ Acknowledged means the existing `resultConsumed` flag was set by inline result d
|
|
|
241
254
|
|
|
242
255
|
Enabled quota-parked schedules awaiting dispatch count as parked even with no agent record, and clear when dispatched, disabled or removed.
|
|
243
256
|
|
|
257
|
+
**Stop all:** `ctrl+shift+k` while the prompt editor has focus (the list collapsed or expanded) shows `Press ctrl+shift+k again within 3s to stop every running agent and workflow.`; a second press inside those three seconds stops every running or queued top-level agent, background survivor and workflow run, and says how many. Nothing else is touched: a later press re-arms instead, an auto-repeat never confirms, and a scheduled job that has not fired is not running. Claude Code's chord is `ctrl+x ctrl+k`, but pi binds `ctrl+x` to copy. It is the same stop as `/agents -> Stop all running`, and needs FleetView enabled.
|
|
258
|
+
|
|
244
259
|
Failed/parked/paused-workflow/fallback counts lead in a shared, uncut flag; at narrow widths these compact to `[E1 P2 B1 F1]` (failed, parked, blocked/paused, fallback). Compact counts above 99 read `99+`; spaces yield before alerts, so all four still fit at 20 columns (`[E99+P99+B99+F99+]`). Below 20 columns alerts yield in reverse priority (fallback, paused, parked), never ahead of the failure flag; if even its count cannot fit, the marker is `!`, never the misleading `[fb]`. Cost uses record-borne estimates only when `showCost` is on; it is the sum for the agents currently summarized, not a session ledger, and excludes workflow-container spend. Narrow lines drop trailing detail and the hint before leading alerts. An empty fleet still hides entirely; the standalone summary renderer has a one-line zero-count form.
|
|
245
260
|
|
|
246
261
|
At an empty prompt, `↓` or `←` temporarily reveals the list and starts navigation. `Esc`, selecting `main`, or typing returns to the explicit disclosure choice; dialogs and conversation overlays keep their keys. `ctrl+shift+a` while navigating collapses and returns to the editor. The separate above-editor activity widget (`widgetMode`) and transcript/result/notice renderers are unchanged.
|
|
@@ -257,8 +272,8 @@ Expanded, the existing navigable list renders **below** the editor:
|
|
|
257
272
|
↓ 3 more: 2 running, 1 failed
|
|
258
273
|
```
|
|
259
274
|
|
|
260
|
-
Running [workflows](#subagentworkflow) 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, or outstanding failures you can inspect, 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: 2 running` / `↓ N more: 1 queued, 1 failed` indicators that count the hidden rows by state
|
|
261
|
-
With [`showModel`](#persistent-settings) on, each row names the model and thinking level it runs at (`▶ running · sonnet 4.6 · think:high · 11s · ↓ 13.1k tokens
|
|
275
|
+
Running [workflows](#subagentworkflow) 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, or outstanding failures you can inspect, 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: 2 running` / `↓ N more: 1 queued, 1 failed` indicators that count the hidden rows by state, without moving the content below it. Unused indicator slots are padded below the content, so `main`, visible rows and nonempty indicators stay contiguous even at a scroll edge. Each agent row carries its state as a glyph and a word in a fixed first column (`▶ running`, `◦ queued`, `✓ done`, `✓ limit`, `✗ failed`, `✗ aborted`, `■ stopped`), so colour is never the only cue; a spawner's `name` leads the description (`pr-1412: Spec review ...`), the configured thinking level reads `think:high` so it cannot be mistaken for the live `thinking · 4s` activity, and columns are sized from every row, not the visible window. Model, thinking, activity, elapsed, token and cost cells all end on their shared column boundary, including when another row has no activity. A configured-model fallback keeps its shared leading `[fallback]` flag even with `showModel` off; column fitting reserves space for flags on hidden rows too, so scrolling cannot shift the tail. On a narrow terminal the description truncates first, then the activity, thinking and model columns drop (in that order). If a fallback row still cannot fit its stats, the shared leading flag compacts to `[fb]` or `!`, never a sliced fragment. For every row, after optional columns and any flag compaction, state words shrink to glyphs, separators and column padding collapse to spaces, and navigation padding disappear as needed. Elapsed, tokens and cost stay whole whenever they physically fit beside the marker (if present); if space is still insufficient, the state glyph and then elapsed yield before tokens or cost may clip. 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`.
|
|
276
|
+
With [`showModel`](#persistent-settings) on, each row names the model and thinking level it runs at (`▶ running · sonnet 4.6 · think:high · 11s · ↓ 13.1k tokens`, and the above-editor widget writes the level the same way), and a running row says what its in-flight model call is doing (see [Live activity](#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.
|
|
262
277
|
|
|
263
278
|
### Fullscreen conversation observer
|
|
264
279
|
|
|
@@ -378,6 +393,8 @@ Individual agent results render Claude Code-style in the conversation:
|
|
|
378
393
|
|
|
379
394
|
Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.
|
|
380
395
|
|
|
396
|
+
For a new `Agent` call, transcript directory and initial entry creation must succeed before the child is queued or started. A setup error fails the call without launching a child; subsequent streaming writes remain best effort. Nested `Agent` calls report the setup error inline.
|
|
397
|
+
|
|
381
398
|
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):
|
|
382
399
|
|
|
383
400
|
```
|
|
@@ -387,13 +404,15 @@ By default, foreground and background agents each stream their full conversation
|
|
|
387
404
|
|
|
388
405
|
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.
|
|
389
406
|
|
|
407
|
+
A failed agent whose session survived is resumable: a provider or credential error (e.g. a transient `!command` API key failure) ends its turn but keeps the whole transcript. Its collapsed notice reads `✗ desc · resume <id> to continue · ...`, and both the expanded notice and the `<task-notification>` `<summary>` say `transcript intact; resume agent <id> to continue`. A run that failed before it had a session carries no such hint.
|
|
408
|
+
|
|
390
409
|
## Default Agent Types
|
|
391
410
|
|
|
392
411
|
| Type | Tools | Model | Prompt Mode | Description |
|
|
393
412
|
|------|-------|-------|-------------|-------------|
|
|
394
413
|
| `general-purpose` | all 7 | inherit | `append` (parent twin) | Inherits the parent's full system prompt — same rules, CLAUDE.md, project conventions |
|
|
395
|
-
| `Explore` | read,
|
|
396
|
-
| `Plan` | read,
|
|
414
|
+
| `Explore` | read, grep, find, ls | `@cheap` | `replace` (standalone) | Fast codebase exploration (read-only); an unbound/unavailable tier visibly inherits the parent by default, or refuses under `modelResolution` `error` |
|
|
415
|
+
| `Plan` | read, grep, find, ls | inherit | `replace` (standalone) | Software architect for implementation planning (read-only) |
|
|
397
416
|
|
|
398
417
|
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.
|
|
399
418
|
|
|
@@ -468,7 +487,7 @@ All fields are optional — sensible defaults for everything.
|
|
|
468
487
|
| `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` |
|
|
469
488
|
| `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:` |
|
|
470
489
|
| `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 |
|
|
471
|
-
| `allowed_subagents` | none | Opt in to scoped nested `Agent`, `get_subagent_result`, and `
|
|
490
|
+
| `allowed_subagents` | none | Opt in to scoped nested `Agent`, `get_subagent_result`, `steer_subagent`, and `stop_subagent` tools. Omitted / empty / `none` / `false` = no nesting; `all` (or `"*"` / `true`) = any enabled agent; comma-separated list = only those agent types |
|
|
472
491
|
| `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) |
|
|
473
492
|
| `inherit_context` | `false` | Fork parent conversation into agent |
|
|
474
493
|
| `run_in_background` | — | Pin this agent to background (`true`) or foreground (`false`). Omit to follow `backgroundByDefault` |
|
|
@@ -481,7 +500,9 @@ Frontmatter is authoritative for `max_turns`, `inherit_context`, `run_in_backgro
|
|
|
481
500
|
- `isolation` holds on every path except workflows: as in Claude Code, only the script's `agent()` option decides there.
|
|
482
501
|
- `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`.
|
|
483
502
|
|
|
484
|
-
**Explicit non-tier tool inputs can use fuzzy names.** These pick the newest model; configured pins and tier/alias members remain exact. A bare family name such as `"opus"` matches several ids; exact ids and [aliases](#machine-local-model-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.
|
|
503
|
+
**Explicit non-tier tool inputs can use fuzzy names.** These pick the newest matching model; configured pins and tier/alias members remain exact. Numeric version runs retain their order and multiplicity before scope filtering or ranking: `sonnet-5-5` can match `claude-5-5-sonnet`, never `claude-4-5-sonnet`, `claude-5-sonnet` or `claude-5-6-sonnet`. A missing requested version is an error, not a neighbouring generation. Dots and dashes are equivalent; a major-only query can still select the newest minor within that major. A bare family name such as `"opus"` matches several ids; exact ids and [aliases](#machine-local-model-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. Ties use the lexicographically smallest case-sensitive `provider/model` identity, independently of registry order; exact id ties use the same ordering without scope filtering. An unresolved explicit input lists that same narrowed candidate set, so it does not suggest models the dispatch path would reject. 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.
|
|
504
|
+
|
|
505
|
+
Agent spawn/results and completion notices echo the resolved `provider/model` identity from the run's retained selection, including nested results and resumes. Scheduled and quota-parked results echo none: they reselect at fire time. A literal virtual model is labelled as a routing alias with an unknown physical response model, not as proof of a physical leaf. Tier notes retain their existing admitted/answered provenance. Only identity strings are echoed, never registry model objects or credentials; identity alone does not trigger a fallback warning.
|
|
485
506
|
|
|
486
507
|
**Exact configuration, convenient tool inputs.** Agent definitions, `agentModels`, tier members and legacy alias candidates resolve exactly, without version ranking or provider retry. An unavailable configured pin inherits the parent with a visible note by default; `modelResolution.onUnavailable: "error"` refuses it instead. Explicit literal/fuzzy tool inputs retain fuzzy matching and unresolved names remain errors; `@tier` and legacy aliases follow `modelResolution`. Built-in Explore uses `@cheap`; bind it globally or in the enterprise manifest. Unbound tiers inherit with disclosure by library default; personal configurations should set both resolution policies to `error`. During enterprise migration only, built-in Explore's unbound `@cheap` can use the existing `modelAliases["claude-haiku-4-5"]` exact candidates as a static cheap tier. An explicit `cheap` binding always wins, including when unavailable; no neighbouring tier or newer model is inferred. User-pinned legacy Explore definitions retain the exact `anthropic/claude-haiku-4-5` compatibility binding. Users who pin Explore explicitly through `agentModels` (e.g. the owner's `agentModels.Explore`) are unaffected. The router's shipped `examples/subscription-router.json` supplies an opt-in ordered `cheap` profile (Haiku then Luna); no profiles are registered implicitly.
|
|
487
508
|
|
|
@@ -499,7 +520,7 @@ allowed_subagents: support-file-finder, support-callsite-tracer # or `all`
|
|
|
499
520
|
|
|
500
521
|
**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.
|
|
501
522
|
|
|
502
|
-
`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](#persistent-settings) 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](#model-scope) exactly like a top-level spawn. Result, resume, and
|
|
523
|
+
`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. The same holds when a listed allowlist resolves to no enabled agent (every name missing or disabled, or `all` over an empty registry): the child gets no nested tools and the prompt says nesting is off, instead of an `Agent` tool whose only advertised type is `none`. Unknown, disabled, and out-of-list types are rejected rather than falling back — regardless of the project's [fallback agent](#persistent-settings) 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](#model-scope) exactly like a top-level spawn. Result, resume, steering, and stop 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.
|
|
503
524
|
|
|
504
525
|
**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.
|
|
505
526
|
|
|
@@ -547,6 +568,8 @@ tools: "*, ext:mcp/search"
|
|
|
547
568
|
isolated: true # hermetic: built-ins only, no extensions/skills/context
|
|
548
569
|
```
|
|
549
570
|
|
|
571
|
+
The embedded `Explore` and `Plan` definitions use only `read`, `grep`, `find`, and `ls`, with `extensions: false`. They deliberately omit `bash`: a prompt cannot make an unrestricted shell read-only. Their tool restriction is an enforced child-tool surface, not an operating-system or filesystem sandbox; custom overrides must choose any extension selectors deliberately.
|
|
572
|
+
|
|
550
573
|
A few rules the examples don't make obvious:
|
|
551
574
|
|
|
552
575
|
- `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.
|
|
@@ -577,7 +600,7 @@ When the effective built-in set lacks `bash` (including `disallowed_tools: bash`
|
|
|
577
600
|
|
|
578
601
|
`Agent` has no per-call tool-override flag. Use a shell-capable type for command work; do not ask a restricted researcher to acquire `bash`.
|
|
579
602
|
|
|
580
|
-
The last two rows are separate because zero built-ins is not zero tools: `tools: none` alongside `extensions:` still surfaces every extension tool, so calling it `none` would understate what the agent can do. Without the `+ extension tools` flag
|
|
603
|
+
The last two rows are separate because zero built-ins is not zero tools: `tools: none` alongside `extensions:` still surfaces every extension tool, so calling it `none` would understate what the agent can do. Without the `+ extension tools` flag, a custom agent configured to search the web through an extension would read as web-less.
|
|
581
604
|
|
|
582
605
|
### Shipping agents in a pi package
|
|
583
606
|
|
|
@@ -628,7 +651,7 @@ Launch a sub-agent.
|
|
|
628
651
|
| `thinking` | string | no | Thinking level: off, minimal, low, medium, high, xhigh, max (availability depends on pi version and model) |
|
|
629
652
|
| `max_turns` | number | no | Max agentic turns. Omit for unlimited (default) |
|
|
630
653
|
| `run_in_background` | boolean | no | Defaults to `true`; `false` blocks and returns the result inline, until [`ctrl+b`](#sending-a-foreground-agent-to-the-background) hands it to the background |
|
|
631
|
-
| `resume` | string | no | Agent ID to resume a previous session. The resumed run gets a fresh `max_turns` budget; its existing model and thinking level remain in effect, so this call's `model` and `thinking` are ignored |
|
|
654
|
+
| `resume` | string | no | Agent ID to resume a previous session. The resumed run gets a fresh `max_turns` budget; its existing model and thinking level remain in effect, so this call's `model` and `thinking` are ignored. An evicted agent whose session was persisted ([`rememberAgents`](#persistent-settings)) is reopened from disk like `@handle`, under a NEW agent id with the model selected anew and this call's `max_turns` applied. It runs only in the background: with `run_in_background: false` the call is refused and says so. The old id keeps resolving to the newest id of that conversation, so a second resume while it runs is answered with that id and status instead of reopening the file twice, and the chain survives the new record's own eviction. The result names the reopened session file, which is the transcript; no `.output` file is written for it |
|
|
632
655
|
| `isolated` | boolean | no | No extension/MCP tools |
|
|
633
656
|
| `isolation` | `"off"` \| `"worktree"` | no | `worktree` runs in an isolated git worktree; `off` (the default) does not. Absent from the schema entirely when `worktreeIsolation: false` |
|
|
634
657
|
| `cwd` | string | no | Working directory for the agent: absolute, `~/`-prefixed, or relative to the session cwd. Default: the session cwd. With `isolation: "worktree"`, the worktree comes from the repo containing it; see [Worktree Isolation](#worktree-isolation). Not combinable with `schedule` |
|
|
@@ -687,13 +710,25 @@ Cancelling a `wait: true` call (for example, with `Esc`) stops only the wait. Th
|
|
|
687
710
|
|
|
688
711
|
### `steer_subagent`
|
|
689
712
|
|
|
690
|
-
Send a
|
|
713
|
+
Send a message to an agent, like Claude Code's `SendMessage`. A running agent is interrupted after the current tool execution. A queued agent (waiting on `maxConcurrent`) accepts it too: the result says it was queued, and it is delivered into the agent's first turn when it starts.
|
|
714
|
+
|
|
715
|
+
A finished agent (`completed`, `steered`, `aborted` at the turn limit, or `error`) is resumed instead: the call runs `Agent` with `resume` set to that agent, `run_in_background: true` and `message` as the prompt, so the result is a background resume's (`Agent resumed in background.`, the agent id, a completion notice to follow) and its full history is kept. That includes reopening an evicted agent's persisted session under a new id (see `resume`). A stopped agent (`stopped`, by the user or by [`stop_subagent`](#stop_subagent)) is refused, not resumed. An agent whose own file sets `run_in_background: false` resumes in the foreground, as `Agent` would. Nested agents keep their own scoped `steer_subagent`, which still only steers a running child.
|
|
691
716
|
|
|
692
717
|
| Parameter | Type | Required | Description |
|
|
693
718
|
|-----------|------|----------|-------------|
|
|
694
719
|
| `agent_id` | string | yes | Agent ID to steer |
|
|
695
720
|
| `message` | string | yes | Message to inject into agent conversation |
|
|
696
721
|
|
|
722
|
+
### `stop_subagent`
|
|
723
|
+
|
|
724
|
+
Stop a running or queued background agent, like Claude Code's `TaskStop`. A steer asking an agent to stop is read only after its current tool call and can be ignored; this cannot. It goes through the same stop as a viewer's `x`: the run is aborted at once, its current tool call gets the abort signal, and it starts no further tool call, so the record ends `stopped`. The tool returns that state (type, tool uses, tokens). A running agent then sends one completion notice, flagged `stopped by the parent with stop_subagent` rather than `STOPPED BY THE USER`, carrying its partial output and, under worktree isolation, the branch its changes were saved to. It counts as read like any `stopped` notice (see [Completion notice results](#completion-notice-results)). A queued agent never starts, frees its place in the queue, and sends no notice; it still emits `subagents:failed` with `status: "stopped"` so event listeners see it end.
|
|
725
|
+
|
|
726
|
+
`agent_id` takes an id, handle or name, like `steer_subagent`, and reaches a background agent that survived a session switch. A `SubagentWorkflow` Task ID stops that run and its agents, and its notice says `Stopped by the parent with stop_subagent`. An unknown id, or one that is no longer running, is an error that lists every running or queued agent (id, `@handle`, status, description) and workflow run. Ownership is unchanged: a nested or workflow child is not reachable from the top-level tool. A nested parent gets its own scoped `stop_subagent` for its own children. Like the other orchestration tools, it is excluded from a child's tools unless the child sets `allowed_subagents`. A stopped agent is not resumed by `steer_subagent`. The tool declares `readOnlyHint: true`, so [Plan mode](../plan-mode/README.md) admits it by default: it edits nothing and starts nothing, it only ends work already running, so a planning session can still stand a fan-out down. A second stop of the same agent says it is already `stopped`, and of a workflow still unwinding, that it is already stopping. Runs of another extension's `workflow` tool are outside this package, and the tool description says so: `@quintinshaw/pi-dynamic-workflows` stops its own with `workflow_control` action `stop`.
|
|
727
|
+
|
|
728
|
+
| Parameter | Type | Required | Description |
|
|
729
|
+
|-----------|------|----------|-------------|
|
|
730
|
+
| `agent_id` | string | yes | Agent ID, handle or name to stop, or a `SubagentWorkflow` Task ID |
|
|
731
|
+
|
|
697
732
|
## Commands
|
|
698
733
|
|
|
699
734
|
| Command | Description |
|
|
@@ -741,12 +776,14 @@ The `/agents` command opens an interactive menu:
|
|
|
741
776
|
|
|
742
777
|
```
|
|
743
778
|
Running agents (2) — 1 running, 1 done ← only shown when agents exist
|
|
779
|
+
Stop all running (1) ← only shown while something runs or is queued
|
|
744
780
|
Agent types (6) ← unified list: defaults + custom
|
|
745
781
|
Create new agent ← manual wizard or AI-generated
|
|
746
782
|
Settings ← max concurrency (background + foreground), max turns, grace turns, join mode
|
|
747
783
|
```
|
|
748
784
|
|
|
749
785
|
- **Running agents** — select one to open its live conversation viewer. While it's still running, press `Enter` to open the steering 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). On a live foreground run, press `b` to hand the same session and in-flight work to the background without stopping or restarting it — the blocked `Agent` call returns the agent ID and the run notifies on completion like any background agent. Press `x` (then `x` again to confirm) to stop/abort it — including **background** agents, which a global Esc can't unambiguously target (Esc still stops a blocking foreground `Agent` call, but no longer reaches one that has been handed off — `x` here does). A stopped agent reports its partial output flagged as incomplete, not as a completion. `m` cycles how much of the transcript renders as Markdown — see [Viewer markdown](#persistent-settings).
|
|
786
|
+
- **Stop all running** — after a confirmation, stops every running or queued top-level agent, background survivor of a session switch, and workflow run (with its agents), the same as the FleetView stop-all chord. A scheduled job that has not fired yet is not running and is left alone.
|
|
750
787
|
- **Agent types** — unified list with source indicators: `•` (project), `◦` (global), `▪` ([package](docs/packages.md)), `✕` (disabled), explained by a legend naming only the badges actually on screen. Each row shows the agent's model, and the highlighted agent's full description appears below the list — prefixed with the declaring package's name for a `▪` agent, since the badge alone doesn't say *which* package. The model column flags `(unavailable, fallback: inherit)` when a configured model can't be resolved (it would silently inherit the parent model), and shows `(→ provider/id)` when it resolves to a different provider or version than configured. Select an agent to manage it:
|
|
751
788
|
- **Default agents** (no override): Eject (export as `.md`), Disable
|
|
752
789
|
- **Default agents** (ejected/overridden): Edit, Disable, Reset to default, Delete
|
|
@@ -759,7 +796,7 @@ Settings ← max concurrency (background + fo
|
|
|
759
796
|
|
|
760
797
|
## Record retention
|
|
761
798
|
|
|
762
|
-
A finished agent's record (and the session behind `get_subagent_result` and `steer_subagent`) is kept for **10 minutes once its result was consumed** (fetched with `get_subagent_result`, or delivered inline to a blocking caller) and **60 minutes from completion while nobody has read it**. After that the record is evicted, and the manager keeps an in-memory note for the rest of the session:
|
|
799
|
+
A finished agent's record (and the session behind `get_subagent_result` and `steer_subagent`) is kept for **10 minutes once its result was consumed** (fetched with `get_subagent_result`, or delivered inline to a blocking caller) and **60 minutes from completion while nobody has read it**. After that the record is evicted, and the manager keeps an in-memory note for the rest of the session: `get_subagent_result` answers `Agent "<id>" (<description>) completed, evicted after N min; transcript: <path>` instead of `Agent not found`, and an `Agent` `resume` or `steer_subagent` that has no persisted session to reopen prefixes that note with `Cannot resume agent "<id>": no saved session is left to reopen.` A `resume` or `steer_subagent` of an evicted agent whose session was persisted reopens it instead (see `resume` above). The path is the `.output` transcript when one was written, else the session file. A nested or workflow child leaves no note, so it stays `Agent not found` to these top-level tools after eviction as before it. When the sweep evicts a top-level agent whose result was **never read**, the parent also gets a `subagent-reaped` message on its next turn (no turn is triggered) carrying that note, so the loss is announced instead of discovered by a later lookup; a read result, a failure whose whole text the completion notice already carried, an agent whose latest run never started (stopped while queued, refused on an already-aborted signal, or failed before the runner began; a resumed one included), a result handed to a blocking caller inline (a foreground resume included), a nested or workflow child, and a record cleared at a session boundary send nothing, and neither does a sweep on a manager a session switch left behind for its running survivors (its `pi` is stale, so the send is dropped; not announced from there, and the new session's manager does not know those records). Running and queued agents are never evicted. Notes are cleared on `/new` and `/resume`, like the records. A background agent that finishes while its session is switched away (`/new`, `/resume`, `/reload`) leaves the same note, kept once its result is delivered to the reopened session, so the id in its notice still resolves. When it also has a saved session, the delivery registers it as reopenable too, so `resume` or `steer_subagent` on that id continues the conversation instead of reporting that no session is left; a name already held by a live agent, an evicted conversation or a reserved survivor stays with its holder, and the id then answers `Cannot resume agent "<id>": its name "<handle>" now belongs to another agent, so its saved session was not kept reopenable.` For tests, `PI_SUBAGENTS_CONSUMED_TTL_MIN` and `PI_SUBAGENTS_UNREAD_TTL_MIN` override the two windows in minutes (fractions and `0` allowed; read when the manager starts); the sweep then runs as often as the shorter window needs, but not more than once a second.
|
|
763
800
|
|
|
764
801
|
## Graceful Max Turns
|
|
765
802
|
|
|
@@ -825,6 +862,16 @@ When background agents complete, they notify the main agent. As in Claude Code,
|
|
|
825
862
|
**Configuration:**
|
|
826
863
|
- Configure join mode in `/agents` → Settings → Join mode
|
|
827
864
|
|
|
865
|
+
### Completion notice results
|
|
866
|
+
|
|
867
|
+
A completion notice carries the agent's whole result, as Claude Code's does, up to `notificationResultChars` per agent (default `16000`). For a failed agent that is its error plus any partial output, the same text `get_subagent_result` returns. A result delivered whole counts as read, like a foreground result: no `get_subagent_result` is needed and the record gets the 10-minute read retention instead of 60 minutes unread. **A failure (`error` or `aborted`) never counts as read from a notice**, however short: delivering a completion notice does not acknowledge a failure in [FleetView](#fleetview), so it stays flagged, with the 60-minute unread retention, until `get_subagent_result`, RPC consume or closing its FleetView conversation acknowledges it. A `stopped` agent's partial output is not a failure and counts as read like any other.
|
|
868
|
+
|
|
869
|
+
The cap counts the raw result characters, before XML escaping (`<` is one char, not the four of `<`); the truncation marker is extra. Past the cap the notice keeps the head and the tail of the result around one marker line, `...(truncated: N of M chars omitted; full result in <output-file>, or use get_subagent_result)...`, and the result stays unread. Notices that land in one message, a `group` batch or completions coalesced at one boundary, share a budget of four caps (64k chars by default): each result takes what it needs up to an even split of what is left, so a short result is never cut to make room for a long one. A notice spooled across a session switch carries the same body and is clipped when it is delivered, so survivors that land together after `/resume` share that budget too. Their records live in the orphaned instance, so they are not marked read.
|
|
870
|
+
|
|
871
|
+
**Trust model:** the `<result>` is the subagent's output, and a subagent reads files, web pages and tool output written by anyone, so the result can carry text crafted to look like instructions. It is XML-escaped, so it cannot close its own `<result>` or forge another notice's tags, and a `<result-note>` line before it tells the parent to evaluate it as data, not follow it. That is framing, not a sandbox: a full result puts more third-party text in the parent's context than a 500-char preview did, so treat agents that read untrusted input accordingly, or set `notificationResultChars: 0`.
|
|
872
|
+
|
|
873
|
+
Only the model-facing `<result>` grows; the rendered notification box keeps its 500-char preview. `notificationResultChars: 0` restores the old result: a 500-char preview (300 per `group` member) with a pointer to `get_subagent_result`, and nothing is marked read. The `<result-note>` line stays in either mode. Set it from `/agents` → Settings → Notice result chars (0 to 1000000).
|
|
874
|
+
|
|
828
875
|
## Model tiers
|
|
829
876
|
|
|
830
877
|
Use `model: "@cheap"` in an agent, `agentModels`, `Agent({ model: "@cheap" })`, RPC or workflow `agent()` options. Explicit tool models still win. Tiers are independent of `enabledModels`, fuzzy matching, model versions and provider naming conventions.
|
|
@@ -835,7 +882,7 @@ Routed personal setup (configure the router profiles separately):
|
|
|
835
882
|
|
|
836
883
|
```json
|
|
837
884
|
{
|
|
838
|
-
"modelTiers": { "cheap": ["router/cheap"], "default": ["router/default"], "deep": ["router/deep"], "review": ["router/review"] },
|
|
885
|
+
"modelTiers": { "cheap": ["router/cheap"], "default": ["router/default"], "deep": ["router/deep"], "frontier": ["router/frontier"], "review": ["router/review"] },
|
|
839
886
|
"modelResolution": { "onUnmapped": "error", "onUnavailable": "error" },
|
|
840
887
|
"agentModels": { "*": "@default", "Explore": "@cheap", "code-reviewer": "@review" }
|
|
841
888
|
}
|
|
@@ -855,6 +902,14 @@ Enterprise manifest (machine-local, permissions `0600`):
|
|
|
855
902
|
{ "version": 1, "subagents": { "modelTiers": { "cheap": ["example-provider/example-wire-model"] }, "agentModels": { "Explore": "@cheap" } } }
|
|
856
903
|
```
|
|
857
904
|
|
|
905
|
+
Enterprise `frontier` binding, two exact flagship members in order (placeholders):
|
|
906
|
+
|
|
907
|
+
```json
|
|
908
|
+
{ "version": 1, "subagents": { "modelTiers": { "frontier": ["example-provider/example-flagship-a", "example-provider/example-flagship-b"] } } }
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
`@frontier` is the tier for the hardest design and architecture work. Skills and agents name `@frontier` on every profile; only this binding differs. Leave it unbound when no flagship is served: an unbound `@frontier` follows `modelResolution.onUnmapped`, so the work default inherits the parent model with a visible note, and the personal `"error"` refuses it. It never falls back to `@deep` or another tier.
|
|
912
|
+
|
|
858
913
|
Ordered static list:
|
|
859
914
|
|
|
860
915
|
```json
|
|
@@ -865,6 +920,8 @@ Static tiers choose the first credentialed, admitted member and retain skipped m
|
|
|
865
920
|
|
|
866
921
|
`modelResolution.onUnmapped` and `onUnavailable` each accept `error` or `inherit`; both library defaults are `inherit`, with a visible note. Neither policy applies to quota exhaustion. Whitespace-only model inputs mean unset; `@` alone and unknown/case-mismatched tier names are visibly unmapped.
|
|
867
922
|
|
|
923
|
+
The `Agent` tool guideline and its `model` parameter list the configured tier keys, e.g. `Configured tiers: @cheap, @deep, @default, @frontier, @review. Prefer a tier over a model id.`, never their members. With no tiers configured they keep the generic tier / `provider/modelId` / fuzzy-name text.
|
|
924
|
+
|
|
868
925
|
Results disclose `Model: @cheap -> provider/id via router/cheap` before the first response using admission's `resolvedModel`, then follow the answering model. Mid-run changes say `switched ... within tier`. Shared leading `[@cheap]` flags identify healthy tiers neutrally; warning flags and grouped alert counts cover only actual switches, parent inheritance, unavailable or unmapped outcomes. Structured `modelTier` outcomes survive records, events, transcripts, eviction and workflow journals. Reopened evicted sessions select/admit anew and retain old outcomes separately as `previousModelTier` and `previousModelFallback`; live resumes keep the existing session's selection. Scheduled tier jobs store `@cheap`, not today's physical pick, and resolve it again when they fire.
|
|
869
926
|
|
|
870
927
|
## Machine-local model aliases
|
|
@@ -896,7 +953,7 @@ Precedence, highest first: the `Agent` tool's `model` parameter, `agentModels[ty
|
|
|
896
953
|
|
|
897
954
|
- **Intentional blank-input change:** empty and whitespace-only model strings mean unset at every precedence level and on every entry path, falling through to the next default without recording a `param` or `definition` request. Previously RPC/workflow explicit blanks and runner-based configured blanks reached fuzzy resolution (and could select an available model); Agent/nested/scheduled paths skipped `""` but resolved whitespace. Nonblank inputs retain their original spelling and matching.
|
|
898
955
|
|
|
899
|
-
|
|
956
|
+
At narrow terminal widths, the workflow dialog reserves its agent-list fallback disclosure: `[fallback]` compacts to `[fb]`, then `!`, rather than showing a clipped fragment. The detail pane middle-elides unusually long fallback notes so both the configured input and final diagnosis stay visible. The full note remains available in the result text, the record, the run log and `get_subagent_result`.
|
|
900
957
|
|
|
901
958
|
## Model Scope
|
|
902
959
|
|
|
@@ -923,12 +980,12 @@ When on, each subagent spawn's effective model is validated against pi's own `en
|
|
|
923
980
|
|
|
924
981
|
## Persistent Settings
|
|
925
982
|
|
|
926
|
-
Runtime tuning values set via `/agents` → Settings (max concurrency, max foreground concurrency, default max turns, grace turns, nested depth, fallback agent, default join mode, scheduling on/off, scope models on/off, disable defaults on/off, strict agent files on/off, package agents on/off, package workflows on/off, agent mentions on/off, output transcript on/off, tool description full/compact/custom, widget all/background/off, usage reporting on/off, cost display on/off, model display on/off, viewer markdown off/assistant/all) persist across pi restarts. Two files, merged on load:
|
|
983
|
+
Runtime tuning values set via `/agents` → Settings (max concurrency, max foreground concurrency, default max turns, grace turns, nested depth, notice result chars, fallback agent, default join mode, scheduling on/off, scope models on/off, disable defaults on/off, strict agent files on/off, package agents on/off, package workflows on/off, agent mentions on/off, output transcript on/off, tool description full/compact/custom, widget all/background/off, usage reporting on/off, cost display on/off, model display on/off, viewer markdown off/assistant/all) persist across pi restarts. Two files, merged on load:
|
|
927
984
|
|
|
928
985
|
- **Global:** `~/.pi/agent/subagents.json` — your machine-wide defaults. Edit by hand; the `/agents` menu never writes here.
|
|
929
986
|
- **Project:** `<cwd>/.pi/subagents.json` — per-project overrides. Written by `/agents` → Settings.
|
|
930
987
|
|
|
931
|
-
**Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `10`, max foreground concurrency `0` = unlimited, default max turns unlimited, grace turns `5`, nested depth `2`, join mode `smart`, defaults enabled).
|
|
988
|
+
**Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `10`, max foreground concurrency `0` = unlimited, default max turns unlimited, grace turns `5`, nested depth `2`, notice result chars `16000`, join mode `smart`, defaults enabled).
|
|
932
989
|
|
|
933
990
|
**Process budget** (`maxActiveSubagents`, default `0` = off; `nestedSubagentReserve`, default `2`): the process-wide ceiling described under [Process budget](#process-budget). Global configuration only — not written by `/agents`, and ignored with a warning in a project file.
|
|
934
991
|
|
|
@@ -944,7 +1001,7 @@ Runtime tuning values set via `/agents` → Settings (max concurrency, max foreg
|
|
|
944
1001
|
|
|
945
1002
|
**Agent mentions** (`agentMentions`, default `"model"`): whether [`@handle message`](#agent-mentions) at the prompt addresses that subagent instead of the main model — messaging, resuming or starting it — and whether `@` offers agents alongside pi's file completion. `"model"` and `"direct"` differ only in [who starts an agent that isn't running](#starting-a-new-agent): an off-screen clone of this conversation, via a `<system-reminder>` and a real `Agent` call, or this extension, immediately and with no model call. Messaging and resuming are direct in both. `"off"` gates all three actions plus the suggestion list, so `@` means only "attach a file" again and every `@…` prompt reaches the main model verbatim. Toggle via `/agents → Settings → Agent mentions`; applied live. The booleans this setting used to take are still read — `true` as `"model"`, `false` as `"off"`.
|
|
946
1003
|
|
|
947
|
-
**Background by default** (`backgroundByDefault`, default `true`): what an `Agent` call that doesn't say means. On — following Claude Code — the agent runs detached, the call returns its ID immediately, and a completion notification carries
|
|
1004
|
+
**Background by default** (`backgroundByDefault`, default `true`): what an `Agent` call that doesn't say means. On — following Claude Code — the agent runs detached, the call returns its ID immediately, and a completion notification carries the result up to [`notificationResultChars`](#completion-notice-results) (`get_subagent_result` for anything it had to cut). Set `false` to restore the previous behaviour, where an unqualified spawn blocked the turn and returned its output inline. An explicit `run_in_background` on the call, or in an agent file's frontmatter, overrides this in both directions; the setting only decides what "unspecified" means. **Top-level only** — a nested spawn (an agent spawning its own) always defaults to foreground, because a detached child is stopped when its parent settles and has no notification path of its own. Toggle via `/agents → Settings → Background by default`; applied live.
|
|
948
1005
|
|
|
949
1006
|
**Remember agents** (`rememberAgents`, default `true`): whether subagents persist their pi session, which is what lets [`@handle`](#agent-mentions) reopen an agent's conversation after its in-memory record has been evicted. Two visible consequences of the default: top-level subagents write a session file, and they nest under the session that spawned them in pi's `/resume`. Agents spawned by another agent are excluded — they get no handle, so nothing could reopen their transcript. A custom agent's `persist_session` frontmatter overrides this per agent, in both directions. Toggle via `/agents → Settings → Remember agents`; with it off, handles expire with their record (roughly ten minutes past completion) and `@explore` then starts a fresh agent rather than resuming — the behaviour before this setting existed.
|
|
950
1007
|
|
|
@@ -969,10 +1026,10 @@ The `~` marks it as pi's estimate rather than a billed figure. **A cost is shown
|
|
|
969
1026
|
|
|
970
1027
|
Independent of `reportUsage`: this one is what you read, that one is what your session counts. Toggle via `/agents → Settings → Show cost`; applied live.
|
|
971
1028
|
|
|
972
|
-
**Show model** (`showModel`, default `false`): whether the widget's running rows and the [FleetView](#fleetview) rows name the model driving each agent and the thinking level it is running at. Toggling it refreshes both. The widget row, then the FleetView row
|
|
1029
|
+
**Show model** (`showModel`, default `false`): whether the widget's running rows and the [FleetView](#fleetview) rows name the model driving each agent and the thinking level it is running at. Toggling it refreshes both. The widget row, then the FleetView row. Both write the level `think:high` so it cannot read as a duplicate of the live `thinking · 4s` activity:
|
|
973
1030
|
|
|
974
1031
|
```text
|
|
975
|
-
├─ ⠹ Explore inspect code · sonnet 4.6 ·
|
|
1032
|
+
├─ ⠹ Explore inspect code · sonnet 4.6 · think:high · ↻3 · 8.2k token · 4.1s
|
|
976
1033
|
○ Explore inspect code ▶ running · sonnet 4.6 · think:high · thinking · 4s · 41s · ↓ 8.2k tokens
|
|
977
1034
|
```
|
|
978
1035
|
|
|
@@ -1004,6 +1061,8 @@ Turn `all` on for tools that genuinely emit Markdown, and off again for a diff o
|
|
|
1004
1061
|
|
|
1005
1062
|
Leaving it unset is not quite the same as `true`. Unset means *auto*: on, unless another extension already provides a workflow tool, in which case this one warns and stands down for the session. Two orchestrators in one tool spec is a worse default than none — the model has to guess which to call and pays for both descriptions to find out — and the extension that was installed on purpose is the one that should survive. Setting `workflowsEnabled` explicitly pins the answer: `true` keeps ours whatever else is loaded, `false` is off regardless.
|
|
1006
1063
|
|
|
1064
|
+
Child sessions do not start a second subagents manager, even when their extension loader imports a separate copy of the package. Their SDK refusal tool still explains why nested workflows are unavailable; the parent keeps its own workflow tool and manager. This holds across a child session's `reload()` too.
|
|
1065
|
+
|
|
1007
1066
|
The match is on the exact tool names `Workflow` (Claude Code's) and `SubagentWorkflow` (ours), never a substring, so a `list_workflows` or `github_workflow_run` from some CI integration does not silently take the feature down. The check runs at `session_start` and nowhere earlier, because `getAllTools` throws during extension loading and load order means a check at registration time could not see an extension that has not loaded yet — so the tool is registered first and withdrawn from the active set through `setActiveTools`, which rebuilds the system prompt before any turn runs. When the other extension took the `SubagentWorkflow` name itself, pi's first-registration-wins rule already dropped ours, so there is nothing to withdraw and only the menu and the CLI flag come down.
|
|
1008
1067
|
|
|
1009
1068
|
**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.
|
|
@@ -1039,24 +1098,29 @@ Every project now starts with concurrency 16 and grace 10, without ever touching
|
|
|
1039
1098
|
|
|
1040
1099
|
## Events
|
|
1041
1100
|
|
|
1042
|
-
Agent lifecycle events are emitted via `pi.events.emit()` so other extensions can react:
|
|
1101
|
+
Agent lifecycle events are emitted via `pi.events.emit()` so other extensions can react. A queued background resume's `subagents:created` includes `queuedAfterSettleSeq` (the preceding terminal event's `settleSeq`) and `queuedResumeSeq` (the new queue entry's sequence). A listener can match the preceding settle before marking an existing agent queued and reject late announcements after a later settle. Lifecycle events also carry `wakesParent` for completion-notice eligibility. A foreground handoff's `subagents:created` carries `backgroundHandoff` and the same `runSeq` as its `started`, so listeners can update eligibility without accepting a stale handoff from an earlier run.
|
|
1043
1102
|
|
|
1044
1103
|
| Event | When | Key fields |
|
|
1045
1104
|
|-------|------|------------|
|
|
1046
|
-
| `subagents:created` | `Agent`-tool background spawn, a detached resume, or a foreground `Agent` call handed to the background (after its `started`) — **not** cross-extension RPC, scheduler, or `@handle` spawns, which are first seen at `subagents:started` | `id`, `type`, `description`, `isBackground` (always `true`); plus `modelFallback`/`modelNote` when already known, see below |
|
|
1047
|
-
| `subagents:started` | Agent transitions to running (including queued→running) | `id`, `type`, `description
|
|
1048
|
-
| `subagents:completed` | Agent finished successfully (background and foreground) | `id`, `type`, `description`, `status`, `durationMs`, `tokens` (display total, `{ input, output, total }` — see the note below), `usage` (the run's spend as a pi `Usage`: token components including `cacheRead`, plus `cost.total` in USD; absent when nothing was spent), `toolUses`, `result`; plus `modelFallback` (`{ input, used, also?, via? }`: `also` and `via` retain older disclosure details; `via` is `"parent"` or `"agent"`) and `modelNote` when a configured model did not resolve and the agent ran on another |
|
|
1049
|
-
| `subagents:failed` | Agent errored, stopped, or aborted (background and foreground) | identical payload to `subagents:completed` — both are built by the same formatter, so `error` and `status` are present on that row too, just empty |
|
|
1105
|
+
| `subagents:created` | `Agent`-tool background spawn, a detached resume, or a foreground `Agent` call handed to the background (after its `started`) — **not** cross-extension RPC, scheduler, or `@handle` spawns, which are first seen at `subagents:started`, or at `subagents:failed` when stopped while still queued | `id`, `type`, `description`, `handle`/`name`/`nameBase`/`previousId`/`spawnSeq` (see below), `isBackground` (always `true`); plus `modelFallback`/`modelNote` when already known, see below |
|
|
1106
|
+
| `subagents:started` | Agent transitions to running (including queued→running) | `id`, `type`, `description`, `handle`/`name`/`nameBase`/`previousId`/`spawnSeq` (see below); plus `modelFallback`/`modelNote` only when the spawn path already knew it, see below |
|
|
1107
|
+
| `subagents:completed` | Agent finished successfully (background and foreground) | `id`, `type`, `description`, `handle`/`name`/`nameBase`/`previousId`/`spawnSeq` (see below), `status`, `durationMs`, `tokens` (display total, `{ input, output, total }` — see the note below), `usage` (the run's spend as a pi `Usage`: token components including `cacheRead`, plus `cost.total` in USD; absent when nothing was spent), `toolUses`, `result`; plus `modelFallback` (`{ input, used, also?, via? }`: `also` and `via` retain older disclosure details; `via` is `"parent"` or `"agent"`) and `modelNote` when a configured model did not resolve and the agent ran on another |
|
|
1108
|
+
| `subagents:failed` | Agent errored, stopped, or aborted (background and foreground), including a top-level agent stopped while still queued, which never emitted `started` | identical payload to `subagents:completed` — both are built by the same formatter, so `error` and `status` are present on that row too, just empty |
|
|
1050
1109
|
| `subagents:steered` | Steering message accepted from any path (`steer_subagent`, `@handle`, the viewers, `subagents:rpc:steer`) — fires for a *queued* steer as well as a delivered one | `id`, `message` |
|
|
1051
1110
|
| `subagents:usage` | Every assistant message any agent spends on — top-level, nested and workflow children alike, live while it runs (not for an agent detached by `/new`, `/resume`, `/fork` or `/reload`: its observers are dropped after the switch) | Always: `id`, `type`, `description`, `depth`, `usage` (that one message's spend as a pi `Usage`; never emitted for a message that spent nothing). Omitted when unknown, never `undefined`: `model` (`provider/id`) and `thinking` (the effective level) until the session has reported them; `requestedThinking`, only when the agent did not get the level it asked for; `parentAgentId`, nested agents only; `workflowId`, a workflow's agents only; `modelFallback` and `modelNote` when a configured model did not resolve |
|
|
1052
|
-
| `subagents:compacted` | Agent's session successfully compacted (not emitted for an agent detached by a session switch) | `id`, `type`, `description`, `reason` (`"manual"` / `"threshold"` / `"overflow"`), `tokensBefore`, `compactionCount`; plus `modelFallback`/`modelNote` when known |
|
|
1111
|
+
| `subagents:compacted` | Agent's session successfully compacted (not emitted for an agent detached by a session switch) | `id`, `type`, `description`, `handle`/`name`/`nameBase`/`previousId`/`spawnSeq` (see below), `reason` (`"manual"` / `"threshold"` / `"overflow"`), `tokensBefore`, `compactionCount`; plus `modelFallback`/`modelNote` when known |
|
|
1053
1112
|
| `subagents:disposed` | After actual shutdown disposal of this activation's manager and child sessions. Not emitted when a session switch detaches surviving background work. Those detached agents no longer emit `subagents:usage` or `subagents:compacted` either (their `pi` is stale, so the observers are dropped), so a spend ledger under-counts them; their completion still arrives through the session spool. On quit, flush **synchronously** (`writeFileSync`, not `fs.promises`): `emit` does not await listeners and pi exits once shutdown handlers return | `{}` |
|
|
1054
1113
|
| `subagents:scheduled` | Schedule lifecycle change | `{ type: "added" \| "removed" \| "updated" \| "fired" \| "error", … }` (job/agentId/error fields per type) |
|
|
1055
1114
|
| `subagents:scheduler_ready` | Scheduler bound to session, enabled jobs armed | `sessionId`, `jobCount` |
|
|
1115
|
+
| `subagents:started` (survivor) | Re-announcement of each background agent still running under this session's file (after `/reload`, or a `/resume` back into the session that spawned them), at every `session_start` and whenever `subagents:announce_survivors` (`{}`) is emitted; `spawnSeq`/`runSeq` are omitted because the orphaned manager counts its own sequence. A survivor's later `subagents:completed` / `:failed` is emitted when its result is delivered, without `settleSeq` | same as `subagents:started` |
|
|
1116
|
+
| `subagents:announce_survivors` | Request, not a notification: a listener that bound after `session_start` (pi-todo sends it once its session is bound) asks this activation to re-emit its survivor announcements | `{}` |
|
|
1117
|
+
| `subagents:other_session_survivor` | At every `session_start` and on `subagents:announce_survivors`: each background agent still running under a session file other than the current one, whatever switch left it (`/fork`, a fork of a fork, `/resume` of another session, `/new`). Its result spools to that file, so it wakes that session, not this one; no `wakesParent`, and no `completed` / `failed` follows here unless that session is later resumed. A listener re-asks to learn that one finished. Listeners must not treat it as armed | same as `subagents:started`, minus `wakesParent`, `spawnSeq` and `runSeq`, plus `sessionFile` (the session it wakes) |
|
|
1056
1118
|
| `subagents:ready` | RPC handlers registered and armed — fired on session start; not emitted in a session that excludes pi-subagents | `{}` (empty object) |
|
|
1057
1119
|
| `subagents:settings_loaded` | Persisted settings applied at extension init | `settings` (merged global + project) |
|
|
1058
1120
|
| `subagents:settings_changed` | `/agents` → Settings mutation was applied | `settings`, `persisted` (`boolean` — `false` on write failure) |
|
|
1059
1121
|
|
|
1122
|
+
**Agent identity on events.** `created`, `started`, `completed`, `failed` and `compacted` carry `handle`, the agent's type-derived `@handle` (`explore`, `explore-2`), and `name`, the second handle drawn from the spawner's `name` (`auth-audit` for `name: "Auth Audit"`), and `nameBase`, that name before collision numbering: a retry spawned as `Auth Audit` while the first try still holds `auth-audit` gets `name: "auth-audit-2"` and `nameBase: "auth-audit"`. An evicted agent reopened under a new id also carries `previousId`, the id it continues. Each is omitted when the agent has none; `previousId` cannot be set through `subagents:rpc:spawn`. `spawnSeq` orders the agent's creation, before any time queued, and `completed`/`failed` add `settleSeq` from the same counter (one per extension activation), so a listener can tell an agent spawned after another settled from one queued behind it. A listener can then match an agent by what the parent typed instead of only its opaque `id`; pi-todo uses them to show a todo `owner`'s live agent state ([owner match rule](https://github.com/nredd/pi-harness/tree/main/packages/todo/docs/overlay.md#owner-match-rule)).
|
|
1123
|
+
|
|
1060
1124
|
**Tier provenance on events.** Payloads that disclose fallback provenance also carry `modelTier` and `modelNote` for tier selections. Outcomes are `routed`, `static`, `unmapped`, `unavailable` or `exhausted`; successful outcomes include the tier, bound profile/member, admitted model and latest answering model, and static outcomes list skipped members and whether a quota signal was used. `subagents:record` history entries and detached completion spools retain the same outcome. Persisted `modelTier.model` contains identity only (`provider`, `id`, `name`, `api`), never provider URLs, headers or other credential-bearing model overlays.
|
|
1061
1125
|
|
|
1062
1126
|
**Model fallback on events.** When a configured model did not resolve, `modelFallback` (`{ input, used, also?, via? }`) and a ready-made `modelNote` ride on `created`, `started`, `compacted`, `usage` and `completed`/`failed` payloads. The shared spawn path records selection before emitting `started`, including RPC, `@mention` and schedules with no explicit model. All later events read the same retained provenance.
|
|
@@ -1122,7 +1186,7 @@ pi.events.emit("subagents:rpc:spawn", {
|
|
|
1122
1186
|
});
|
|
1123
1187
|
```
|
|
1124
1188
|
|
|
1125
|
-
`options` is the manager's spawn-option object, not the `Agent` tool's parameter schema — the background flag is `isBackground`, and the tool's snake_case `run_in_background` is forwarded verbatim and ignored. Every RPC spawn returns its id immediately and runs detached either way; `isBackground: true` is what makes the agent occupy one of the `maxConcurrent` slots (and queue behind them when they are full). It does not affect `subagents:created`, which is never emitted for an RPC spawn at all — the first event you see for one is `subagents:started
|
|
1189
|
+
`options` is the manager's spawn-option object, not the `Agent` tool's parameter schema — the background flag is `isBackground`, and the tool's snake_case `run_in_background` is forwarded verbatim and ignored. Every RPC spawn returns its id immediately and runs detached either way; `isBackground: true` is what makes the agent occupy one of the `maxConcurrent` slots (and queue behind them when they are full). It does not affect `subagents:created`, which is never emitted for an RPC spawn at all — the first event you see for one is normally `subagents:started`; an agent stopped while still queued emits only `subagents:failed` (see [docs/rpc.md](docs/rpc.md)). Leaving it unset starts the agent immediately regardless of that limit, though not of the [process budget](#process-budget) when it is on. `maxConcurrentForeground` never applies here whatever `isBackground` says: it bounds only spawns a caller is blocking on inline, and every RPC spawn is detached. A top-level RPC spawn renders in the widget and FleetView while it runs, with the same live tool activity and turn counter an `Agent`-tool spawn gets — only an explicit `isBackground: false` is dropped by the widget's default `background` mode, the way a foreground `Agent` call is. Nested spawns stay hidden from both.
|
|
1126
1190
|
|
|
1127
1191
|
`options.model` accepts either a `Model` object (e.g. `ctx.model`) or a `"provider/modelId"` string — strings are validated against `ctx.modelRegistry` at the RPC boundary and selected by the shared spawn path, which records the original input and source. Cross-extension callers can forward serializable values without losing auth context. Resolution is fuzzy, so a bare `"sonnet"` can land on a provider you never named: with [Model Scope](#model-scope) on, an override that resolves outside `enabledModels` is refused with an error envelope listing the allowed models, exactly as a caller-supplied `Agent({ model })` is. `null` means unset — the agent inherits, same as omitting the field.
|
|
1128
1192
|
|
|
@@ -1185,6 +1249,31 @@ Memory uses a `MEMORY.md` index file and individual memory files with frontmatte
|
|
|
1185
1249
|
|
|
1186
1250
|
The `disallowed_tools` field is respected when determining write capability — an agent with `tools: write` + `disallowed_tools: write` correctly gets read-only memory.
|
|
1187
1251
|
|
|
1252
|
+
## Plan mode
|
|
1253
|
+
|
|
1254
|
+
A child started while the parent session is in [`pi-plan-mode`](../plan-mode/README.md)'s Plan mode plans too, whatever its own tools say, as Claude Code keeps a subagent spawned in plan mode in plan mode. Before every spawn the manager asks Plan mode on the parent's `pi.events` (`pi-plan-mode:state-query`, answered synchronously); while Plan is active the answer carries Plan's tool policy and the child runs under it:
|
|
1255
|
+
|
|
1256
|
+
- `edit` and `write` are refused at call time, `bash` and `powershell` run only Plan's reviewed read-only commands (`git status` runs, `rm -rf x` does not), and extension tools run only when Plan would admit them (trusted `readOnlyHint`, or selected in the Plan policy). Tool schemas stay visible, as in the planner, so the child's prompt cache and a later resume are unaffected
|
|
1257
|
+
- The child never needs Plan mode loaded itself: the policy is enforced by this extension, including for `extensions: false` and `isolated` agents. Explore and Plan keep their read-only surfaces unchanged
|
|
1258
|
+
- Every spawn path asks: the `Agent` tool, `@handle` mentions, RPC, schedules created before Plan and `SubagentWorkflow` stages, each as it starts. Nested `allowed_subagents` children also inherit their owner's restriction, even after the parent leaves Plan
|
|
1259
|
+
- The decision is per run. A child spawned during Plan keeps the restriction after you approve and leave Plan, since it was briefed as a planner; resuming it after Plan gets its normal surface back, and resuming any agent during Plan restricts that run. Both hold for `Agent` `resume` and for `steer_subagent` on a finished agent, which resumes through the same path. An agent started before Plan keeps its surface while it runs
|
|
1260
|
+
- `isolation: "worktree"` is refused while planning: a worktree exists to make edits
|
|
1261
|
+
- Nothing Plan's model starts may outlive Plan unrestricted: `Agent` with `schedule` is refused while planning, and so is parking a quota-blocked background dispatch (it fails with the reset instead). Both would fire as fresh spawns after Plan
|
|
1262
|
+
- `steer_subagent` and the `subagents:rpc:steer` channel are refused while planning for a running or queued agent that runs without Plan's restrictions (one started before Plan), so a steer from Plan's model or another extension's tool cannot drive it into edits. Its running gate is never changed underneath it: wait for it to finish and steer it then (that resumes it read-only), stop it with `stop_subagent` (Plan admits it and never asks the gate), or leave Plan first. An agent already running under Plan stays steerable
|
|
1263
|
+
- A malformed answer to the state query (wrong version, no boolean `active`) never counts as Plan being off: it restricts like an unusable one. That is how a version-skewed `pi-plan-mode` (a future v2) or another extension answering `pi-plan-mode:state-query` shows up: the child is limited to `read`, `grep`, `find` and `ls`, its other calls fail with `Plan mode is active and sent no tool policy this subagent understands`, and schedules, parking and steers of pre-Plan agents are refused, even with Plan off
|
|
1264
|
+
- The child's prompt says it is in Plan mode, and its expanded `Agent` row and conversation viewer tags read `plan`
|
|
1265
|
+
- Extension tools are classified from the child's own tool metadata (`sourceInfo`): a tool the planner selected is admitted only from the same source path, so a project-scope copy, or a project-scope `readOnlyHint`, stays blocked
|
|
1266
|
+
- An answer saying Plan is active but carrying no policy this extension understands fails closed to `read`, `grep`, `find` and `ls`. Without `pi-plan-mode` nothing answers and nothing changes
|
|
1267
|
+
- Plan mode and this extension shake hands (`pi-plan-mode:gate-query`): Plan blocks `Agent`, `SubagentWorkflow` and `steer_subagent` while active unless this gate answers (only those three, by name: `get_subagent_result`, `stop_subagent` and any other tool that starts no child stay available in Plan), and once Plan mode has shown itself a state query nobody answers refuses the spawn (a resume runs fail-closed). Needs `@nredd/pi-plan-mode` newer than `0.59.3-nredd.3` with this extension newer than `0.22.0-nredd.8`; update both together
|
|
1268
|
+
|
|
1269
|
+
What Plan does not reach:
|
|
1270
|
+
|
|
1271
|
+
- A child queued before Plan starts later with its full surface: the decision was made when it was spawned. Plan's model cannot steer it meanwhile
|
|
1272
|
+
- A schedule created before Plan fires during Plan as a fresh spawn, and is restricted then like any other
|
|
1273
|
+
- Steering from `/agents`, the conversation viewer and `@handle` is the user, not Plan's model, and is not refused
|
|
1274
|
+
- The handshake is hardening, not a sandbox. Every extension shares `pi.events`, and a well-formed `{ version: 1, active: false }` reply from one of them cannot be told apart from Plan's own. Any active answer still wins, but a spoofed one hides a Plan that stayed silent, so the child runs unrestricted. Extension code runs in-process with full access anyway
|
|
1275
|
+
- A nested child of a pre-Plan owner is restricted when Plan is on at its own spawn
|
|
1276
|
+
|
|
1188
1277
|
## Worktree Isolation
|
|
1189
1278
|
|
|
1190
1279
|
Set `isolation: worktree` to run an agent in a temporary git worktree:
|
|
@@ -1193,11 +1282,13 @@ Set `isolation: worktree` to run an agent in a temporary git worktree:
|
|
|
1193
1282
|
Agent({ subagent_type: "refactor", prompt: "...", isolation: "worktree" })
|
|
1194
1283
|
```
|
|
1195
1284
|
|
|
1196
|
-
The agent gets a full, isolated copy of the repository.
|
|
1285
|
+
The agent gets a full, isolated copy of the repository. On successful cleanup the worktree directory is removed; whether a branch is left behind depends on the work:
|
|
1197
1286
|
- **No changes:** worktree is cleaned up automatically, no branch
|
|
1198
|
-
- **Changes made:** changes are committed to a new branch (`pi-agent-<id>`), and the result names the branch and the `git merge` command for it.
|
|
1287
|
+
- **Changes made:** changes are committed to a new branch (`pi-agent-<id>`), and the result names the branch and the `git merge` command for it. After successful cleanup the branch is the only artifact — the worktree path is gone, so nothing points into it
|
|
1199
1288
|
- **Agent committed its own work:** the branch is created at the agent's HEAD, preserving its commits (uncommitted leftovers are committed on top first)
|
|
1200
1289
|
|
|
1290
|
+
If Git cannot inspect, commit, create the branch, or remove the worktree, cleanup stops without force-removing it. The result reports the error and worktree path (and the branch if already created); inspect and recover the tree before removing it. A failed status check does not establish whether changes exist. Git removal also refuses uncommitted work that appears after the status check. Git refuses to remove any worktree with submodule state (a gitlink in the index, registered in `.gitmodules` or not, or a `modules` dir in the worktree's git dir), so such a worktree is removed with `--force` only after the outer tree and every nested repository re-verify clean; that is decided from the repository state, not git's locale-dependent message. Status checks pass `--untracked-files=all` and removal pins `status.showUntrackedFiles=all`, so a `status.showUntrackedFiles=no` config cannot hide new files from cleanup. A nested repository with uncommitted files (untracked included), a stash, or commits no remote-tracking ref contains, on a branch, a tag or HEAD (all deleted with the worktree), keeps the worktree and reports `submodule work would be lost on removal`. Offline a fetched tag cannot be told from a local one, so a tag on an unpublished commit fails safe. Nested clones under a gitignored path, which no gitlink records, are found through `git status --ignored` and inspected the same way, including ones nested inside a submodule or another ignored clone. Status checks also pass `--ignore-submodules=none`, so `submodule.<name>.ignore` or `diff.ignoreSubmodules` cannot hide a moved gitlink; if `git add -A` then stages nothing (it skips a gitlink hidden by `ignore = all`), cleanup keeps the worktree and says the changes could not be staged. Status checks first clear `assume-unchanged` and `skip-worktree` index bits (a skip-worktree file absent from disk is left alone), so an edit those bits hide is committed, not deleted; a regular file with a mode change staged in the index (vs HEAD, or on a newly added file) that the file on disk no longer matches (e.g. `update-index --chmod`, or a `chmod` after `git add`; a plain unstaged `chmod` is committed normally) cannot be told apart from a later on-disk change and `add -A` would silently restage the disk mode, so cleanup fails safe before staging: it keeps the worktree and reports `mode staged only in the index; worktree kept:` with the paths (skipped under `core.fileMode=false`). Path listings used for these decisions (`ls-files`, `ls-tree`, `diff --raw`, `status --ignored`) run with `core.quotePath=true` and without `-z`, and quoted names are decoded back by `unquoteGitPath`, so a multibyte name split across `pi.exec`'s output chunks cannot be mis-decoded to U+FFFD; nested git dirs are read from `.git` rather than `rev-parse` output. A name git prints raw non-ASCII or that is not valid UTF-8 cannot be inspected, so the worktree is kept with an error. A submodule HEAD at the sha the base commit records is not at risk, and neither is a leftover module git dir at a recorded sha. A cleanup failure marks a normally successful agent as failed, including a structured workflow child; a stopped agent stays stopped and reports the failure.
|
|
1291
|
+
|
|
1201
1292
|
The agent's system prompt names the worktree as an isolated copy and tells it to work only there, even if other instructions name the main checkout — otherwise an inherited parent prompt or a task prompt mentioning the project path walks it straight back out of the copy. This is a directive, not a sandbox: an agent with shell access can still `cd` out, so don't rely on `isolation` alone to protect the main checkout.
|
|
1202
1293
|
|
|
1203
1294
|
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.
|
|
@@ -1234,25 +1325,11 @@ skills: api-conventions, error-handling
|
|
|
1234
1325
|
---
|
|
1235
1326
|
```
|
|
1236
1327
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
| Scope | Path | Source |
|
|
1240
|
-
|---|---|---|
|
|
1241
|
-
| Project | `<cwd>/.pi/skills/` | Pi-standard |
|
|
1242
|
-
| Project | `<cwd>/.agents/skills/` | [Agent Skills spec](https://agentskills.io/integrate-skills) |
|
|
1243
|
-
| User | `$PI_CODING_AGENT_DIR/skills/` (default `~/.pi/agent/skills/`) | Pi-standard |
|
|
1244
|
-
| User | `~/.agents/skills/` | [Agent Skills spec](https://agentskills.io/integrate-skills) |
|
|
1245
|
-
| User | `~/.pi/skills/` | Legacy (pre-Pi) |
|
|
1246
|
-
|
|
1247
|
-
**Per root, a skill named `foo` resolves to the first of:**
|
|
1248
|
-
|
|
1249
|
-
- `<root>/foo.md` — flat file at the top level
|
|
1250
|
-
- `<root>/foo/SKILL.md` — directory skill (top-level)
|
|
1251
|
-
- `<root>/*/.../foo/SKILL.md` — directory skill, found by recursive descent
|
|
1328
|
+
Named preloads use Pi's configured-resource resolution and parser. Trusted project discovery, the configured agent directory, and installed package manifests use Pi's precedence; a trusted project's ancestor `.agents/skills` roots participate when Pi discovers them. Pi-disabled resources, including `skills: ["-skills/review"]`, remain disabled and cannot return through compatibility lookup. Compatibility lookup otherwise retains the historical user roots (`<home>/.agents/skills`, `<home>/.pi/skills`) and explicit top-level `<name>.md` files. A package skill hidden behind a rejected project symlink alias is recovered through a scratch-cwd Pi resolution, so the project's package filters (`skills` allowlists and exclusions, relative or absolute sources) still decide whether it loads. Package discovery remains inspect-only: unavailable configured npm or Git sources are skipped, never installed or lifecycle-executed.
|
|
1252
1329
|
|
|
1253
|
-
|
|
1330
|
+
**Name matching:** Pi matches a skill by its frontmatter `name:`. The compatibility lookup runs only when Pi finds no enabled or disabled skill of that name, and matches by file or directory name instead, so a `review/` directory declaring `name: other` answers both `other` and `review`. Per root, `foo` resolves to the first of `<root>/foo.md`, `<root>/foo/SKILL.md`, then `<root>/*/.../foo/SKILL.md` by recursive descent. Descent skips dotfile directories and `node_modules`, never enters a directory that has its own `SKILL.md`, and is byte-order sorted. An unreadable match is skipped and the walk continues. Names with path-traversal characters are rejected: anything but a leading letter or digit followed by letters, digits, `.`, `_` or `-`, or longer than 128 chars. The project compatibility roots (`.pi/skills`, each `.agents/skills`) are walked only for a trusted project.
|
|
1254
1331
|
|
|
1255
|
-
**
|
|
1332
|
+
**Project links:** automatically discovered project roots reject symlinks at `<cwd>/.pi`, `.pi/skills`, each ancestor `.agents`, `.agents/skills`, and their skill entries. This filter runs before this extension accepts project candidates; a separate untrusted Pi resolution restores only a user or package candidate that Pi's realpath deduplication hid behind a rejected project alias, retaining Pi's package-identity precedence. A candidate whose path cannot be canonicalized (raced away or unreadable) is never restored, and the rest of the preload continues. User compatibility roots and installed package resources retain Pi's normal link handling, so a project or global package's `skills/<name>` symlink that points outside the package is followed and injected. The former loader refused links under project roots only; packages were never covered by that refusal, and this matches Pi. An explicit injected home excludes the runtime process's ambient `~/.agents/skills` result while preserving `<home>/.agents/skills`, so ambient content cannot enter a child prompt. The host still scans the ambient location; this extension filters returned resources rather than providing filesystem isolation.
|
|
1256
1333
|
|
|
1257
1334
|
## Tool Denylist
|
|
1258
1335
|
|
|
@@ -1298,17 +1375,20 @@ src/
|
|
|
1298
1375
|
nested-tools.ts # Delegation tools handed to subagents (nested spawn/collect/steer)
|
|
1299
1376
|
process-budget.ts # Process-wide running-agent budget shared by every manager (maxActiveSubagents)
|
|
1300
1377
|
child-context.ts # AsyncLocalStorage flag marking work done for a child session
|
|
1378
|
+
plan-mode-children.ts # Plan mode for children spawned while the parent plans (#437)
|
|
1301
1379
|
abortable.ts # Race a wait against Esc without cancelling the background child
|
|
1302
1380
|
model-call.ts # In-flight model call tracking: thinking / writing / stalled?
|
|
1303
1381
|
completion-outbox.ts # Deliver completion notifications at the parent's next boundary, coalesced
|
|
1382
|
+
notification-result.ts # How much of a result a notice carries: per-agent cap, batch budget, head+tail clip
|
|
1304
1383
|
clock.ts # Shared scheduler/outbox/admission/retention/UI clock and native timers
|
|
1305
1384
|
detached-results.ts # Session-scoped spool and registry for background agents surviving reload/switch
|
|
1306
1385
|
group-join.ts # `group` join mode: one notification per turn's batch, with timeout
|
|
1307
1386
|
status-note.ts # Honest status note + salvaged partial output for non-normal outcomes
|
|
1387
|
+
error-message.ts # The text a caught value reports: an Error's message, else String(value)
|
|
1308
1388
|
usage.ts # Token usage shapes, accumulators, session-stats readers
|
|
1309
1389
|
|
|
1310
1390
|
# Invocation surface
|
|
1311
|
-
invocation-config.ts # Shared tool-parameter schemas (isolation,
|
|
1391
|
+
invocation-config.ts # Shared tool-parameter schemas (isolation, cwd, thinking, ...)
|
|
1312
1392
|
model-selection.ts # Shared precedence, scope and resolution outcome; stored on the agent record
|
|
1313
1393
|
model-tiers.ts # Exact static/routed tiers, admission, policy and durable disclosure
|
|
1314
1394
|
model-resolver.ts # Model matching: exact provider/modelId, aliases, fuzzy fallback ranked by version
|
|
@@ -1326,9 +1406,10 @@ src/
|
|
|
1326
1406
|
home-paths.ts # Injected home normalization and Pi agent-directory defaults
|
|
1327
1407
|
resource-loader.ts # Fresh scoped settings paths for Pi's real resource/package loader
|
|
1328
1408
|
memory.ts # Persistent agent memory (resolve, read, build prompt blocks)
|
|
1329
|
-
skill-loader.ts # Preload skills
|
|
1409
|
+
skill-loader.ts # Preload named skills through Pi's configured resource discovery (trust-gated, inspect-only)
|
|
1330
1410
|
output-file.ts # Streaming output file transcripts for agent sessions
|
|
1331
1411
|
worktree.ts # Git worktree isolation (create, cleanup, prune)
|
|
1412
|
+
git-path.ts # Decode git's C-quoted paths (`unquoteGitPath`) for worktree cleanup
|
|
1332
1413
|
prompts.ts # Config-driven system prompt builder
|
|
1333
1414
|
context.ts # Parent conversation context for inherit_context
|
|
1334
1415
|
settings.ts # Persistent settings (~/.pi/agent/subagents.json + .pi/subagents.json)
|