@nredd/pi-subagents 0.0.0-stage → 0.22.0-nredd.10
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/LICENSE +21 -0
- package/README.md +1444 -2
- package/dist/chunks/chunk-MSGGFB5Q.ts +6755 -0
- package/dist/chunks/chunk-MSGGFB5Q.ts.map +7 -0
- package/dist/chunks/conversation-viewer-A7AMRDVY.ts +15 -0
- package/dist/chunks/conversation-viewer-A7AMRDVY.ts.map +7 -0
- package/dist/index.ts +9874 -0
- package/dist/index.ts.map +7 -0
- package/package.json +82 -4
- package/src/abortable.ts +43 -0
- package/src/agent-color.ts +161 -0
- package/src/agent-file-toggle.ts +298 -0
- package/src/agent-manager.ts +2431 -0
- package/src/agent-runner.ts +1703 -0
- package/src/agent-source-badge.ts +75 -0
- package/src/agent-types.ts +346 -0
- package/src/child-context.ts +33 -0
- package/src/clock.ts +25 -0
- package/src/completion-outbox.ts +144 -0
- package/src/context.ts +58 -0
- package/src/cross-extension-rpc.ts +200 -0
- package/src/custom-agents.ts +441 -0
- package/src/default-agents.ts +126 -0
- package/src/detached-results.ts +150 -0
- package/src/enabled-models.ts +181 -0
- package/src/enterprise-local.ts +67 -0
- package/src/env.ts +33 -0
- package/src/error-message.ts +11 -0
- package/src/git-path.ts +66 -0
- package/src/group-join.ts +142 -0
- package/src/home-paths.ts +27 -0
- package/src/index.ts +5047 -0
- package/src/invocation-config.ts +158 -0
- package/src/memory.ts +179 -0
- package/src/mention-clone.ts +200 -0
- package/src/mention.ts +141 -0
- package/src/model-call.ts +83 -0
- package/src/model-resolver.ts +333 -0
- package/src/model-scope.ts +71 -0
- package/src/model-selection.ts +170 -0
- package/src/model-tiers.ts +176 -0
- package/src/nested-tools.ts +512 -0
- package/src/notification-result.ts +118 -0
- package/src/output-file.ts +186 -0
- package/src/package-resources.ts +639 -0
- package/src/plan-mode-children.ts +176 -0
- package/src/process-budget.ts +346 -0
- package/src/prompts.ts +161 -0
- package/src/quota-admission.ts +121 -0
- package/src/resource-loader.ts +65 -0
- package/src/schedule-store.ts +153 -0
- package/src/schedule.ts +514 -0
- package/src/settings.ts +851 -0
- package/src/skill-loader.ts +288 -0
- package/src/stale-ctx.ts +16 -0
- package/src/startup-error.ts +29 -0
- package/src/status-note.ts +113 -0
- package/src/structured-output.ts +130 -0
- package/src/types.ts +487 -0
- package/src/ui/agent-mention.ts +219 -0
- package/src/ui/agent-row.ts +245 -0
- package/src/ui/agent-widget.ts +722 -0
- package/src/ui/conversation-viewer.ts +837 -0
- package/src/ui/fallback-flag.ts +114 -0
- package/src/ui/fleet-list.ts +1020 -0
- package/src/ui/fleet-state.ts +154 -0
- package/src/ui/notification-view.ts +139 -0
- package/src/ui/schedule-menu.ts +105 -0
- package/src/ui/select-item.ts +45 -0
- package/src/ui/viewer-keys.ts +39 -0
- package/src/ui/workflow-card.ts +459 -0
- package/src/ui/workflow-dialog.ts +1138 -0
- package/src/ui/workflow-menu.ts +193 -0
- package/src/usage.ts +167 -0
- package/src/workflow/collisions.ts +123 -0
- package/src/workflow/entry.ts +47 -0
- package/src/workflow/host.ts +398 -0
- package/src/workflow/journal.ts +176 -0
- package/src/workflow/json-schema.ts +129 -0
- package/src/workflow/meta.ts +329 -0
- package/src/workflow/progress.ts +554 -0
- package/src/workflow/runtime.ts +1249 -0
- package/src/workflow/saved.ts +259 -0
- package/src/workflow/task.ts +317 -0
- package/src/workflow/tool-description.ts +200 -0
- package/src/workflow/worker-source.ts +903 -0
- package/src/worktree.ts +520 -0
- package/src/xml.ts +13 -0
package/README.md
CHANGED
|
@@ -1,3 +1,1445 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nredd/pi-subagents
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@nredd/pi-subagents` is maintained in [`nredd/pi-harness`](https://github.com/nredd/pi-harness/tree/main/packages/subagents). It was imported with history from [`nredd/pi-subagents`](https://github.com/nredd/pi-subagents) commit [`526fa2df76ba85df31bd14a843fae370e8efe78a`](https://github.com/nredd/pi-subagents/commit/526fa2df76ba85df31bd14a843fae370e8efe78a), tagged `v0.22.0-nredd.5`, which is rebased on [`tintinweb/pi-subagents`](https://github.com/tintinweb/pi-subagents) master commit `ca93a5247e72e7053fcd10bea7125d6c4205f949`. Original authorship and the MIT `LICENSE` are retained; subsequent local changes are maintained by nredd.
|
|
4
|
+
|
|
5
|
+
A [pi](https://pi.dev) extension that brings **Claude Code-style autonomous sub-agents and workflow orchestration** to pi. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level. Run them in the background (the default) or block on them, steer them mid-run, resume completed sessions, and define your own custom agent types. When the orchestration shouldn't be improvised, hand a deterministic JavaScript script to the `SubagentWorkflow` tool — `agent()`, `parallel()`, `pipeline()` — and scripts written for Claude Code's `Workflow` tool run here unchanged.
|
|
6
|
+
|
|
7
|
+
<img width="600" alt="pi-subagents screenshot" src="https://github.com/tintinweb/pi-subagents/raw/master/media/screenshot.png" />
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
|
|
11
|
+
|
|
12
|
+
<img width="600" alt="pi-color-badges-white" src="https://github.com/user-attachments/assets/555dcae4-333e-4ff0-b420-7b3369c018a4" />
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Claude Code look & feel** — same tool names, calling conventions, and UI patterns (`Agent`, `get_subagent_result`, `steer_subagent`, `stop_subagent`) — feels native
|
|
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
|
+
- **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). `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
|
+
- **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
|
+
- **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
|
+
- **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`, `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
|
+
- **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
|
+
- **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
|
+
- **Mid-run steering** — inject messages into running agents to redirect their work without restarting
|
|
28
|
+
- **Session resume** — pick up where an agent left off, preserving full conversation context. Resumes detached by default and notifies you on completion, just like a fresh spawn; pass `run_in_background: false` to block and get the result inline
|
|
29
|
+
- **Graceful turn limits** — agents get a "wrap up" warning before hard abort, producing clean partial results instead of cut-off output
|
|
30
|
+
- **Case-insensitive agent types** — `"explore"`, `"Explore"`, `"EXPLORE"` all work. A type that doesn't resolve to exactly one *enabled* agent — unknown, disabled, or ambiguous between two agents differing only by case — falls back to general-purpose with a note, or is refused outright under [`fallbackSubagent: none`](#persistent-settings)
|
|
31
|
+
- **Fuzzy model selection** — specify models by name (`"haiku"`, `"sonnet"`) instead of full IDs, with automatic filtering to only available/configured models
|
|
32
|
+
- **Context inheritance** — optionally fork the parent conversation into a sub-agent so it knows what's been discussed
|
|
33
|
+
- **Persistent agent memory** — three scopes (project, local, user) with automatic read-only fallback for agents without write tools
|
|
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 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
|
+
- **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
|
|
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
|
|
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
|
|
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)**
|
|
41
|
+
- **Schedule subagents** — pass `schedule` to the `Agent` tool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results arrive as the same completion notification as a manual background agent; manage via `/agents → Scheduled jobs`
|
|
42
|
+
- **Model scope enforcement** — opt-in validation that subagent model choices stay within your pi `enabledModels` allowlist (sourced from `/scoped-models`, with both global and project-local pi settings honored). Caller-supplied out-of-scope → hard error to orchestrator; frontmatter-pinned out-of-scope → warning + runs anyway (frontmatter authoritative). Toggle via `/agents → Settings → Scope models`
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pi install npm:@nredd/pi-subagents
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or load directly for development:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pi -e ./src/index.ts
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Requires pi **1.1.0 or newer**: pi 0.86 and 0.87 moved the system prompt and conversation history into the session transcript, and the `@handle` conversation clone and the agent transcript files are built on that model. The `peerDependencies` range declares it, so npm flags an older pi at install time.
|
|
57
|
+
|
|
58
|
+
### In-process factory, clock and home
|
|
59
|
+
|
|
60
|
+
`src/index.ts` exports `createSubagentsExtension(options: SubagentsExtensionOptions = {}): ExtensionFactory`.
|
|
61
|
+
The default export is `createSubagentsExtension()` and keeps native time and timer reference behavior.
|
|
62
|
+
For an in-process host, pass `createSubagentsExtension({ clock })` as an inline extension. No setting or
|
|
63
|
+
process-global clock patch is needed. The same clock object can be shared with session-kit and its
|
|
64
|
+
`now` method with the subscription router:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { createSubagentsExtension, type Clock } from "@nredd/pi-subagents/src/index.ts";
|
|
68
|
+
|
|
69
|
+
// Host-provided clock; use () => clock.now() when passing `now` separately.
|
|
70
|
+
declare const clock: Clock;
|
|
71
|
+
const extension = createSubagentsExtension({ clock });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`SubagentsExtensionOptions` accepts `clock?: Clock` and `homeDir?: string`. An explicit home binds
|
|
75
|
+
user-scoped discovery, settings and child-session paths without mutating `HOME`; omitted homes keep
|
|
76
|
+
call-time host defaults. Explicit agent/session directory overrides still win. The boundary audit and
|
|
77
|
+
test seams are documented [here](docs/home-injection.md).
|
|
78
|
+
|
|
79
|
+
The exported clock contract matches session-kit's scheduling boundary:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
interface TimerHandle { cancel(): void; unref?(): void }
|
|
83
|
+
interface Clock {
|
|
84
|
+
now(): number;
|
|
85
|
+
setTimeout(callback: () => void, ms: number): TimerHandle;
|
|
86
|
+
setInterval(callback: () => void, ms: number): TimerHandle;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Cancellation wraps native `clearTimeout` / `clearInterval`; the supplied clock owns whether its timers
|
|
91
|
+
are referenced or unreferenced. The default clock does not add or remove `unref()` calls. Clock callbacks
|
|
92
|
+
must be deferred, not invoked synchronously by `setTimeout` / `setInterval`.
|
|
93
|
+
|
|
94
|
+
The clock controls relative schedule parsing, ISO validation, one-shot/interval dispatch, quota
|
|
95
|
+
re-parking, the Agent admission timeout, manager retention and lifecycle timestamps, model-call
|
|
96
|
+
activity/stall ages, UI repaint tickers and the 200ms idle completion hold. Cron remains owned by Croner, including
|
|
97
|
+
validation and next-run calculation; this seam deliberately leaves its existing behavior untouched.
|
|
98
|
+
`SubagentScheduler` also accepts `{ clock }` directly, and `CompletionOutbox` accepts it as its third
|
|
99
|
+
constructor argument. Static `detectSchedule(s, now?)` and `parseRelativeTime(s, now?)` accept epoch
|
|
100
|
+
milliseconds; omitted `now` uses `Date.now()`.
|
|
101
|
+
|
|
102
|
+
This is not a whole-extension virtual clock: cron, scheduled re-admission's RPC timeout, group-join
|
|
103
|
+
holds, workflow execution timers, child-shutdown watchdogs, detached-spool filenames, core scrollbar
|
|
104
|
+
auto-hide and schedule-store lock retries retain native time. Queued result waits use the manager's
|
|
105
|
+
start gate when present; legacy queued resumes retain the clock-driven fallback.
|
|
106
|
+
|
|
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
|
|
120
|
+
files (`test/{session-scenarios,fleet-session,viewer-session,worktree-isolation-e2e}.test.ts`) run under
|
|
121
|
+
both engines on core `v1.3.0-nredd.10` release tarballs. They use exported `createHarness`
|
|
122
|
+
and `createInteractiveHarness`/`VirtualTerminal`, real child sessions and git, one injected extension
|
|
123
|
+
clock, promise-held tools/streams and raw SGR mouse input. Homes, settings and provider credentials
|
|
124
|
+
are fixtures; no installed pi, tmux or real-clock sleeps are needed. Existing old-host strict-mode
|
|
125
|
+
coverage explicitly removes the optional SDK export; the shared faux runtime uses the host's real
|
|
126
|
+
`withBeforeProviderRequest` facade. Maintainer guidance on public index callback
|
|
127
|
+
coverage and retained safety guards is [here](../../docs/subagents-index-continuation.md).
|
|
128
|
+
|
|
129
|
+
### Other hosts
|
|
130
|
+
|
|
131
|
+
This extension is developed and tested against [pi](https://pi.dev).
|
|
132
|
+
|
|
133
|
+
Third-party adapters report running it elsewhere. These are maintained independently of this project: not tested here, not covered by our CI, and compatibility may break with any release.
|
|
134
|
+
|
|
135
|
+
- **DeepSeek Harness (`dsh`)** — via an adapter that maps pi's host API onto native DSH agents. Details and reports: [#258](https://github.com/tintinweb/pi-subagents/issues/258)
|
|
136
|
+
|
|
137
|
+
## Quick Start
|
|
138
|
+
|
|
139
|
+
The parent agent spawns sub-agents using the `Agent` tool:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
Agent({
|
|
143
|
+
subagent_type: "Explore",
|
|
144
|
+
prompt: "Find all files that handle authentication",
|
|
145
|
+
description: "Find auth files",
|
|
146
|
+
run_in_background: true,
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
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).
|
|
151
|
+
|
|
152
|
+
### Sending a foreground agent to the background
|
|
153
|
+
|
|
154
|
+
Press `ctrl+b` while a foreground `Agent` call is running to hand it to the background, as in Claude Code. The same run keeps going — same session, in-flight turn, transcript and worktree — and the blocked call returns the agent ID with the same result a `run_in_background: true` spawn gets. Completion then arrives as the normal background notification, naming the original tool call. After 2 seconds the running call's row shows `(ctrl+b to run in background)`.
|
|
155
|
+
|
|
156
|
+
- `ctrl+b` hands off every running foreground `Agent` call at once. To hand off just one, press `b` in its conversation viewer (FleetView or `/agents → Running agents`).
|
|
157
|
+
- The key is claimed only while such a call is running and the prompt editor has focus; the rest of the time — and in dialogs or the viewer's steering composer — `ctrl+b` is still pi's cursor-left. With FleetView disabled the extension cannot see which component has focus, so it claims the key whenever a call is running.
|
|
158
|
+
- One hint shows per batch of parallel calls, on the earliest one still running.
|
|
159
|
+
- Esc in the parent no longer reaches a handed-off agent. Stop it from `/agents → Running agents` with `x`.
|
|
160
|
+
- It is one-way: there is no way back to foreground.
|
|
161
|
+
- Other tool calls in the same assistant message still hold the model's next turn until they finish; pi runs a message's tool calls as one batch.
|
|
162
|
+
- Not offered for a foreground call still queued for a slot, a foreground `resume`, a workflow's agents or nested children. There is no automatic timeout.
|
|
163
|
+
|
|
164
|
+
### Scheduling
|
|
165
|
+
|
|
166
|
+
Add a `schedule` field to register the agent to fire later instead of running now:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
Agent({
|
|
170
|
+
subagent_type: "Explore",
|
|
171
|
+
prompt: "Look at recent commits and summarize what changed since last week",
|
|
172
|
+
description: "Weekly commit review",
|
|
173
|
+
schedule: "0 0 9 * * 1", // 9am every Monday (6-field cron)
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Schedule formats:
|
|
178
|
+
|
|
179
|
+
- **Cron** — 6-field (`second minute hour day-of-month month day-of-week`), e.g. `"0 0 9 * * 1"` for 9am every Monday, `"0 */15 * * * *"` for every 15 minutes.
|
|
180
|
+
- **Interval** — `"5m"`, `"1h"`, `"30s"`, `"2d"`. Fires repeatedly at that interval.
|
|
181
|
+
- **One-shot relative** — `"+10m"`, `"+2h"`, `"+1d"`. Fires once at that future time.
|
|
182
|
+
- **One-shot absolute** — full ISO timestamp, e.g. `"2026-12-25T09:00:00.000Z"`.
|
|
183
|
+
|
|
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.
|
|
185
|
+
|
|
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.
|
|
187
|
+
|
|
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.
|
|
189
|
+
|
|
190
|
+
**Disable the feature entirely**: `/agents → Settings → Scheduling → disabled` removes `schedule` from the `Agent` tool spec (no LLM-context cost), hides the menu entry, and stops any active scheduler. The schema-level removal takes effect on the next pi session; the runtime kill is immediate. Re-enable from the same menu.
|
|
191
|
+
|
|
192
|
+
Restrictions:
|
|
193
|
+
- `schedule` cannot be combined with `inherit_context` (no parent conversation exists at fire time) or `resume` (schedules create fresh agents).
|
|
194
|
+
- `run_in_background: false` is refused — scheduled jobs always run in the background. Omitting it, or passing `true`, is fine.
|
|
195
|
+
- Scheduled fires bypass the `maxConcurrent` queue so a 5-minute interval cannot be deferred behind long-running manual agents.
|
|
196
|
+
- **Headless `pi -p` doesn't wait for scheduled subagents.**
|
|
197
|
+
|
|
198
|
+
### Quota admission
|
|
199
|
+
|
|
200
|
+
With a subscription router installed (e.g. [pi-subscription-router](https://github.com/nredd/pi-subscription-router)), a fresh `Agent` dispatch first asks it whether the model is out of included quota, over `pi.events`: `router:rpc:admit` `{ requestId, model: "provider/id" }`, replied on `router:rpc:admit:reply:<requestId>` as `{ success, data: { ok, resolvedModel?, resetAt?, wait? } }` (`resolvedModel` labels the admitted model for tiers; `wait: false` is the router's exhaustion policy saying fail now, absent means wait).
|
|
201
|
+
|
|
202
|
+
- Admitted, or no router answers within 100ms: the agent starts as usual
|
|
203
|
+
- Refused, background, reset known, `wait` not `false`, scheduling on: the dispatch is parked as a one-shot scheduled job a minute after the reset, persisted like any schedule. When it fires it asks again and re-parks at the new reset while still blocked; if the refusal can no longer park (`wait: false`, no reset, or a reset already past) the job is not started and ends with `lastStatus: "error"` and an error event. Only an explicit admission, or no router answering, starts it. If pi was closed through the reset, it asks again 10s after the session starts, once the router has warmed its usage cache
|
|
204
|
+
- Refused otherwise (foreground, `inherit_context`, scheduling off, unknown reset, or `wait: false`): the call fails with the reset time and no agent starts
|
|
205
|
+
|
|
206
|
+
Live resumes keep their existing selection. Tier requests are admitted on every fresh entry path, including RPC, mentions, nested and workflow children, and every scheduled fire. Only top-level `Agent` background calls and scheduled fires can persist a quota wait; other refused starts error. A `router/*` virtual model still routes and fails over inside the child.
|
|
207
|
+
|
|
208
|
+
## UI
|
|
209
|
+
|
|
210
|
+
The extension renders a persistent widget above the editor showing active agents. By default it shows background runs only (`widgetMode: background`) — foreground agents already render inline as the `Agent` tool result, so the widget would otherwise double-render them. Switch to `all` (every agent) or `off` (hide the widget) via `/agents → Settings → Widget`:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
● Agents
|
|
214
|
+
├─ ⠹ Agent Refactor auth module · ↻5≤30 · 5 tool uses · 33.8k token (62%) · 12.3s
|
|
215
|
+
│ ⎿ editing 2 files…
|
|
216
|
+
├─ ⠹ Explore Find auth files · ↻3 · 3 tool uses · 12.4k token (8%) · 4.1s
|
|
217
|
+
│ ⎿ searching…
|
|
218
|
+
├─ ⠹ Agent Long-running task · ↻42 · 38 tool uses · 91.0k token (84% · ⇊2) · 2m17s
|
|
219
|
+
│ ⎿ reading…
|
|
220
|
+
└─ 2 queued
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The token field is annotated with two optional signals inside parens:
|
|
224
|
+
- **`NN%`** — context-window utilization (color-coded: <70% dim, 70–85% warning, ≥85% error). Omitted when the model has no declared `contextWindow`, or briefly right after compaction.
|
|
225
|
+
- **`⇊N`** — number of times the session has compacted, when > 0. Stays dim; the percent's color carries urgency.
|
|
226
|
+
|
|
227
|
+
### The `Agent` tool row
|
|
228
|
+
|
|
229
|
+
pi 1.0 folds every collapsed tool row to `<first line of the call> · <first line of the result>` and draws the `▸`/`▾` marker itself, so the `Agent` row's call and result each put what matters on their first line and draw no marker:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
Plan design the cache · ⠹ thinking · 2m41s · 12s · 3.2k token collapsed, running
|
|
233
|
+
Plan design the cache · ✓ done · 41s · 33.8k token collapsed, finished
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The status, elapsed time and tokens are read from the agent record on every render (and the host is asked to repaint once a second while it runs), so the row keeps moving after a background launch instead of freezing on `Running in background`. Expanded, the row shows the prompt, the model and thinking tags (`↳ opus 4.6 · thinking: high`), the transcript link, then the result.
|
|
237
|
+
|
|
238
|
+
### FleetView
|
|
239
|
+
|
|
240
|
+
FleetView starts **collapsed**, rendering exactly one line below the editor, with no overflow rows or blank spacer. Always-collapsed is deliberate: it saves space even for one agent and avoids resizing when a fan-out crosses a threshold. `ctrl+shift+a` toggles it; the settled explicit choice is recorded as a versioned `subagents:fleet-state` custom entry, outside model context.
|
|
241
|
+
|
|
242
|
+
Auto-repeat events are ignored. A 300ms quiet period coalesces rapid toggles (net-no-change bursts write nothing); shutdown flushes the final choice before the activation loses its context. This bounds writes per burst, not all-time size of the host's append-only journal. The last valid choice on the active branch survives ticks, widget resets, `/reload`, and restarting/resuming that session. A fresh session defaults to collapsed. Temporary arrow navigation writes no state entry. This uses the fleet's existing focus-aware terminal-input handler, like its arrows and the foreground `ctrl+b` handoff; there is no subagent key-rebinding setting. Todo's `ctrl+shift+t`, monitors' `ctrl+shift+m`, and pi's `ctrl+o` keep their existing roles.
|
|
243
|
+
|
|
244
|
+
```text
|
|
245
|
+
agents: 3 running, 2 queued, 4 done · ~$28.70 · ctrl+shift+a to expand
|
|
246
|
+
[1 failed, 2 parked, 1 fallback] agents: 3 running, 1 failed, 2 parked · ctrl+shift+a to expand
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Counts cover top-level records, including running starts and queued/waiting agents without a session, plus workflow containers (not their children). `steered` counts as done; errors and aborted runs count as failed; stopped runs stay distinct. Successful/stopped records retain the existing four-second linger.
|
|
250
|
+
|
|
251
|
+
Unacknowledged errors and aborted runs do not expire from the fleet: the UI journals minimal failure identities and acknowledgements, replayed from the active session branch on reload or disk resume. Terminal callbacks and detached-result ingress record failures explicitly, including failures between render ticks. It retains at most 128 detailed alerts (id, agent type, short sanitized description, terminal state and timestamp), never complete records/results/sessions; four durable per-kind overflow counters preserve exact outstanding totals beyond that cap. Overflow contributes to both collapsed and expanded headers, but has no individual navigation rows. Specific overflow ids can still be acknowledged through result consumption. The existing session journal is the identity authority: overflow deduplication/acknowledgement scans it backwards in O(branch length) time and O(1) extra space; checkpoints restore bounded state without replaying all failures. No additional file/global registry or unbounded UI id/ack set is kept. Live record and workflow-owner observation uses weak keys, not retained records; recreated workflow display projections do not rescan acknowledged/overflow identities every tick. Each failure/ack transition writes one bounded checkpoint (up to 128 minimal details plus counters), trading journal bytes for bounded checkpoint replay; no all-time compaction is claimed. Old failures cannot leave a healthy-looking header or hide the widget.
|
|
252
|
+
|
|
253
|
+
Acknowledged means the existing `resultConsumed` flag was set by inline result delivery, `get_subagent_result`, RPC consume, or successfully closing the failed agent's FleetView conversation. A session-less/restored failure is acknowledged by selecting it to read its error or minimal-metadata toast. Restored workflow failures without a live inspector use the same toast acknowledgement. Merely delivering a completion notice, ticking, or a viewer-open failure does not acknowledge it. Failed workflow containers stay until their inspector closes successfully; paused runs stay blocked until resumed/stopped.
|
|
254
|
+
|
|
255
|
+
Enabled quota-parked schedules awaiting dispatch count as parked even with no agent record, and clear when dispatched, disabled or removed.
|
|
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
|
+
|
|
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.
|
|
260
|
+
|
|
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.
|
|
262
|
+
|
|
263
|
+
Expanded, the existing navigable list renders **below** the editor:
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
esc to interrupt · ← for agents · ↓ to manage · ctrl+shift+a to collapse
|
|
267
|
+
|
|
268
|
+
● main
|
|
269
|
+
○ workflow audit-src 12/40 agents · 32s · ↓ 26.4k tokens
|
|
270
|
+
○ general-purpose Sleep then report 1 ▶ running · 11s · ↓ 13.1k tokens
|
|
271
|
+
○ general-purpose Sleep then report 2 ▶ running · 11s · ↓ 13.1k tokens
|
|
272
|
+
↓ 3 more: 2 running, 1 failed
|
|
273
|
+
```
|
|
274
|
+
|
|
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.
|
|
277
|
+
|
|
278
|
+
### Fullscreen conversation observer
|
|
279
|
+
|
|
280
|
+
In fullscreen mode (pi's default), opening an agent from FleetView, `/agents`, or the workflow inspector fills the terminal. Regular mode keeps the centered floating viewer and keyboard controls. Neither mode replaces the main session or pauses either agent. Escape returns to the previous view; while composing, the first Escape cancels the draft.
|
|
281
|
+
|
|
282
|
+
In pi's `/settings → TUI mode → fullscreen`, the observer supports:
|
|
283
|
+
|
|
284
|
+
- Mouse-wheel scrolling. Scrolling up pauses automatic following; reaching the bottom resumes it.
|
|
285
|
+
- A single-column scrollbar using pi's `│` track, `┃` thumb, and theme colors. The thumb becomes `█` while hovered or dragged. Track clicks page; holding and moving also drags.
|
|
286
|
+
- pi's `/settings → Fullscreen scrollbar` preference applies to the observer: `auto` appears on scrolling or hovering and hides after one second of inactivity; `always` reserves a column and stays visible even when the conversation fits; `hidden` removes the rail and its mouse target. The observer reads the main session's settings when opened, without changing them.
|
|
287
|
+
- Clickable footer controls for steering, stopping, Markdown mode, and closing. Stopping still takes two presses, as with `x`.
|
|
288
|
+
- Keyboard navigation and steering as before. Mouse scrolling also works while composing a steering message.
|
|
289
|
+
- The bottom divider stays pinned above the footer (and steering composer when open). While scrolled away, a centered `[ ↓ N new messages · Ctrl+End ]` indicator within the divider counts new conversation messages, not streaming updates. Before any arrive, it reads `[ ↓ Jump to latest message · Ctrl+End ]`. Click it or press `Ctrl+End` to resume following and clear the count, without losing an open steering draft.
|
|
290
|
+
|
|
291
|
+
Observer mouse events are consumed, including at scroll limits, so they do not scroll, select, or click the main view underneath. Closing restores focus without editing the main prompt. The main conversation can still advance as its agent runs.
|
|
292
|
+
|
|
293
|
+
This is an initial implementation. Mouse text selection, copy-on-select, link activation, and switching agents without leaving the observer are not implemented. pi's global shortcuts remain host-owned: its fullscreen search searches the main transcript, not the observer. The scrollbar, jump indicator, and mouse controls apply only to fullscreen mode; regular mode retains the original viewer.
|
|
294
|
+
|
|
295
|
+
### Live activity
|
|
296
|
+
|
|
297
|
+
While a model call is in flight, the widget, the fleet row and the conversation viewer say what it is doing instead of `thinking…` for as long as it takes:
|
|
298
|
+
|
|
299
|
+
| Shown | Meaning |
|
|
300
|
+
|---|---|
|
|
301
|
+
| `thinking · 2m41s` | the call has been thinking (no text yet) for 2m41s |
|
|
302
|
+
| `writing · 3.2k tok` | it is writing text or tool-call arguments; the figure is an estimate (characters / 4) until the provider reports usage |
|
|
303
|
+
| `stalled? · 2m41s` | no stream event of any kind for 2 minutes (the time is the silence, not the call) |
|
|
304
|
+
|
|
305
|
+
Tools running take precedence (`reading…`, `running command…`). Activity is tracked for spawns made through the `Agent` tool, RPC and mentions, and for foreground and background resumes. Each resume gets fresh text/model-call tracking and turn counts; physical-model labels follow the assistant messages.
|
|
306
|
+
|
|
307
|
+
### Conversation viewer input
|
|
308
|
+
|
|
309
|
+
The fullscreen observer uses pi's wheel delta, draggable scrollbar and clickable footer controls (see above). Regular mode keeps the centered floating box and keyboard controls. Both stream the reply being written before it lands in the transcript, and repaint once a second while the agent runs so activity timers count.
|
|
310
|
+
|
|
311
|
+
### Agent mentions
|
|
312
|
+
|
|
313
|
+
Subagents are addressable. Every agent has a typeable handle — the agent type, lowercased, numbered when instances collide (`explore`, `explore-2`) — and `@handle <message>` at the prompt talks to that agent, whatever state it happens to be in. Type `@` to pick one:
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
❯ @
|
|
317
|
+
@auth-audit send message · Explore · running · audit the auth flow
|
|
318
|
+
@explore-2 send message · running · find flaky tests
|
|
319
|
+
@code-review resume · code-review · check the diff
|
|
320
|
+
@plan start agent · Software architect agent for designing implementation plans.
|
|
321
|
+
index.ts src/index.ts ← pi's own file rows, still there
|
|
322
|
+
index.d.ts dist/index.d.ts
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The handle names the **agent**, not one process, so a single syntax covers its whole lifecycle:
|
|
326
|
+
|
|
327
|
+
| State | `@explore fix the flaky test` does |
|
|
328
|
+
|-------|-----------------------------------|
|
|
329
|
+
| running or queued | sends the message into its conversation, exactly as `steer_subagent` would |
|
|
330
|
+
| finished | **resumes** it in the background from its existing session, continuing where it left off |
|
|
331
|
+
| finished long ago, record gone | **reopens** its session from disk and continues there |
|
|
332
|
+
| never started | **starts** it — by default, through a clone of this conversation ([below](#starting-a-new-agent)) |
|
|
333
|
+
|
|
334
|
+
No turn is ever spent in the main conversation, and nothing about the mention enters the chat. The answer comes back as the ordinary background-completion notification, so the main model can relay it.
|
|
335
|
+
|
|
336
|
+
#### Starting a new agent
|
|
337
|
+
|
|
338
|
+
Claude Code does not start a mentioned agent itself. `@agent-<type>` becomes an *attachment* appending a `<system-reminder>` to your prompt — "the user has expressed a desire to invoke the agent X; please invoke the agent appropriately, passing in the required context to it" — and the main model makes the tool call. There is no tool forcing and no allowed-tools narrowing: the mention constrains *which* agent, not what it is told. So the model writes the agent's prompt, giving it the conversation context a cold spawn lacks.
|
|
339
|
+
|
|
340
|
+
The cost is a visible turn — the model's reasoning and its tool block, narrating a decision you already made by typing the handle. This extension keeps the mechanism and moves it off-screen. The conversation is copied into a throwaway in-memory session, that clone takes the turn holding only the `Agent` tool, and what it starts is an ordinary top-level agent:
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
@cyan whats your favorite color → (nothing in the chat)
|
|
344
|
+
└─ clone of this conversation, off-screen
|
|
345
|
+
└─ Agent(subagent_type: "cyan", prompt: …)
|
|
346
|
+
▸ Cyan Agent favorite color ← widget, fleet row, handle
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
It is a literal clone — the session's own entries and the same system prompt, not [`inherit_context`](#agent-frontmatter)'s text rendering of them — taken from memory and compaction-aware, so what the copy reads is what the main model is working from. The clone gets one tool and one job; it cannot read, write or run anything, because an invisible turn with the full toolset could do invisible work. The agent it starts is attributed to the *real* session, so its transcript and `rootSessionId` land where they would have anyway, and it carries no `tool-use-id` — the main conversation never issued one.
|
|
350
|
+
|
|
351
|
+
| Mode | `@plan sketch the migration`, with no Plan agent running |
|
|
352
|
+
|------|----------------------------------------------------------|
|
|
353
|
+
| `model` (default) | a clone of this conversation takes the turn off-screen and calls `Agent`, so the agent starts with a prompt **written from the conversation**. Nothing reaches the chat but a `Prompting @plan…` toast — the wording marks the wait for that turn, where `direct`'s `Started @plan` means it is already running |
|
|
354
|
+
| `direct` | the agent starts here, immediately, with your message verbatim as its prompt. No model call at all, so no latency before it begins |
|
|
355
|
+
| `off` | `@` means only "attach a file" again |
|
|
356
|
+
|
|
357
|
+
Either way the started agent honours its own frontmatter — `model:`, `thinking:`, `max_turns:` all apply, since neither path passes them and the agent's config wins. Mentioning something as the very first thing in a session works: there is simply no history to carry, and the clone still runs on your model and system prompt. If it cannot deliver at all — a model can always answer in prose instead of calling the tool — the agent is started directly with your text and the toast says so, rather than leaving you with nothing running.
|
|
358
|
+
|
|
359
|
+
`model` is also the only mode that works outside the TUI: `pi -p '@plan the migration'` clones, spawns, and reports through the normal completion path, where a direct start would have detached the agent and printed nothing. Messaging and resuming stay TUI-only for that reason, in both modes.
|
|
360
|
+
|
|
361
|
+
Two things to weigh against `direct`: the clone re-sends the whole conversation, and the agent does not start until that turn finishes.
|
|
362
|
+
|
|
363
|
+
**Named agents.** The `Agent` tool takes an optional `name`, so the orchestrator can call one `auth-audit` instead of leaving you to tell `@explore-2` from `@explore-3`. A name is *additive*: the type-derived handle is still assigned, so `@explore` keeps reaching that agent rather than starting a second one beside it. Both names share one namespace — an alias can never shadow a live handle or the reverse — and the popup shows one row per agent, under its alias, with the type moved into the description. `steer_subagent` and `get_subagent_result` accept a handle too, so you and the model address agents the same way.
|
|
364
|
+
|
|
365
|
+
**Resuming much later.** Because subagent sessions are persisted by default ([`rememberAgents`](#persistent-settings)), a handle keeps working after the agent's in-memory record is evicted: `@explore anything else?` reopens the conversation from disk. Only the *definition* is re-resolved, so a continuation runs under the agent type's current frontmatter, not the one the first run used. If the type has since been deleted or disabled, the resume is refused rather than falling back to another agent — re-enable it and the handle works again. Names from an evicted agent stay reserved, so a later Explore becomes `explore-2` rather than shadowing something you can still reach; the 100 most recent are kept, and all of them are forgotten on `/new` and session switch. A resumed agent takes those names back, so `@explore` keeps meaning the same conversation. An agent whose session was only ever in memory leaves nothing to reopen, and the mention starts a fresh one instead; if the session file has since been deleted, the mention says so and frees the handle rather than silently sending your message to a new agent.
|
|
366
|
+
|
|
367
|
+
The grammar mirrors Claude Code's, and is deliberately narrow so nothing gets swallowed by accident:
|
|
368
|
+
|
|
369
|
+
| Input | Goes to |
|
|
370
|
+
|-------|---------|
|
|
371
|
+
| `@explore fix the flaky test` | the `explore` agent |
|
|
372
|
+
| `@agent-explore fix the flaky test` | the same agent — Claude Code's manual spelling, accepted as a synonym |
|
|
373
|
+
| `@main @explore is not a mention` | the main model, with `@main ` stripped — the escape hatch |
|
|
374
|
+
| `@explore` (no message) | the main model — a bare handle is never a send |
|
|
375
|
+
| `hey @explore look at this` | the main model — only a **leading** mention is routed |
|
|
376
|
+
| `@src/index.ts summarize this` | the main model, with pi's normal file attachment |
|
|
377
|
+
| `@nosuchagent hello` | the main model, verbatim — no agent, no type, no interception |
|
|
378
|
+
|
|
379
|
+
While an agent is live its handle addresses *it*, so `@explore` never starts a second Explore alongside a running one — use the `Agent` tool for deliberate parallelism. `@<agent-id>` works too. `main` is reserved and can never be an agent's handle (a type slugging to it gets `main-2`); handles are capped at 64 characters. A handle written as typed always wins over the `@agent-` form, so an agent genuinely called `agent-explore` stays reachable. [Nested subagents](#nested-subagents) are not addressable — they are hidden from every top-level surface and only their owner may steer them, so a handle that would name one starts a fresh top-level agent instead of reaching through that boundary. Suggestions list live agents first, then resumable ones, then startable types — and then pi's own file rows, in the same popup: `@` stays the file picker it always was, and the handles are added to it rather than replacing it. Disable the whole thing via `/agents → Settings → Agent mentions`.
|
|
380
|
+
|
|
381
|
+
A `direct`-mode start takes the non-tool spawn path shared with the scheduler and cross-extension RPC, so — like those — it writes no `.output` transcript. That is the trade for skipping the model call: a `model`-mode start goes through the real `Agent` tool and keeps everything. Live tool activity and the turn counter are *not* part of that trade — a direct start renders them like any other agent. A mention-*resumed* agent goes through the full resume wiring and keeps both in either mode.
|
|
382
|
+
|
|
383
|
+
Individual agent results render Claude Code-style in the conversation:
|
|
384
|
+
|
|
385
|
+
| State | Example |
|
|
386
|
+
|-------|---------|
|
|
387
|
+
| **Running** | `⠹ ↻3≤30 · 3 tool uses · 12.4k token (8%)` / `⎿ searching, reading 3 files…` |
|
|
388
|
+
| **Completed** | `✓ ↻8 · 5 tool uses · 33.8k token (62%) · 12.3s` / `⎿ Done` |
|
|
389
|
+
| **Wrapped up** | `✓ ↻50≤50 · 50 tool uses · 89.1k token (84% · ⇊2) · 45.2s` / `⎿ Wrapped up (turn limit)` |
|
|
390
|
+
| **Stopped** | `■ ↻3 · 3 tool uses · 12.4k token (8%)` / `⎿ Stopped` |
|
|
391
|
+
| **Error** | `✗ ↻3 · 3 tool uses · 12.4k token (8%)` / `⎿ Error: timeout` |
|
|
392
|
+
| **Aborted** | `✗ ↻55≤50 · 55 tool uses · 102.3k token (95% · ⇊3)` / `⎿ Aborted (max turns exceeded)` |
|
|
393
|
+
|
|
394
|
+
Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.
|
|
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
|
+
|
|
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):
|
|
399
|
+
|
|
400
|
+
```
|
|
401
|
+
✓ Find auth files · 3 tools · 12.4k token · ~$0.0042 · 4.1s collapsed
|
|
402
|
+
3 agents finished · ✓ a, ✓ b, ✗ c collapsed, grouped
|
|
403
|
+
```
|
|
404
|
+
|
|
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.
|
|
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
|
+
|
|
409
|
+
## Default Agent Types
|
|
410
|
+
|
|
411
|
+
| Type | Tools | Model | Prompt Mode | Description |
|
|
412
|
+
|------|-------|-------|-------------|-------------|
|
|
413
|
+
| `general-purpose` | all 7 | inherit | `append` (parent twin) | Inherits the parent's full system prompt — same rules, CLAUDE.md, project conventions |
|
|
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) |
|
|
416
|
+
|
|
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.
|
|
418
|
+
|
|
419
|
+
Default agents can be **ejected** (`/agents` → select agent → Eject) to export them as `.md` files for customization, **overridden** by creating a `.md` file with the same name (e.g. `.pi/agents/general-purpose.md`), or **disabled** per-project with `enabled: false` frontmatter.
|
|
420
|
+
|
|
421
|
+
## Custom Agents
|
|
422
|
+
|
|
423
|
+
Define custom agent types by creating `.md` files. The frontmatter `name:` is the `subagent_type` and dispatch identity, falling back to the filename when absent; `display_name` only changes the UI label. Claiming a default agent's name overrides it.
|
|
424
|
+
|
|
425
|
+
Agents are discovered from four locations (higher priority wins):
|
|
426
|
+
|
|
427
|
+
| Priority | Location | Scope |
|
|
428
|
+
|----------|----------|-------|
|
|
429
|
+
| 1 (highest) | `.pi/agents/<name>.md` | Project — pi's config dir; authoritative, and where `/agents` writes |
|
|
430
|
+
| 2 | `.agents/agents/<name>.md` | Project — the shared cross-tool `.agents` workspace (same convention as `.agents/skills/`) |
|
|
431
|
+
| 3 | `$PI_CODING_AGENT_DIR/agents/<name>.md` (default `~/.pi/agent/agents/<name>.md`) | Global — available everywhere |
|
|
432
|
+
| 4 (lowest) | Paths declared by an installed pi package | Package — see [Shipping agents in a pi package](#shipping-agents-in-a-pi-package) |
|
|
433
|
+
|
|
434
|
+
**Project agents need project trust.** `.pi/agents/` and `.agents/agents/` are project-supplied, so they load only when pi trusts the project (`/trust`, a saved decision, `defaultProjectTrust: "always"`, or `--approve`). In an untrusted project (including `--no-approve`) both roots are skipped unread: they cannot override or disable a global or package agent, and a malformed file there neither warns nor aborts a `strictAgentFiles` startup. Global agents and packages configured globally stay available; packages configured only by the project's `.pi/settings.json` are gated by the same decision. The decision is pi's canonical one, `ctx.isProjectTrusted()` at `session_start` (so `--approve`/`--no-approve` and `/trust` override a saved `trust.json`), which this extension never reads itself. Activation runs before that answer exists, so it loads global and globally configured package agents only; the first `session_start` loads the project roots and re-registers the `Agent` tool so its description and `subagent_type` list match, before any provider turn. This gates reading project *agent definitions* (and a project `agent-tool-description.md`); the extension still reads operational project settings such as `.pi/subagents.json` before trust is known. A nested subagent applies its parent's decision, including after a spawn with a custom `cwd`: the decision for the parent's config root travels with that root through every later generation, and never extends to the custom working directory. The `/agents` menu writes new project agents to `.pi/agents/`, which an untrusted project does not list. Trusted projects keep the order above.
|
|
435
|
+
|
|
436
|
+
Project-level agents override global ones with the same name, so you can customize a global agent for a specific project. If both project locations define the same name, **`.pi/agents/` wins** — `.pi` stays the project authority; `.agents/agents/` is an additional read location for projects that keep their agent assets in the `.agents` workspace. The global location follows the upstream `PI_CODING_AGENT_DIR` env var — set it to relocate all pi-coding-agent state (agents, skills, settings) to a custom directory. An agent's name is its frontmatter `name:`, falling back to the filename, so two files can now claim the same one — the later load wins, and the warning below names the file that took over.
|
|
437
|
+
|
|
438
|
+
An unreadable or unparseable agent file is skipped, not fatal — a warning names the file and the error. If it was overriding a same-named agent, a second line names the file that loads instead. Set `strictAgentFiles: true` in `subagents.json` (or `/agents → Settings → Strict agent files`) to fail startup on a broken file instead; mid-session reloads still only warn. Strict validation runs in two phases. At activation, a malformed global agent file fails the factory as an ordinary load error; a malformed file from a package (even one configured globally) only warns, since it is a third party's file and not the user's to fix. At the first canonical `session_start`, once trust is known, the trusted project roots are loaded and validated, and a malformed file there throws the host's `ExtensionStartupError` (an untrusted project's files are never read, so they never trip it). The host class is needed for the second phase: on an older host with `strictAgentFiles: true` the extension refuses at activation with an upgrade message, before any agent file is read, rather than degrading to warnings. Non-strict use works on any host in the peer range.
|
|
439
|
+
|
|
440
|
+
### Example: `.pi/agents/auditor.md`
|
|
441
|
+
|
|
442
|
+
```markdown
|
|
443
|
+
---
|
|
444
|
+
color: red
|
|
445
|
+
description: Security Code Reviewer
|
|
446
|
+
tools: read, grep, find, bash
|
|
447
|
+
model: anthropic/claude-opus-4-6
|
|
448
|
+
thinking: high
|
|
449
|
+
max_turns: 30
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
You are a security auditor. Review code for vulnerabilities including:
|
|
453
|
+
- Injection flaws (SQL, command, XSS)
|
|
454
|
+
- Authentication and authorization issues
|
|
455
|
+
- Sensitive data exposure
|
|
456
|
+
- Insecure configurations
|
|
457
|
+
|
|
458
|
+
Report findings with file paths, line numbers, severity, and remediation advice.
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Then spawn it like any built-in type:
|
|
462
|
+
|
|
463
|
+
```
|
|
464
|
+
Agent({ subagent_type: "auditor", prompt: "Review the auth module", description: "Security audit" })
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Frontmatter Fields
|
|
468
|
+
|
|
469
|
+
All fields are optional — sensible defaults for everything.
|
|
470
|
+
|
|
471
|
+
| Field | Default | Description |
|
|
472
|
+
|-------|---------|-------------|
|
|
473
|
+
| `description` | filename | Agent description shown in tool listings |
|
|
474
|
+
| `name` | filename | **The agent's type** — what `subagent_type` and `@handle` address. Claude Code's rule: the filename doesn't have to match, so `blubb.md` with `name: code-review` dispatches as `code-review`. Omit it and the filename is used. Any value works except one containing `:`, which Claude Code reserves for plugin-scoped identifiers — such a file is skipped with a warning. Two files may declare the same name; the later load wins, as a filename clash always did |
|
|
475
|
+
| `display_name` | the type | Label shown in the UI (widget, agent list, badges) — cosmetic only, and independent of `name`. Claude Code has no equivalent; a file that sets only `name` badges as its type, unchanged |
|
|
476
|
+
| `color` | — | Background color for the agent name badge in the Agent tool header, widget, FleetView, and conversation viewer. Supports Claude Code's `red`, `blue`, `green`, `yellow`, `purple`, `orange`, `pink`, `cyan` (the values its own default theme uses); quoted six-digit hex such as `"#8B5CF6"`; and Agency Agents aliases (`amber`, `teal`, `indigo`, `gold`, `neon-green`, `neon-cyan`, `metallic-blue`, `violet`, `rose`, `lime`, `gray`/`grey`, `fuchsia`, `slate`, `navy`). Badge text is black or white, whichever clears 4.5:1 against the rendered background — Claude Code uses one inverse color for every badge. Invalid values render no badge and preserve each surface's existing theme foreground |
|
|
477
|
+
| `tools` | all 7 | Which tools the agent can call. Built-in names (`read, grep, …`), `*` / `all` (all built-ins), `none`, and `ext:<extension>` / `ext:<extension>/<tool>` selectors for extension tools. See [Tool & extension scoping](#tool--extension-scoping) below |
|
|
478
|
+
| `extensions` | `true` | Which extensions to load for the agent. `true` (all defaults), `false` (none), or an explicit list: `[mcp, "/abs/path.ts", "*"]`. See [Tool & extension scoping](#tool--extension-scoping) below |
|
|
479
|
+
| `exclude_extensions` | — | Extension denylist applied after `extensions:` — exclude wins. Plain names only (case-insensitive), no paths or `*`. Useful with `extensions: true` to drop one extension (e.g. `pi-notify`) |
|
|
480
|
+
| `skills` | `true` | `true` inherits the parent's skills; `false` inherits none. A comma-separated list preloads **only** those skills into the system prompt and does not inherit the rest (see [Skill Preloading](#skill-preloading) for discovery locations) |
|
|
481
|
+
| `memory` | — | Persistent agent memory scope: `project`, `local`, or `user`. Auto-detects read-only agents |
|
|
482
|
+
| `disallowed_tools` | — | Comma-separated tools to deny even if extensions provide them |
|
|
483
|
+
| `isolation` | — | Set to `worktree` to run in an isolated git worktree, or `off` to refuse one even when the caller passes `isolation: "worktree"` (frontmatter is authoritative). Workflow scripts are the exception: as in Claude Code, only their `agent()` option decides. `none`, `no`, and `false` are accepted spellings of `off` |
|
|
484
|
+
| `model` | inherit parent | Default exact `provider/modelId`, [tier](#model-tiers) such as `@cheap`, or [legacy alias](#machine-local-model-aliases). An explicit `Agent({ model })` selection overrides this default. Configured values are never fuzzy matched |
|
|
485
|
+
| `thinking` | inherit | Default thinking level: off, minimal, low, medium, high, xhigh, max — an explicit `Agent({ thinking })` overrides it for that run. Actual availability depends on your pi version and model; pi clamps unsupported levels down |
|
|
486
|
+
| `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited |
|
|
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` |
|
|
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:` |
|
|
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 |
|
|
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 |
|
|
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) |
|
|
492
|
+
| `inherit_context` | `false` | Fork parent conversation into agent |
|
|
493
|
+
| `run_in_background` | — | Pin this agent to background (`true`) or foreground (`false`). Omit to follow `backgroundByDefault` |
|
|
494
|
+
| `isolated` | `false` | Hermetic specialist mode: forces `extensions: false` + `skills: false` + drops `ext:` selectors. Only built-in tools. Distinct from `isolation: worktree` (filesystem) |
|
|
495
|
+
| `enabled` | `true` | Set to `false` to disable an agent (useful for hiding a default agent per-project) |
|
|
496
|
+
|
|
497
|
+
Frontmatter is authoritative for `max_turns`, `inherit_context`, `run_in_background`, `isolated`, and `isolation`. An agent file's `model` and `thinking` are portable defaults: an explicit `Agent({ model, thinking })` selection overrides either for that run. With no explicit thinking level, the definition wins; if it has none either, no level is passed and pi picks one at session creation (the per-model setting, else `defaultThinkingLevel`, clamped to the model), which does not follow the parent's live level. Only the `@mention` clone path starts from the parent's `ctx.thinkingLevel`. Model selection additionally follows the [`agentModels`](#per-agent-models) precedence. Other `Agent` tool parameters only fill fields the agent config leaves unspecified. Other spawn paths differ per field:
|
|
498
|
+
|
|
499
|
+
- `isolated` holds on every spawn path, including `@handle` mentions, workflows and [cross-extension RPC](https://github.com/tintinweb/pi-subagents/blob/master/docs/rpc.md).
|
|
500
|
+
- `isolation` holds on every path except workflows: as in Claude Code, only the script's `agent()` option decides there.
|
|
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`.
|
|
502
|
+
|
|
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.
|
|
506
|
+
|
|
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.
|
|
508
|
+
|
|
509
|
+
### Nested subagents
|
|
510
|
+
|
|
511
|
+
Nested delegation is default-off. Set `allowed_subagents` only on a non-isolated custom agent that owns a real fan-out responsibility:
|
|
512
|
+
|
|
513
|
+
```yaml
|
|
514
|
+
---
|
|
515
|
+
tools: read, grep, find
|
|
516
|
+
extensions: false
|
|
517
|
+
allowed_subagents: support-file-finder, support-callsite-tracer # or `all`
|
|
518
|
+
---
|
|
519
|
+
```
|
|
520
|
+
|
|
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.
|
|
522
|
+
|
|
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.
|
|
524
|
+
|
|
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.
|
|
526
|
+
|
|
527
|
+
The hard cap is depth 2 by default: main session (0) → subagent (1) → nested child (2). Change it project-wide with `maxSubagentDepth` in `subagents.json` (or `/agents → Settings → Nested depth`); `0` or `1` turns nesting off everywhere. An agent already at the cap gets no nested tools at all — not even `get_subagent_result`, since it can never own a child. A child must independently set `allowed_subagents` to delegate again; isolated agents never receive nested tools.
|
|
528
|
+
|
|
529
|
+
Nested children occupy no slot in either per-session pool — their parent already holds one, and queueing them behind it would deadlock a parent waiting on its own child. They do take a lease from the [process budget](#process-budget) when it is on, which reserves slots for them so that cannot deadlock. The depth cap bounds how *deep* nesting goes, not how *wide*: a parent's only limit on concurrent children is that each spawn costs it a turn. Pair `allowed_subagents` with a `max_turns` on that agent if you want a hard ceiling on its fan-out.
|
|
530
|
+
|
|
531
|
+
**Depth-2 review delegation.** The default depth already allows main session (0) → implementation agent (1) → reviewer (2). Give the implementation agent an explicit allowlist naming only the reviewer, and leave the reviewer without `allowed_subagents`, so it is a leaf:
|
|
532
|
+
|
|
533
|
+
```yaml
|
|
534
|
+
---
|
|
535
|
+
# ~/.pi/agent/agents/implementer.md
|
|
536
|
+
allowed_subagents: code-reviewer
|
|
537
|
+
---
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
The implementer then calls `Agent({ subagent_type: "code-reviewer", ... })` in the foreground and gets the review inline. With the process budget on, its slot stays held while the reviewer runs, and the reserve keeps reviewers startable when every top-level slot is an implementer waiting on one.
|
|
541
|
+
|
|
542
|
+
Because a subagent session never activates this extension (that is what keeps a child from building a second agent manager, and it is why nested tools are injected directly instead), a subagent also gets none of the extension's other surfaces: no `/agents` command, no cross-extension RPC handlers, no `subagents:ready` event.
|
|
543
|
+
|
|
544
|
+
### Tool & extension scoping
|
|
545
|
+
|
|
546
|
+
`extensions:` decides **which extensions load**, `tools:` decides **which tools surface to the LLM**. They compose:
|
|
547
|
+
|
|
548
|
+
```yaml
|
|
549
|
+
# Default (both omitted): all extensions load, all 7 built-ins surface
|
|
550
|
+
|
|
551
|
+
tools: read, grep, find # narrow to listed built-ins; extensions still load
|
|
552
|
+
tools: "*" # all 7 built-ins (alias: `all`)
|
|
553
|
+
tools: none # zero built-ins (alias: `""`)
|
|
554
|
+
tools: "*, ext:mcp/search" # built-ins plus one extension tool
|
|
555
|
+
|
|
556
|
+
extensions: false # no extensions load
|
|
557
|
+
extensions: [mcp] # only mcp loads
|
|
558
|
+
extensions: [builtin:mcp, builtin:codemode] # pi's built-in MCP + codemode
|
|
559
|
+
extensions: ["*", "/abs/foo.ts"] # all defaults plus one path-loaded extension
|
|
560
|
+
|
|
561
|
+
exclude_extensions: pi-notify # everything except pi-notify (with extensions: true)
|
|
562
|
+
exclude_extensions: builtin:mcp # everything except pi's built-in MCP
|
|
563
|
+
|
|
564
|
+
# Specialist: load one extension, expose only one of its tools, keep built-ins
|
|
565
|
+
extensions: [mcp]
|
|
566
|
+
tools: "*, ext:mcp/search"
|
|
567
|
+
|
|
568
|
+
isolated: true # hermetic: built-ins only, no extensions/skills/context
|
|
569
|
+
```
|
|
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
|
+
|
|
573
|
+
A few rules the examples don't make obvious:
|
|
574
|
+
|
|
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.
|
|
576
|
+
- Any `ext:` entry flips extension tools to an explicit allowlist — unnamed extensions still load (handlers fire) but expose no tools. So `tools: "*, ext:mcp/search"` exposes only `search` from `mcp`, nothing from any other extension.
|
|
577
|
+
- Extension names match case-insensitively (`[Mcp]` = `[mcp]`); tool names in `ext:foo/bar` stay case-sensitive.
|
|
578
|
+
- pi's built-in extensions (`builtin:mcp`, `builtin:codemode`, `builtin:tool-search`) load in subagents as they do in the main session, so with `extensions: true` **every subagent starts its own set of `mcp.json` servers**, which stay up until the subagent's session is disposed (on quit, or ~10 minutes after it finishes). Address them by the full `builtin:` name — bare `[mcp]` matches a third-party extension named `mcp`, not the built-in — and narrow their tools with `ext:builtin:mcp/<tool>`. MCP tools are reached through codemode (or `tool_search` for `deferred` servers), so an `ext:` allowlist must name that bridge too, as pi's `--tools read,codemode,'mcp__*'` does: `tools: "read, ext:builtin:codemode, ext:builtin:mcp/mcp__radius__search"`. Opt out per agent with `exclude_extensions: builtin:mcp`, or everywhere with `-builtin:mcp` in pi's `extensions` setting; starting pi with `--no-mcp` keeps MCP out of subagents too, and `--no-extensions` keeps all three built-ins out. MCP tools stay undeclared until codemode or `tool_search` reaches them, as in the main session, and a codemode script can only call tools inside the agent's scope. Out-of-scope tools are blocked but can still appear in codemode's `searchTools()` and `tool_search` results.
|
|
579
|
+
- Subagents inherit the parent session's **project trust**: in a project you did not trust, a subagent loads none of its `.pi` extensions or settings and never starts the servers in its `.pi/mcp.json`. A subagent spawned into another directory (RPC `cwd`) treats that directory as untrusted.
|
|
580
|
+
- Extensions that register tools **lazily** work too. MCP-backed extensions typically can't enumerate their tools until their servers connect, so they register from `session_start` or `before_agent_start` rather than at load. Subagent scoping is re-derived as tools appear, so these surface normally — including under `ext:` selectors, which keep narrowing correctly no matter when a tool shows up.
|
|
581
|
+
- Extensions bound into a subagent see **both ends** of that session's lifecycle: `session_start` when the agent starts, `session_shutdown` (reason `quit`) when its session is disposed — on quit, and when its record is evicted (10 minutes after its result was consumed, 60 after it finished if nobody read it). Release per-session resources there; anything left armed outlives the session it belongs to. Handlers are given three seconds on quit, after which teardown proceeds regardless.
|
|
582
|
+
- An installed **package** extension matches by its package short name (`@scope/pi-subagents` → `[pi-subagents]`), in addition to its path-derived name (a package whose entry is `src/index.ts` also answers to `[src]`). A directory entry counts too: `pi-web-access` declares `./dist`, pi loads `dist/index.js`, and it answers to `[pi-web-access]` as well as `[dist]`. Prefer the package name — the path-derived one is incidental.
|
|
583
|
+
- Plain `tools:` typos fail loudly: `tools: reed, grep` fires `tools-error:…` instead of silently producing an under-tooled agent.
|
|
584
|
+
- `exclude_extensions:` wins over `extensions:` and over `ext:` selectors — an excluded extension never loads and a `tools: ext:` entry can't pull it back. Plain names only (no paths, no `*`); a name matching nothing fires an `extension-error:…` warning.
|
|
585
|
+
- `exclude_extensions:` is **not a sandbox**: excluded extensions' factory code still executes once during loading. Exclusion suppresses their tools and their bound lifecycle hooks (`pi.on` handlers like `session_start` only fire for extensions bound to the session), but not other load-time side effects — a factory that subscribes directly to the shared `pi.events` bus stays live. Don't rely on it to contain an untrusted extension.
|
|
586
|
+
- Array and string forms are equivalent: `[a, b]` == `"a, b"`.
|
|
587
|
+
|
|
588
|
+
**How an agent's scope is advertised.** The Agent tool description lists every available agent with a `(Tools: …)` suffix, and that suffix is what the orchestrator reads when deciding where to route work. It names the **built-in scope** and flags that extension tools load too, without listing them (extensions may register lazily, see above, so they can't be enumerated when the description is built):
|
|
589
|
+
|
|
590
|
+
| `tools:` | extensions loading | suffix |
|
|
591
|
+
|---|---|---|
|
|
592
|
+
| omitted, `*`, or `all` | yes | `* + extension tools` |
|
|
593
|
+
| omitted, `*`, or `all` | `extensions: false` or `isolated: true` | `*` |
|
|
594
|
+
| a list of built-ins | yes | e.g. `read, grep + extension tools` |
|
|
595
|
+
| a list of built-ins | no | that list, e.g. `read, grep` |
|
|
596
|
+
| `none` with `isolated: true` or `extensions: false` | no | `none` |
|
|
597
|
+
| `none`, or only `ext:` entries | yes | `no built-ins, extension tools only` |
|
|
598
|
+
|
|
599
|
+
When the effective built-in set lacks `bash` (including `disallowed_tools: bash`), the shared suffix adds `; no built-in shell: cannot run bash commands`. This applies to every type in full, compact and custom-template type lists. It does not claim extensions cannot execute commands: an extension may supply `ssh_exec` or another command tool. Put stronger restrictions in the agent description's **first sentence** so compact mode preserves them, e.g. `no shell/ssh: cannot run commands`. Shell-less prompts should require a FIRST-turn explanation, before tool calls, when a task needs unavailable commands. This is prompt guidance, not a dispatcher classifier.
|
|
600
|
+
|
|
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`.
|
|
602
|
+
|
|
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.
|
|
604
|
+
|
|
605
|
+
### Shipping agents in a pi package
|
|
606
|
+
|
|
607
|
+
A pi package can ship subagents the same way it ships skills, so a reusable agent travels with the package that needs it instead of being copied into every project's `.pi/agents/` ([#109](https://github.com/tintinweb/pi-subagents/issues/109)). Declare the paths in the package's `package.json`:
|
|
608
|
+
|
|
609
|
+
```json
|
|
610
|
+
{
|
|
611
|
+
"name": "my-subagents",
|
|
612
|
+
"keywords": ["pi-package"],
|
|
613
|
+
"pi": {
|
|
614
|
+
"subagents": {
|
|
615
|
+
"agents": ["./agents"],
|
|
616
|
+
"workflows": ["./workflows"]
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Users get the agents by installing the package with pi — no copy step:
|
|
623
|
+
|
|
624
|
+
```bash
|
|
625
|
+
pi install npm:my-subagents # or git:…, or a local path; -l for project scope
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Four rules define the feature:
|
|
629
|
+
|
|
630
|
+
- **Declaration is required.** Pi falls back to convention directories (`skills/`, `prompts/`, …) for a package with no `pi` key at all; this extension never does. A package carrying an `agents/` folder for some other tool contributes nothing until it names it — the author's half of the agreement, where installing is yours.
|
|
631
|
+
- **Only pi's own packages count.** Discovery reads `packages[]` from pi's `settings.json`, global and project, through pi's package manager. `node_modules` is never scanned, and a package listed only by an untrusted project stays invisible until you trust it.
|
|
632
|
+
- **Package agents rank last.** Any `.pi/agents/`, `.agents/agents/` or global file of the same name wins, so a package can offer a `reviewer` but never take the name away from you. Same tier pi gives package-provided skills, and Claude Code plugin agents.
|
|
633
|
+
- **They are read-only in `/agents`,** badged `▪`, offering *Eject* (an editable copy that shadows the original) and *Disable* (an `enabled: false` stub that does the same).
|
|
634
|
+
|
|
635
|
+
`packageAgents` and `packageWorkflows` in [`subagents.json`](#persistent-settings) default to `true`; set either to `false` or to a list of package names to narrow it. Installing a package is already the trust decision — pi runs that package's `extensions/` and puts its `skills/` in the system prompt with no further prompt, so a declared `.md` agent is strictly less privileged than what it already runs.
|
|
636
|
+
|
|
637
|
+
**Full guide:** [`docs/packages.md`](https://github.com/tintinweb/pi-subagents/blob/master/docs/packages.md) — manifest syntax and the four accepted spellings, `!` exclusions, the development loop, naming, precedence, and troubleshooting.
|
|
638
|
+
|
|
639
|
+
## Tools
|
|
640
|
+
### `Agent`
|
|
641
|
+
|
|
642
|
+
Launch a sub-agent.
|
|
643
|
+
|
|
644
|
+
| Parameter | Type | Required | Description |
|
|
645
|
+
|-----------|------|----------|-------------|
|
|
646
|
+
| `prompt` | string | yes | The task for the agent |
|
|
647
|
+
| `description` | string | yes | Short 3-5 word summary (shown in UI) |
|
|
648
|
+
| `name` | string | no | Memorable name for this agent (`auth-audit`), addressable as `@name` and accepted by `steer_subagent`/`get_subagent_result`. Additive — the type-derived handle is still assigned |
|
|
649
|
+
| `subagent_type` | string | yes | Agent type (built-in or custom) |
|
|
650
|
+
| `model` | string | no | Model — `provider/modelId`, [machine-local alias](#machine-local-model-aliases), or fuzzy name (`"haiku"`, `"sonnet"`). Resolved tolerantly (`.`/`-` and a trailing date stamp interchangeable) with provider fallback |
|
|
651
|
+
| `thinking` | string | no | Thinking level: off, minimal, low, medium, high, xhigh, max (availability depends on pi version and model) |
|
|
652
|
+
| `max_turns` | number | no | Max agentic turns. Omit for unlimited (default) |
|
|
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 |
|
|
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 |
|
|
655
|
+
| `isolated` | boolean | no | No extension/MCP tools |
|
|
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` |
|
|
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` |
|
|
658
|
+
| `inherit_context` | boolean | no | Fork parent conversation into agent |
|
|
659
|
+
|
|
660
|
+
### `SubagentWorkflow`
|
|
661
|
+
|
|
662
|
+
Run a deterministic script that orchestrates many subagents. Returns a task id immediately; the run continues in the background and notifies on completion.
|
|
663
|
+
|
|
664
|
+
| Parameter | Type | Required | Description |
|
|
665
|
+
|-----------|------|----------|-------------|
|
|
666
|
+
| `script` | string | no | The workflow source. Must begin with `export const meta = { name, description }` |
|
|
667
|
+
| `scriptPath` | string | no | Path to a script file. Takes precedence over `script` and `name` |
|
|
668
|
+
| `name` | string | no | A saved workflow — `<name>.js` in `.pi/workflows/`, `.agents/workflows/` or `<agent dir>/workflows/`, carrying an `export const meta` declaration |
|
|
669
|
+
| `args` | any | no | Passed through to the script as the `args` global, verbatim |
|
|
670
|
+
| `resumeFromRunId` | string | no | Replay an earlier run in this session — its unchanged leading `agent()` calls return their recorded results instead of spawning |
|
|
671
|
+
| `title` / `description` | string | no | Accepted and ignored, as in Claude Code — a workflow is named by its `meta` block |
|
|
672
|
+
|
|
673
|
+
At least one of `script` / `scriptPath` / `name` is required; `scriptPath` wins over `script`, which wins over `name`. Each invocation's script is persisted to the session directory and its path returned, so iterating means editing that file and re-running rather than resending the source. A saved workflow reports its own file instead, so the same loop works on it — project `.pi/workflows/` shadows a same-named global one. Those directories are ordinary folders that may hold other scripts, so only files carrying the `export const meta = { name, description }` declaration are listed or resolved; naming anything else reports that it is not a workflow rather than running it. The check is a regex over the source — nothing in the file is executed to make it, and even a real parse evaluates only the `meta` object literal, in an empty `node:vm` context with a 100ms bound.
|
|
674
|
+
|
|
675
|
+
```js
|
|
676
|
+
export const meta = {
|
|
677
|
+
name: 'auth-audit',
|
|
678
|
+
description: 'Find routes missing auth checks, then verify each finding',
|
|
679
|
+
phases: [{ title: 'Scan' }, { title: 'Audit' }],
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
phase('Scan')
|
|
683
|
+
const listing = await agent('List every route file under src/routes/. One path per line.')
|
|
684
|
+
const files = listing.split('\n').map(s => s.trim()).filter(Boolean)
|
|
685
|
+
log('auditing ' + files.length + ' files')
|
|
686
|
+
|
|
687
|
+
phase('Audit')
|
|
688
|
+
return await pipeline(
|
|
689
|
+
files,
|
|
690
|
+
file => agent(`Audit ${file} for missing auth checks.`, { label: `audit:${file}` }),
|
|
691
|
+
(found, file) => agent(`Try to REFUTE this finding about ${file}: ${found}`, { label: `verify:${file}` }),
|
|
692
|
+
)
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Concurrency is capped at `max(1, min(16, cpus - 2))` — the run's own limit, independent of the session's `maxConcurrent` pool, which its agents do not enter. There are 1000 agents per run and 4096 items per `parallel`/`pipeline` call.
|
|
696
|
+
|
|
697
|
+
**Full guide:** [`docs/workflows.md`](https://github.com/tintinweb/pi-subagents/blob/master/docs/workflows.md) — how the model writes the script for you, how to edit and re-run it, how to save one as a reusable named workflow, plus the complete `agent()` option reference, recipes and troubleshooting.
|
|
698
|
+
|
|
699
|
+
### `get_subagent_result`
|
|
700
|
+
|
|
701
|
+
Check status and retrieve results from a background agent.
|
|
702
|
+
|
|
703
|
+
| Parameter | Type | Required | Description |
|
|
704
|
+
|-----------|------|----------|-------------|
|
|
705
|
+
| `agent_id` | string | yes | Agent ID to check |
|
|
706
|
+
| `wait` | boolean | no | Wait for completion |
|
|
707
|
+
| `verbose` | boolean | no | Include full conversation log |
|
|
708
|
+
|
|
709
|
+
Cancelling a `wait: true` call (for example, with `Esc`) stops only the wait. The background agent keeps running, and its completion notification still arrives normally.
|
|
710
|
+
|
|
711
|
+
### `steer_subagent`
|
|
712
|
+
|
|
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.
|
|
716
|
+
|
|
717
|
+
| Parameter | Type | Required | Description |
|
|
718
|
+
|-----------|------|----------|-------------|
|
|
719
|
+
| `agent_id` | string | yes | Agent ID to steer |
|
|
720
|
+
| `message` | string | yes | Message to inject into agent conversation |
|
|
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
|
+
|
|
732
|
+
## Commands
|
|
733
|
+
|
|
734
|
+
| Command | Description |
|
|
735
|
+
|---------|-------------|
|
|
736
|
+
| `/agents` | Interactive agent management menu — agent types, running agents, scheduled jobs, workflow runs, settings |
|
|
737
|
+
|
|
738
|
+
`/agents → Workflows` (shown only when [workflows](#persistent-settings) are on) opens a framed two-pane inspector over a run, with two levels of depth:
|
|
739
|
+
|
|
740
|
+
```
|
|
741
|
+
audit-src
|
|
742
|
+
Dynamically discover files under src/ and audit each … 1/3 agents · 32s
|
|
743
|
+
|
|
744
|
+
╭ Phases ──────────┬ Discover · 1 agent ──────────────────────────────────────────────╮
|
|
745
|
+
│ ❯ ✔ Discover 1/1 │ ❯ ✔ discover:src Opus 5 (1M context) · 26.4k tok 25s │
|
|
746
|
+
│ 2 Audit 0/2 │ │
|
|
747
|
+
│ 3 Verify │ │
|
|
748
|
+
│ 4 Synthesize │ │
|
|
749
|
+
╰──────────────────┴──────────────────────────────────────────────────────────────────╯
|
|
750
|
+
↑↓ select · ⏎ open · f filter · x stop · esc close · c convo
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
The overview puts the phases on the left (a phase shows its number until it finishes, then `✔`/`✘`) and the selected phase's agents on the right. `⏎` opens one: the agents move to the left pane and the right becomes that agent's **Prompt**, **Activity** and **Outcome**, with `⏎` now expanding the prompt and `esc` going back a level rather than closing. `↑↓` (or `j`/`k`) move and `f` cycles the state filter, naming it in the pane title. The dialog opens as a centered overlay, like the conversation viewer in regular mode; the frame sizes itself to what it holds, between six rows and twenty-two, so a three-agent run is not twenty rows of nothing and a two-hundred-agent one scrolls inside the pane. Long titles truncate with `…` rather than tearing it. With more than one workflow in the session it asks which, newest first.
|
|
754
|
+
|
|
755
|
+
The run itself takes five keys, and the footer offers each only while it can actually do something:
|
|
756
|
+
|
|
757
|
+
| Key | What it does |
|
|
758
|
+
|-----|--------------|
|
|
759
|
+
| `x` | Stop the run. Live runs only — a settled one has nothing left to stop |
|
|
760
|
+
| `p` | Pause / resume. Pausing stops *starting* agents; ones already running are left to finish, because killing model work mid-turn throws away everything it has spent. Held time is subtracted from the run's elapsed clock |
|
|
761
|
+
| `s` | Skip the selected agent: its `agent()` call returns `null`, exactly as a terminal failure does, and the row renders skipped. Offered while the agent is queued or running |
|
|
762
|
+
| `r` | Retry the selected agent: the child is stopped and the same call runs again, so the script's `agent()` promise is still the one waiting and gets the new answer. Running agents only — once a call has settled its value is already the script's, and a re-run would have nowhere to put one. The row then reads `attempt 2 · user retry` |
|
|
763
|
+
| `c` | Open the selected agent's **conversation** — the same live, scrolling viewer a fleet-list row opens, over the dialog, which hides itself underneath and comes back when you close it. The one key here that shows something rather than changing the run, so it works at both levels and on an agent that has already finished; reading what a child actually did is most of why anyone opens the inspector. Offered once the child has a record to open, which excludes a queued agent and one replayed from the resume journal. Records are swept ten minutes after they finish, and the key says so rather than opening an empty viewer |
|
|
764
|
+
|
|
765
|
+
Skipping is immediate for a running agent and for one held at a pause; an agent parked behind the concurrency limit takes its skip when it reaches the front of the queue.
|
|
766
|
+
|
|
767
|
+
### CLI flags
|
|
768
|
+
|
|
769
|
+
| Flag | Description |
|
|
770
|
+
|------|-------------|
|
|
771
|
+
| `--subagents-workflow-file=<path>` | Run a workflow script at session start |
|
|
772
|
+
|
|
773
|
+
Use the `=` form. The bare `--flag value` spelling consumes the next argument, so `pi --subagents-workflow-file review.js "do the thing"` would take the prompt as the flag's value. Composes with headless mode: `pi -p --subagents-workflow-file=review.js`. With no tool call to attach to, the run renders as a session entry and its result is handed to the model as context for its next turn.
|
|
774
|
+
|
|
775
|
+
The `/agents` command opens an interactive menu:
|
|
776
|
+
|
|
777
|
+
```
|
|
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
|
|
780
|
+
Agent types (6) ← unified list: defaults + custom
|
|
781
|
+
Create new agent ← manual wizard or AI-generated
|
|
782
|
+
Settings ← max concurrency (background + foreground), max turns, grace turns, join mode
|
|
783
|
+
```
|
|
784
|
+
|
|
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.
|
|
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:
|
|
788
|
+
- **Default agents** (no override): Eject (export as `.md`), Disable
|
|
789
|
+
- **Default agents** (ejected/overridden): Edit, Disable, Reset to default, Delete
|
|
790
|
+
- **Custom agents**: Edit, Disable, Delete
|
|
791
|
+
- **Disabled agents**: Enable, Edit, Delete
|
|
792
|
+
- **Eject** — writes the embedded default config as a `.md` file to project or personal location, so you can customize it
|
|
793
|
+
- **Disable/Enable** — toggle agent availability. Disabled agents stay visible in the list (marked `✕`) and can be re-enabled
|
|
794
|
+
- **Create new agent** — choose project/personal location, then manual wizard (step-by-step prompts for name, tools, model, thinking, system prompt) or AI-generated (describe what the agent should do and a sub-agent writes the `.md` file). Any name is allowed, including default agent names (overrides them)
|
|
795
|
+
- **Settings** — configure max concurrency (background and foreground), default max turns, grace turns, and join mode at runtime
|
|
796
|
+
|
|
797
|
+
## Record retention
|
|
798
|
+
|
|
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.
|
|
800
|
+
|
|
801
|
+
## Graceful Max Turns
|
|
802
|
+
|
|
803
|
+
Instead of hard-aborting at the turn limit, agents get a graceful shutdown:
|
|
804
|
+
|
|
805
|
+
1. At `max_turns` — steering message: *"Wrap up immediately — provide your final answer now."*
|
|
806
|
+
2. Up to 5 grace turns to finish cleanly
|
|
807
|
+
3. Hard abort only after the grace period
|
|
808
|
+
4. Whenever a limit fired and the agent did not end on a finished answer (text, no pending tool call), one more tool-less turn asks for the full final report. The steer is only a request: an agent deep in a tool loop ignores it, and without that turn the result was whatever it narrated before the abort ("Now let me check the tests..."). That turn is tool-free even when another extension re-activates its tools from `before_agent_start` (e.g. pi-deferred-context-engine): the loadout is pinned empty for the turn, any call that still gets through is refused, and the turn is stopped if it calls a tool or runs past one turn
|
|
809
|
+
|
|
810
|
+
A resumed agent gets a fresh budget: `max_turns` is counted from the resume prompt, resolved as for a spawn -- the agent file's `max_turns`, else the resume call's, else the limit the agent was spawned with, else the default. The same bounded, tool-free final report applies on resume. A caller stop, including one arriving while an aborted batch drains, never launches that report.
|
|
811
|
+
|
|
812
|
+
| Status | Meaning | Icon |
|
|
813
|
+
|--------|---------|------|
|
|
814
|
+
| `completed` | Finished naturally | `✓` green |
|
|
815
|
+
| `steered` | Hit limit, wrapped up (on its own, or via the forced final report) | `✓` yellow |
|
|
816
|
+
| `aborted` | Grace period exceeded and the final report turn produced nothing | `✗` red |
|
|
817
|
+
| `stopped` | User-initiated abort | `■` dim |
|
|
818
|
+
|
|
819
|
+
## Concurrency
|
|
820
|
+
|
|
821
|
+
There are two independent pools.
|
|
822
|
+
|
|
823
|
+
**Background** (`maxConcurrent`, default 10). Excess agents are automatically queued and start as running agents complete. The widget shows queued agents as a collapsed count. Since agents run in the background by default, nearly every spawn takes a slot; the limit was raised from 4 so that ordinary parallel fan-outs don't queue.
|
|
824
|
+
|
|
825
|
+
**Foreground** (`maxConcurrentForeground`, default `0` = unlimited). Off by default, so nothing changes unless you set it. pi dispatches a message's tool calls through `Promise.all`, so several `Agent` calls with `run_in_background: false` in one message have always started at once — this bounds that. Useful mainly with local models, where parallel agents thrash the prompt cache ([#253](https://github.com/tintinweb/pi-subagents/issues/253)). A queued foreground agent appears in `/agents → Running agents` as `queued` and can be stopped there; its `Agent` call says so while it waits and then returns its result normally.
|
|
826
|
+
|
|
827
|
+
The two are deliberately **not** one limit. A foreground agent blocks the parent anyway — the parent could have done that work itself without paying a slot — so charging it to the background pool would let a saturated pool starve the main session.
|
|
828
|
+
|
|
829
|
+
The foreground pool does not cover `resume`: a foreground resume reopens an existing session and never reaches the spawn path, so several blocking resumes in one message can still run at once (the process budget below does cover them). A *background* resume does take a background slot and queues behind them like any other background agent. A resume of an agent that is still `running` or `queued` is refused on either path, including two resumes of one agent in the same message.
|
|
830
|
+
|
|
831
|
+
Nested children and a [workflow](#subagentworkflow)'s agents are outside the pool entirely. A nested child would deadlock behind a parent waiting on it; a workflow already bounds its own fan-out at `max(1, min(16, cpus - 2))`, and counting its agents twice would let one run fill the session's pool and starve everything else.
|
|
832
|
+
|
|
833
|
+
### Process budget
|
|
834
|
+
|
|
835
|
+
**Process-wide** (`maxActiveSubagents`, default `0` = off; `nestedSubagentReserve`, default `2`). One ceiling on running subagents for the whole pi *process* — not the machine: a second pi process has its own. Unlike the two pools it counts every start, in every agent manager in the process: top-level, nested, workflow, scheduled, RPC and `@handle` spawns, foreground and background resumes, and agents a `/new`, `/resume`, `/fork` or `/reload` left running in the previous instance. It bounds running agents only; it does not look at CPU or memory and does not serialize commands.
|
|
836
|
+
|
|
837
|
+
A run holds its slot until it actually settles, worktree cleanup included, and a parent keeps its slot while it waits on a child: pi runs a turn's tool calls in parallel, so the parent's other tools may still be executing. Shutting a session down does not free its runs' slots either; each run frees its own when it returns. To keep held slots from deadlocking, `nestedSubagentReserve` slots are held back per nesting level, up to the *global* `maxSubagentDepth` `L`. With `limit(j) = maxActiveSubagents - nestedSubagentReserve × (L - j)`, a run at depth `d` (top-level, workflow and scheduled runs are depth 1) starts only while, for every level `j` from `d` to `L`, fewer than `limit(j)` runs at depth ≤ `j` hold slots. With `maxActiveSubagents: 12`, the default reserve and the default depth 2, at most 10 top-level runs hold slots and the last 2 only ever go to depth-2 children. With `maxSubagentDepth` 0 or 1 top-level runs get all 12.
|
|
838
|
+
|
|
839
|
+
The guarantee needs a coherent policy: with nesting on, a reserve of at least 1 and `maxActiveSubagents ≥ nestedSubagentReserve × (L - 1) + 1`. A budget that breaks that (`nestedSubagentReserve: 0`, or 12 and 2 at depth 8) is ignored with a warning and the previous policy stays in force. A project file can still deepen its own nesting, but the reserve covers only `L`: while the budget is on, a nested `Agent` call deeper than `L` fails at the call instead of waiting behind its own ancestors.
|
|
840
|
+
|
|
841
|
+
Waiting starts are served first come, first served among those that fit: a child may pass an earlier top-level start that has to wait for the reserve. A waiting `Agent` call or resume shows as `queued` and can be cancelled with Esc or `/agents` as usual. `bypassQueue` starts (scheduled jobs, the `/agents` agent generator) skip the per-session pools but not this budget, so a scheduled job can start late. At most 128 starts wait at once while the budget is on; the next spawn or resume fails at the call.
|
|
842
|
+
|
|
843
|
+
Both settings are read from global configuration only (`~/.pi/agent/subagents.json` or the enterprise manifest); a project file's value is ignored with a warning, since the budget spans every session in the process. The same goes for the depth it reserves for: the global `maxSubagentDepth`, not a project's. The last activation to apply its settings wins, and removing the key turns the budget off. Turning it off or raising a limit applies at once; turning it on, lowering a limit or changing the global depth waits until no subagent in the process holds a slot, since running parents were admitted under the old limits. A stderr warning names a deferred policy; the previous policy remains in force, and continuous work can delay the change indefinitely. Stop starting work and let the process become idle to apply it safely.
|
|
844
|
+
|
|
845
|
+
Managers loaded from a pi-subagents build without the budget are not counted; restart pi after upgrading for a complete count. A reload between builds with incompatible budget state versions refuses the entire new activation: no tools, commands or manager are registered, and `session_start` fails the bind with the host's `ExtensionStartupError` and an explicit restart diagnostic, even if the requested policy is off. On an older host without that fatal-bind API, the factory refuses immediately with the restart and host-upgrade diagnostic. It never continues unbudgeted or creates a parallel private count; existing runs in the old build can still settle under their original state.
|
|
846
|
+
|
|
847
|
+
### Sending to the background
|
|
848
|
+
|
|
849
|
+
Sending a foreground agent to the background moves its slot: it gives up the foreground slot (so a queued blocking `Agent` call can start) and takes a background one. If the background pool is already full it runs above capacity rather than being suspended, and queued background work waits until the count drops back under the limit.
|
|
850
|
+
|
|
851
|
+
## Join Strategies
|
|
852
|
+
|
|
853
|
+
When background agents complete, they notify the main agent. As in Claude Code, a notification reaches a busy parent after its current batch of tool calls, before its next model call — running tools are not interrupted — and everything that finished during that batch arrives as one message. An idle parent gets a turn about 200 ms after a completion, together with anything else that finished in that window. If you interrupt the parent (Esc), what was waiting is kept and delivered once the interrupted turn has stopped, starting a turn — as Claude Code does. The **join mode** decides whether a result waits for its siblings. It applies only to background agents.
|
|
854
|
+
|
|
855
|
+
| Mode | Behavior |
|
|
856
|
+
|------|----------|
|
|
857
|
+
| `smart` (default) | No waiting: each result is delivered at the parent's next boundary, as in Claude Code. `async` is accepted as an older name for it. |
|
|
858
|
+
| `group` | The background agents spawned in one parent turn report together, in one notification once all of them have finished. Fewer parent turns, at the cost of an early result waiting for the slowest sibling. |
|
|
859
|
+
|
|
860
|
+
**Timeout behavior (`group`):** a 30-second timeout starts after the first agent of a group completes. If not all agents finish in time, a partial notification is sent with completed results and remaining agents continue with a shorter 15-second re-batch window for stragglers.
|
|
861
|
+
|
|
862
|
+
**Configuration:**
|
|
863
|
+
- Configure join mode in `/agents` → Settings → Join mode
|
|
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
|
+
|
|
875
|
+
## Model tiers
|
|
876
|
+
|
|
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.
|
|
878
|
+
|
|
879
|
+
Bindings live ONLY in global `~/.pi/agent/subagents.json` or the `subagents` object of an owner-only `enterprise.local.json`. Project tier definitions and resolution policies are ignored with a warning. Keys match `^[a-z0-9][a-z0-9._-]*$` and are case sensitive. Each nonempty list contains either exactly one registered `router/<profile>`, or only exact physical `provider/id` members. No mixtures, nested tiers, fuzzy entries or implicit neighbouring tiers. Exact model IDs may contain revision markers such as `@20240620`; a bare `@other-tier` binding entry is still rejected. Invalid bindings are dropped with a warning; references to them are `unmapped`.
|
|
880
|
+
|
|
881
|
+
Routed personal setup (configure the router profiles separately):
|
|
882
|
+
|
|
883
|
+
```json
|
|
884
|
+
{
|
|
885
|
+
"modelTiers": { "cheap": ["router/cheap"], "default": ["router/default"], "deep": ["router/deep"], "frontier": ["router/frontier"], "review": ["router/review"] },
|
|
886
|
+
"modelResolution": { "onUnmapped": "error", "onUnavailable": "error" },
|
|
887
|
+
"agentModels": { "*": "@default", "Explore": "@cheap", "code-reviewer": "@review" }
|
|
888
|
+
}
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
A routed tier starts on its virtual model, not a physical chain member. The router owns headroom ranking, admission and mid-run failover. Exhaustion never tries `router/default` or another tier: it follows the existing quota parking/error rules. Missing router profiles are `unavailable`, not fuzzy guesses.
|
|
892
|
+
|
|
893
|
+
GPT-only setup, with no Anthropic dependency or router required:
|
|
894
|
+
|
|
895
|
+
```json
|
|
896
|
+
{ "modelTiers": { "cheap": ["openai/gpt-6-luna"] }, "agentModels": { "Explore": "@cheap" } }
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
Enterprise manifest (machine-local, permissions `0600`):
|
|
900
|
+
|
|
901
|
+
```json
|
|
902
|
+
{ "version": 1, "subagents": { "modelTiers": { "cheap": ["example-provider/example-wire-model"] }, "agentModels": { "Explore": "@cheap" } } }
|
|
903
|
+
```
|
|
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
|
+
|
|
913
|
+
Ordered static list:
|
|
914
|
+
|
|
915
|
+
```json
|
|
916
|
+
{ "modelTiers": { "cheap": ["provider-a/fast-model", "provider-b/fast-model"] } }
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
Static tiers choose the first credentialed, admitted member and retain skipped members with `no-credentials` or `quota` reasons. With a router installed, each member is admitted separately. Without one, admission fails open and disclosure explicitly says `static; no quota signal`.
|
|
920
|
+
|
|
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.
|
|
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
|
+
|
|
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.
|
|
926
|
+
|
|
927
|
+
## Machine-local model aliases
|
|
928
|
+
|
|
929
|
+
`~/.pi/agent/enterprise.local.json` is an optional, owner-only (`0600`) machine-local manifest for provider configuration that must never enter a repository. Its `subagents.modelAliases` section maps a logical name to ordered canonical `provider/modelId` candidates. The extension loads it only from the user agent directory, never from a project, and only accepts manifest version `1`. The same `modelAliases` key is also accepted in the global `~/.pi/agent/subagents.json` (when both define it, the manifest's whole set replaces the global one); a project `.pi/subagents.json` value is ignored with a warning, so a cloned repo cannot redirect which provider a subagent runs on.
|
|
930
|
+
|
|
931
|
+
```json
|
|
932
|
+
{
|
|
933
|
+
"version": 1,
|
|
934
|
+
"subagents": {
|
|
935
|
+
"modelAliases": {
|
|
936
|
+
"logical-coding-model": ["example-provider/example-wire-model"]
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
Alias candidates are resolved in order against authenticated models. Agent records retain the requested input and selection outcome alongside the selected model; schedules retain the explicit request and resolve it again at fire time. Missing, malformed, group-readable, or unsupported manifests are ignored with a safe path-and-reason warning. Aliases may not contain `/`; candidates must be canonical IDs, so aliases cannot recurse. Legacy aliases are exact static tiers with admission and skip disclosure, not fuzzy shortcuts. An unavailable alias follows `modelResolution.onUnavailable`, never exposing credential data.
|
|
943
|
+
|
|
944
|
+
## Per-agent models
|
|
945
|
+
|
|
946
|
+
`agentModels` in the global `~/.pi/agent/subagents.json` (or the `subagents` section of `enterprise.local.json`) maps an agent `type`, or `"*"` for every type, to an exact `provider/modelId`, `@tier`, or legacy alias. Agent types match case-insensitively; tier names do not.
|
|
947
|
+
|
|
948
|
+
```json
|
|
949
|
+
{ "agentModels": { "Plan": "@deep", "Explore": "@cheap", "*": "@default" } }
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
Precedence, highest first: the `Agent` tool's `model` parameter, `agentModels[type]`, `agentModels["*"]`, the agent definition's `model:`, the parent's model. The shared `selectModel` module applies it on every fresh spawn: top-level and nested `Agent`, workflow `agent()`, scheduled jobs, RPC, direct `@mention` and reopened evicted sessions. `/agents` labels use the same configured selection. An unresolved configured value falls directly to the parent, not a lower-priority configured default, with disclosure: the spawn path records `Note: configured model "<input>" did not resolve; running on parent model <provider>/<id>.` on the agent, and every surface reads it from there: the `Agent` result text and expanded row, `get_subagent_result`, the completion notice, nested `Agent` results, workflow rows and `agent()` results (text, error and a `modelNote` field), the widget, fleet list and conversation viewer, completion events, and agents started by scheduler fire, cross-extension RPC or `@mention`. A live-session resume retains its record's selection and note, ignoring new model inputs and config. An evicted conversation's `modelTier`, `modelFallback` and every model note describe the continuation's actual outcome; `previousModelTier` and `previousModelFallback` separately retain the prior run's outcomes, never as substitutes for a missing or changed binding. Explicit unresolved literal/fuzzy model names are hard errors on every fresh dispatch, including scheduled fires; tiers and legacy aliases follow `modelResolution`. Configured inputs and alias members are exact; only explicit non-tier tool inputs retain fuzzy ranking and provider retry. Tier failures obey `modelResolution`. A workflow `agent({ schema })` result stays pure JSON: the note is a sibling `_modelNote` on an object payload and a line in the run log. Like `modelAliases` it chooses providers, so a project `.pi/subagents.json` value is ignored with a warning.
|
|
953
|
+
|
|
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.
|
|
955
|
+
|
|
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`.
|
|
957
|
+
|
|
958
|
+
## Model Scope
|
|
959
|
+
|
|
960
|
+
**Opt-in:** off by default. Enable via `/agents → Settings → Scope models`.
|
|
961
|
+
|
|
962
|
+
When on, each subagent spawn's effective model is validated against pi's own `enabledModels` list (configured via pi's `/scoped-models` UI). pi-subagents reads that list; it doesn't manage it. Both of pi's settings files are honored: global `~/.pi/agent/settings.json` and project-local `<cwd>/.pi/settings.json`. **Project overrides global** — mirrors pi's `SettingsManager` deep-merge, so a tighter per-project scope (hand-edited into the project settings) is respected.
|
|
963
|
+
|
|
964
|
+
**Out-of-scope handling depends on source:**
|
|
965
|
+
|
|
966
|
+
| Model source | Out-of-scope behavior |
|
|
967
|
+
|---|---|
|
|
968
|
+
| Caller-supplied via `Agent({ model: "..." })` | Hard error returned to the orchestrator, listing allowed models |
|
|
969
|
+
| Caller-supplied via cross-extension RPC (`subagents:rpc:spawn`, e.g. pi-tasks `TaskExecute`) | Hard error returned to the calling extension, listing allowed models |
|
|
970
|
+
| Agent-frontmatter default (no caller model) | Warning toast + the configured default runs |
|
|
971
|
+
| Parent-inherited (neither set) | Warning toast + parent's model runs |
|
|
972
|
+
|
|
973
|
+
**Design:** `scopeModels` is a guardrail against the orchestrator picking unexpected models at runtime, not a hard policy against user-level config. An explicit caller model is an orchestrator-level choice and is therefore refused when out of scope; an agent's default or an inherited parent model warns and proceeds. This preserves portable agent definitions while letting an orchestrator deliberately select a target-local logical alias.
|
|
974
|
+
|
|
975
|
+
**Nested spawns** ([nested subagents](#nested-subagents)) apply the same table against the parent's config root. The hard-error case is identical; the warning cases proceed silently, since a subagent session has no UI to toast to.
|
|
976
|
+
|
|
977
|
+
**Pattern format:** only exact `provider/modelId` entries are honored (e.g. `anthropic/claude-haiku-4-5-20251001`). Glob patterns (`*sonnet*`), bare model IDs, and `:thinking` suffixes — which pi itself supports — are silently dropped here. pi's `/scoped-models` picker writes exact entries, so the limitation is invisible if you configure scope through the UI. Hand-edited globs produce an empty allowed set (scope check becomes a no-op).
|
|
978
|
+
|
|
979
|
+
**No-op safety:** if `enabledModels` is missing or empty in pi's settings, scope check skips entirely — no false positives, no spurious errors.
|
|
980
|
+
|
|
981
|
+
## Persistent Settings
|
|
982
|
+
|
|
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:
|
|
984
|
+
|
|
985
|
+
- **Global:** `~/.pi/agent/subagents.json` — your machine-wide defaults. Edit by hand; the `/agents` menu never writes here.
|
|
986
|
+
- **Project:** `<cwd>/.pi/subagents.json` — per-project overrides. Written by `/agents` → Settings.
|
|
987
|
+
|
|
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).
|
|
989
|
+
|
|
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.
|
|
991
|
+
|
|
992
|
+
**Nested depth** (`maxSubagentDepth`, default `2`): the hard ceiling on [nested delegation](#nested-subagents), counted from the main session (main = 0, its subagents = 1). `0` or `1` disables nesting project-wide regardless of any agent's `allowed_subagents`. Read when a subagent session is built, so a change applies to agents started after it.
|
|
993
|
+
|
|
994
|
+
**Fallback agent** (`fallbackSubagent`, default `general-purpose`): the agent used when a caller-supplied `subagent_type` doesn't resolve to exactly one enabled agent — unknown, disabled, or ambiguous because two agents differ only by case. Name any enabled agent to route those calls there instead, or set `none` for **strict**, fail-closed dispatch: the call is refused with an error listing the available types, and nothing spawns. Strict mode matters most for background and scheduled calls, which would otherwise start executing a substituted agent before the caller learns anything. Also settable from `/agents → Settings → Fallback agent`. The boolean `false` is accepted as a spelling of `none`, because it would otherwise be dropped as the wrong type and silently leave the permissive default in place. Every other value is read as an agent name, so a mistaken `off` fails loudly at dispatch rather than meaning one thing in the settings file and another in the resolver. A fallback agent that is itself unknown or disabled is a misconfiguration and is reported rather than quietly replaced. Note the default is unchanged and stays permissive by design: with `disableDefaultAgents` and no `general-purpose` of your own, an unresolvable type still resolves to a built-in config carrying *all* tools — set `none` (or name one of your own agents) to close that.
|
|
995
|
+
|
|
996
|
+
**Strict agent files** (`strictAgentFiles`, default `false`): when on, an unreadable or unparseable [agent file](#custom-agents) aborts extension load at startup and names the file, instead of being skipped with a warning — so a checked-in `.pi/agents/` can't silently fall through to a same-named agent from another location. Startup only: the mid-session reload that runs on each `Agent` call keeps warning either way, since a bad edit shouldn't kill a session on an unrelated spawn. Two phases: a malformed global file fails at activation as an ordinary factory error; a malformed package file only warns, in both phases; a malformed trusted-project file fails at the first `session_start`, once project trust is known, with the host's `ExtensionStartupError`. Strict mode needs a newer pi host that exports that class. On an older supported host, activation refuses before reading any agent file; upgrade pi or set `strictAgentFiles: false`. Also settable from `/agents → Settings → Strict agent files`.
|
|
997
|
+
|
|
998
|
+
**Package agents / package workflows** (`packageAgents` and `packageWorkflows`, both default `true`): whether agents and workflow scripts declared by installed pi packages are loaded, and from which packages. `true` admits every declaring package, `false` none, and a list admits only the packages it names — matched case-insensitively against the unscoped short name (`@acme/tools` → `tools`), the full package name, or the settings source string. The two are separate so package agents can be accepted while package workflow scripts are refused; an agent is markdown handed to a model, a workflow is `.js` this extension executes. Toggle both from `/agents → Settings`; an allowlist is hand-written in `subagents.json`, and toggling the row replaces it (the toast names what it replaced). See [Shipping agents in a pi package](#shipping-agents-in-a-pi-package).
|
|
999
|
+
|
|
1000
|
+
**Disable defaults** (`disableDefaultAgents`, default `false`): when on, the three built-in agents (general-purpose, Explore, Plan) are not registered — only your project/global custom agents are advertised and spawnable. User-defined agents are unaffected, including ones that override a default by name. The Agent tool's type list updates on the next pi session (the tool schema is registered at startup).
|
|
1001
|
+
|
|
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"`.
|
|
1003
|
+
|
|
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.
|
|
1005
|
+
|
|
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.
|
|
1007
|
+
|
|
1008
|
+
**Output transcript** (`outputTranscript`, default `true`): the project/global default for writing each subagent's `.output` transcript. Toggle via `/agents → Settings → Output transcript`, or set `false` in `subagents.json` to make transcripts opt-in project-wide — useful when run transcripts shouldn't sit on disk for backup or DLP tooling to pick up. A custom agent's `output_transcript` frontmatter overrides this per agent. Applied live at spawn time. Governs only the transcript, not `persist_session`, worktree commits, or memory files.
|
|
1009
|
+
|
|
1010
|
+
**Worktree isolation** (`worktreeIsolation`, default `true`): whether `isolation: "worktree"` may create a worktree at all. Toggle via `/agents → Settings → Worktree isolation`, or set `false` in `subagents.json` on a repo where a copy costs too much time or disk. Off, the `Agent` tool's `isolation` parameter is dropped from the schema entirely and the bullet describing it leaves the tool description with it — nothing to pass, and no context spent describing it — and worktrees are refused on every other path too (agent files, scheduled jobs, cross-extension RPC). The `/agents` agent-file generator stops offering the `isolation:` frontmatter field too, so a generated agent can't bake in a request that would be refused. A requested worktree is downgraded to a normal run rather than failing the call, since declining one is the point; there is deliberately no note on the result, which is exactly why the prose has to go when the parameter does. The refusal applies immediately; the parameter and its prose appear or disappear on the next pi session. See [Turning worktrees off](#turning-worktrees-off).
|
|
1011
|
+
|
|
1012
|
+
**Report usage to session** (`reportUsage`, default `false`): whether subagent spend is added to *this* session's own totals. Subagents run in their own pi sessions, so by default pi's footer, statusline and `/cost` count only what the main model spent — a session that delegated most of its work reads as nearly free. Turn it on and each `Agent` / `get_subagent_result` / `steer_subagent` result carries the spend accumulated since the last one, which pi folds into `getSessionStats()`; `/cost` attributes it to the **Tools/summaries** bucket. Toggle via `/agents → Settings → Report usage to session`; applied live.
|
|
1013
|
+
|
|
1014
|
+
Three things worth knowing about the numbers. Every token component is reported, `cacheRead` included — the cached prefix genuinely is re-read and re-billed on every call, and pi counts it the same way for the session's own messages, so withholding it would make a subagent's rows count differently from every other row in one total. (The extension's *own* token displays still leave it out, which is a different question: there it inflates a reading of how much work was done.) Cost is pi's own per-message figure, priced from the model's listed rates; a model pi has no rates for contributes zero rather than an estimate. And the context-window percentage is untouched: pi derives it from assistant messages alone, so a delegating session's context doesn't appear to fill up faster. Agents that finish in the background have no tool result of their own to ride on, so their spend is carried by the next one you make — the footer catches up on the following call, not the moment they finish.
|
|
1015
|
+
|
|
1016
|
+
**Show cost** (`showCost`, default `false`): whether the subagent surfaces print an estimated cost beside their token counts — the widget (running *and* finished lines), [FleetView](#fleetview), the conversation viewer, foreground results, `get_subagent_result`, and completion notifications:
|
|
1017
|
+
|
|
1018
|
+
```text
|
|
1019
|
+
├─ ⠹ Explore inspect code · ↻3 · 8.2k token · ~$0.0042 · 4.1s
|
|
1020
|
+
✓ Explore inspect code · ↻8 · 5 tool uses · ~$0.0181 · 12.3s
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
When several background agents finish together, their notification is topped with the batch total (`3 agents · 45.1k token · ~$0.042`) so the figures don't have to be added up by hand.
|
|
1024
|
+
|
|
1025
|
+
The `~` marks it as pi's estimate rather than a billed figure. **A cost is shown only when there is one to show:** a model pi has no pricing data for reports zero, and `$0.00` beside its tokens would say the run was measured and found free rather than never measured — so nothing is printed at all, on every surface. For the same reason a real cost too small to render reads `<$0.0001`. Figures keep cents at minimum and four decimals at most (`~$0.0042`, `~$0.05`, `~$1.24`) — rounding everything to cents would print the same number for runs that differed fourfold.
|
|
1026
|
+
|
|
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.
|
|
1028
|
+
|
|
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:
|
|
1030
|
+
|
|
1031
|
+
```text
|
|
1032
|
+
├─ ⠹ Explore inspect code · sonnet 4.6 · think:high · ↻3 · 8.2k token · 4.1s
|
|
1033
|
+
○ Explore inspect code ▶ running · sonnet 4.6 · think:high · thinking · 4s · 41s · ↓ 8.2k tokens
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
Off by default because the row already carries the description, turns, tool uses, tokens and elapsed time, and every character it gains is one the description loses on a narrow terminal. The other surfaces show the pair either way: the `Agent` tool result names the model beside its tags, and the conversation viewer's `↳` row spells out the canonical `provider/model-id`.
|
|
1037
|
+
|
|
1038
|
+
Both places report what the run *actually* used, read back from the child session once pi has resolved its defaults and clamped the level to what the model supports — not what the call asked for. Where Pi clamps the requested thinking level, the request is kept beside the effective value rather than dropped. An explicit `model` selection also overrides a frontmatter default; model resolution has no requested-versus-effective display:
|
|
1039
|
+
|
|
1040
|
+
```text
|
|
1041
|
+
↳ anthropic/claude-haiku-4-5 · thinking: low (asked max) · background
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
Toggle via `/agents → Settings → Show model`; applied live.
|
|
1045
|
+
|
|
1046
|
+
**Viewer markdown** (`viewerMarkdown`, default `"assistant"`): how much of the [conversation viewer](#ui)'s transcript is rendered as Markdown rather than shown verbatim.
|
|
1047
|
+
|
|
1048
|
+
```text
|
|
1049
|
+
off every line literal, as before this setting existed
|
|
1050
|
+
assistant assistant text rendered; tool results verbatim and dim (default)
|
|
1051
|
+
all tool results rendered too
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
Scoped rather than all-or-nothing because the two kinds of content have different contracts. Assistant text *is* Markdown — the model writes it that way, and the viewer was the only surface showing its source. A tool result is whatever bytes the tool produced, and a Markdown pass over one rewrites things that occur constantly in real output: `# section` in a shell script loses its `#`, a `---` line is swallowed as a setext heading, indented output is re-fenced, and `| a | b |` is redrawn as a box-drawing table. Each of those reads as the *tool* having misbehaved, which is why `all` is opt-in.
|
|
1055
|
+
|
|
1056
|
+
Two rewrites are suppressed outright rather than left to the mode, because they change *data* rather than layout: ordered-list markers keep their source numbering (`3) 7) 9)` stays, instead of being renumbered `3. 4. 5.`) and backslash escapes are not normalised.
|
|
1057
|
+
|
|
1058
|
+
Turn `all` on for tools that genuinely emit Markdown, and off again for a diff or a log. `m` in the viewer cycles the three and persists the choice, so the key and this setting are the same value — the footer shows which is in force as `m raw` / `m md` / `m md+`. Code fences are syntax-highlighted using pi's own Markdown theme — which is also why fenced code is the one part of a result *not* dimmed under `all`; result prose still is, so the transcript keeps its hierarchy. Applied live; also settable from `/agents → Settings → Viewer markdown`.
|
|
1059
|
+
|
|
1060
|
+
**Workflows** (`workflowsEnabled`, default `true`): the master switch for scripted workflows. Toggle it via `/agents → Settings → Workflows`, or set it in `subagents.json`. Off, the `SubagentWorkflow` tool is never registered — the model is not told the feature exists and cannot call it, so it costs no tool-spec context — the `/agents → Workflows` entry is hidden, and `--subagents-workflow-file` refuses with a pointer to the setting rather than doing nothing. Read at extension load, so it applies on the next pi session; runs already in flight are left alone.
|
|
1061
|
+
|
|
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.
|
|
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
|
+
|
|
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.
|
|
1067
|
+
|
|
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.
|
|
1069
|
+
|
|
1070
|
+
`"custom"` registers your own description from `<cwd>/.pi/agent-tool-description.md` (project) or `<agentDir>/agent-tool-description.md` (global; project wins). The file is read once at tool registration, so edits also apply on the next pi session. Dynamic parts stay live via placeholders — a static agent list would go stale the moment you add a custom agent:
|
|
1071
|
+
|
|
1072
|
+
```markdown
|
|
1073
|
+
Launch an autonomous agent. Available types:
|
|
1074
|
+
{{typeList}}
|
|
1075
|
+
|
|
1076
|
+
Custom agents live in .pi/agents/ or {{agentDir}}/agents/.
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
Placeholders: `{{typeList}}` (full per-agent descriptions), `{{compactTypeList}}` (first sentence each), `{{agentDir}}`, `{{isolationGuideline}}` and `{{scheduleGuideline}}` (each expands with its own leading newline + `- ` bullet when the matching feature is on — place them directly after your last rule line; empty when [worktree isolation](#turning-worktrees-off) / scheduling is off). Unknown placeholders are left verbatim with a stderr warning; a missing or empty file falls back to `"full"` with a warning. Note the usual trust umbrella: a project-level file shapes the orchestrator's prompt, same as project agents and extensions do.
|
|
1080
|
+
|
|
1081
|
+
**Starting point:** copy [`examples/agent-tool-description.md`](examples/agent-tool-description.md) — it reproduces the default full description exactly (a CI test keeps it in sync), so you can trim from a known-good baseline instead of writing from scratch.
|
|
1082
|
+
|
|
1083
|
+
**Example — global defaults for a beefy machine:**
|
|
1084
|
+
|
|
1085
|
+
```bash
|
|
1086
|
+
mkdir -p ~/.pi/agent
|
|
1087
|
+
cat > ~/.pi/agent/subagents.json <<'EOF'
|
|
1088
|
+
{
|
|
1089
|
+
"maxConcurrent": 16,
|
|
1090
|
+
"graceTurns": 10
|
|
1091
|
+
}
|
|
1092
|
+
EOF
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
Every project now starts with concurrency 16 and grace 10, without ever touching the menu. Individual projects can still override via `/agents` → Settings.
|
|
1096
|
+
|
|
1097
|
+
**Failure behavior:** missing file is silent; malformed JSON logs a `[pi-subagents] Ignoring malformed settings at …` warning to stderr; invalid/out-of-range field values are dropped per-field; write failures downgrade the `/agents` toast to a warning with `(session only; failed to persist)`.
|
|
1098
|
+
|
|
1099
|
+
## Events
|
|
1100
|
+
|
|
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.
|
|
1102
|
+
|
|
1103
|
+
| Event | When | Key fields |
|
|
1104
|
+
|-------|------|------------|
|
|
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 |
|
|
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` |
|
|
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 |
|
|
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 |
|
|
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 | `{}` |
|
|
1113
|
+
| `subagents:scheduled` | Schedule lifecycle change | `{ type: "added" \| "removed" \| "updated" \| "fired" \| "error", … }` (job/agentId/error fields per type) |
|
|
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) |
|
|
1118
|
+
| `subagents:ready` | RPC handlers registered and armed — fired on session start; not emitted in a session that excludes pi-subagents | `{}` (empty object) |
|
|
1119
|
+
| `subagents:settings_loaded` | Persisted settings applied at extension init | `settings` (merged global + project) |
|
|
1120
|
+
| `subagents:settings_changed` | `/agents` → Settings mutation was applied | `settings`, `persisted` (`boolean` — `false` on write failure) |
|
|
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
|
+
|
|
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.
|
|
1125
|
+
|
|
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.
|
|
1127
|
+
|
|
1128
|
+
The four agent-lifecycle events — `subagents:started`, `:completed`, `:failed`, `:compacted` — are emitted for **top-level agents only**; nested subagents and a workflow's children report through the parent or workflow that owns them. `subagents:usage` is the one event that is not scoped, on purpose: it is a ledger, not a lifecycle. It fires once per message for every agent, so summing `usage.cost.total` across it gives what this session's subagents spent, each message counted exactly once under the agent that spent it — unlike the ancestor records, which fold a nested child's spend into every parent. It fires regardless of `reportUsage`, which decides only whether the parent session's own totals include the spend.
|
|
1129
|
+
|
|
1130
|
+
`tokens.total` = `input + output + cacheWrite`. `cacheRead` is excluded — each turn's `cacheRead` is the cumulative cached prefix re-read on that one API call, so summing per-message would over-count it as a measure of work done. Use `contextUsage.percent` (surfaced as `(NN%)` in the widget) for current context size.
|
|
1131
|
+
|
|
1132
|
+
`usage` answers the other question — what was billed — and so does include `cacheRead`, because the prefix really is re-read and re-charged on every call. It is a pi `Usage`, the same shape pi puts on `ToolResultEvent` and `AssistantMessage`, so `usage.cost.total` is where a listener already expects the money and anything pi adds to `Usage` arrives without a change here. Neither field derives from the other; `tokens` is a view model, `usage` is the data.
|
|
1133
|
+
|
|
1134
|
+
## Cross-Extension RPC
|
|
1135
|
+
|
|
1136
|
+
Other pi extensions can spawn and stop subagents programmatically via the `pi.events` event bus, without importing this package directly.
|
|
1137
|
+
|
|
1138
|
+
All RPC replies use a standardized envelope: `{ success: true, data?: T }` on success, `{ success: false, error: string }` on failure.
|
|
1139
|
+
|
|
1140
|
+
**Full reference:** [`docs/rpc.md`](https://github.com/tintinweb/pi-subagents/blob/master/docs/rpc.md) — the complete spawn-option surface (including the fields that are silently stripped), every error string, the completion-notification race, the `Symbol.for("pi-subagents:manager")` registry, and what protocol version `2` does and does not promise. [`tintinweb/pi-tasks`](https://github.com/tintinweb/pi-tasks) is the reference implementation.
|
|
1141
|
+
|
|
1142
|
+
### Discovery
|
|
1143
|
+
|
|
1144
|
+
Listen for `subagents:ready` to know when RPC handlers are available:
|
|
1145
|
+
|
|
1146
|
+
```typescript
|
|
1147
|
+
pi.events.on("subagents:ready", () => {
|
|
1148
|
+
// RPC handlers are registered — safe to call ping/spawn/stop
|
|
1149
|
+
});
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
`subagents:ready` fires only when pi-subagents is actually loaded **and bound** in the current session. A session that excludes it (via an agent's `extensions:`) emits no `subagents:ready` and does not answer the RPC channels — exactly as if pi-subagents were not installed. Treat "no `subagents:ready`" as "not available here" and give discovery a timeout rather than waiting indefinitely.
|
|
1153
|
+
|
|
1154
|
+
### Ping
|
|
1155
|
+
|
|
1156
|
+
Check if the subagents extension is loaded and get the protocol version:
|
|
1157
|
+
|
|
1158
|
+
```typescript
|
|
1159
|
+
const requestId = crypto.randomUUID();
|
|
1160
|
+
const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, (reply) => {
|
|
1161
|
+
unsub();
|
|
1162
|
+
if (reply.success) console.log("Protocol version:", reply.data.version);
|
|
1163
|
+
});
|
|
1164
|
+
pi.events.emit("subagents:rpc:ping", { requestId });
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
### Spawn
|
|
1168
|
+
|
|
1169
|
+
Spawn a subagent and receive its ID:
|
|
1170
|
+
|
|
1171
|
+
```typescript
|
|
1172
|
+
const requestId = crypto.randomUUID();
|
|
1173
|
+
const unsub = pi.events.on(`subagents:rpc:spawn:reply:${requestId}`, (reply) => {
|
|
1174
|
+
unsub();
|
|
1175
|
+
if (!reply.success) {
|
|
1176
|
+
console.error("Spawn failed:", reply.error);
|
|
1177
|
+
} else {
|
|
1178
|
+
console.log("Agent ID:", reply.data.id);
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
pi.events.emit("subagents:rpc:spawn", {
|
|
1182
|
+
requestId,
|
|
1183
|
+
type: "general-purpose",
|
|
1184
|
+
prompt: "Do something useful",
|
|
1185
|
+
options: { description: "My task", isBackground: true },
|
|
1186
|
+
});
|
|
1187
|
+
```
|
|
1188
|
+
|
|
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.
|
|
1190
|
+
|
|
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.
|
|
1192
|
+
|
|
1193
|
+
`options.cwd` (absolute path to an existing directory — anything else returns an error envelope; `null` means unset) runs the agent in a different working directory than the parent session. Its tools operate there and the prompt's environment block describes it, but **`.pi` config still loads from the parent session's project** — the target directory's `.pi` extensions never execute, and its agents/skills/settings are not picked up. Combined with `isolation: "worktree"`, the worktree is created *from* the target directory's repo, the agent works at the equivalent subdirectory inside the copy (a monorepo-package cwd stays scoped to that package), and the resulting `pi-agent-*` branch lands in that repo — the completion message names it. On session end, worktree registrations are pruned in every repo that received one; only a hard crash can leave a stale entry (then: `git worktree prune` in the target repo). Agents with `memory:` keep reading/writing the parent project's memory.
|
|
1194
|
+
|
|
1195
|
+
### Stop
|
|
1196
|
+
|
|
1197
|
+
Stop a running agent by ID:
|
|
1198
|
+
|
|
1199
|
+
```typescript
|
|
1200
|
+
const requestId = crypto.randomUUID();
|
|
1201
|
+
const unsub = pi.events.on(`subagents:rpc:stop:reply:${requestId}`, (reply) => {
|
|
1202
|
+
unsub();
|
|
1203
|
+
if (!reply.success) console.error("Stop failed:", reply.error);
|
|
1204
|
+
});
|
|
1205
|
+
pi.events.emit("subagents:rpc:stop", { requestId, agentId: "agent-id-here" });
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
### Consume
|
|
1209
|
+
|
|
1210
|
+
Say that an agent's result has been shown to the model, so its completion notification is not delivered on top of it:
|
|
1211
|
+
|
|
1212
|
+
```typescript
|
|
1213
|
+
pi.events.emit("subagents:rpc:consume", { requestId: crypto.randomUUID(), agentId: "agent-id-here" });
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
This is the bus-side half of what `get_subagent_result` does when it returns a result. A caller that joins an agent on `subagents:completed` and reports the result itself should consume it — otherwise the notification still reaches a parent that has already seen the result, costing a model call to dismiss. Fire-and-forget is the intended use: the reply carries nothing to act on, and the channel is outside the `subagents:rpc:ping` version handshake, so a caller can send it unconditionally and an older extension that has no handler simply keeps notifying. Consuming a running or unknown agent is refused (`success: false`) and changes nothing — a running agent has no result to have been read, and its notification is still the caller's only signal that it finished.
|
|
1217
|
+
|
|
1218
|
+
### Steer
|
|
1219
|
+
|
|
1220
|
+
Deliver a message to a running agent you spawned, when something it needs arrives mid-run (the bus-side half of `steer_subagent`):
|
|
1221
|
+
|
|
1222
|
+
```typescript
|
|
1223
|
+
pi.events.emit("subagents:rpc:steer", { requestId: crypto.randomUUID(), agentId: "agent-id-here", message: "A second check failed on the same head; include it before you push." });
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
Same ownership guard as stop; `Agent is not running` once the agent has settled. A successful steer emits `subagents:steered`.
|
|
1227
|
+
|
|
1228
|
+
Reply channels are scoped per `requestId`, so concurrent requests don't interfere.
|
|
1229
|
+
|
|
1230
|
+
## Persistent Agent Memory
|
|
1231
|
+
|
|
1232
|
+
Agents can have persistent memory across sessions. Set `memory` in frontmatter to enable:
|
|
1233
|
+
|
|
1234
|
+
```yaml
|
|
1235
|
+
---
|
|
1236
|
+
memory: project # project | local | user
|
|
1237
|
+
---
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
| Scope | Location | Use case |
|
|
1241
|
+
|-------|----------|----------|
|
|
1242
|
+
| `project` | `.pi/agent-memory/<name>/` | Shared across the team (committed) |
|
|
1243
|
+
| `local` | `.pi/agent-memory-local/<name>/` | Machine-specific (gitignored) |
|
|
1244
|
+
| `user` | `<agentDir>/agent-memory/<name>/` (default `~/.pi/agent/agent-memory/`, honors `PI_CODING_AGENT_DIR`) | Global personal memory |
|
|
1245
|
+
|
|
1246
|
+
The `user` scope previously hardcoded `~/.pi/agent-memory/`. If that legacy directory exists for an agent and the new location doesn't, it keeps being used — existing memories aren't orphaned.
|
|
1247
|
+
|
|
1248
|
+
Memory uses a `MEMORY.md` index file and individual memory files with frontmatter. Agents with write tools get full read-write access. **Read-only agents** (no `write`/`edit` tools) automatically get read-only memory — they can consume memories written by other agents but cannot modify them. This prevents unintended tool escalation.
|
|
1249
|
+
|
|
1250
|
+
The `disallowed_tools` field is respected when determining write capability — an agent with `tools: write` + `disallowed_tools: write` correctly gets read-only memory.
|
|
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
|
+
|
|
1277
|
+
## Worktree Isolation
|
|
1278
|
+
|
|
1279
|
+
Set `isolation: worktree` to run an agent in a temporary git worktree:
|
|
1280
|
+
|
|
1281
|
+
```
|
|
1282
|
+
Agent({ subagent_type: "refactor", prompt: "...", isolation: "worktree" })
|
|
1283
|
+
```
|
|
1284
|
+
|
|
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:
|
|
1286
|
+
- **No changes:** worktree is cleaned up automatically, no branch
|
|
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
|
|
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)
|
|
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
|
+
|
|
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.
|
|
1293
|
+
|
|
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.
|
|
1295
|
+
|
|
1296
|
+
The worktree is made from the repo containing the agent's working directory, which is the session cwd unless the call passes `cwd`. To isolate a repo the session wasn't started in, name it:
|
|
1297
|
+
|
|
1298
|
+
```
|
|
1299
|
+
Agent({ subagent_type: "general-purpose", prompt: "...", isolation: "worktree", cwd: "~/code/other-repo/pkg" })
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
The agent works at the same subdirectory inside the copy (`pkg/` here), and the result names the branch and that repo's toplevel to merge in. `~/` expansion honors `HOME` (`USERPROFILE` on Windows), including on Bun; it falls back to the OS home when unset. The `cwd` gets the same treatment as the [RPC `cwd` option](#cross-extension-rpc): `.pi` config still loads from the session's project.
|
|
1303
|
+
|
|
1304
|
+
If the worktree cannot be created, the `Agent` call fails with an error naming the directory it tried and the cause (not a git repo, no commits yet, a git timeout, or `git worktree add`'s stderr) instead of running unisolated — `isolation: "worktree"` is a strict guarantee, not a hint. The call is reported as a failed tool call, not as a subagent that ran and returned that message, so the model doesn't retry it as if the agent had merely reported a problem. Initialize git and commit at least once, or omit `isolation`.
|
|
1305
|
+
|
|
1306
|
+
A worktree is a *copy*, so the agent cannot see uncommitted or staged changes in the main checkout. Never use it to review a working-tree or staged diff: the agent finds an empty `git diff` and reports nothing wrong.
|
|
1307
|
+
|
|
1308
|
+
### Turning worktrees off
|
|
1309
|
+
|
|
1310
|
+
Three levers, from narrowest to broadest:
|
|
1311
|
+
|
|
1312
|
+
- **Per call** — omit `isolation`, or pass `isolation: "off"`. The explicit value exists because some models fill every optional parameter they are offered; with `worktree` as the only legal value they had no way to decline one (#231, #184).
|
|
1313
|
+
- **Per agent** — `isolation: off` in an agent file. Frontmatter is authoritative, so this refuses a worktree even when the caller passes `isolation: "worktree"` — the only way to override a caller. Workflow `agent()` calls are the exception: as in Claude Code, only the script's option decides there.
|
|
1314
|
+
- **Per project** — `"worktreeIsolation": false` in `subagents.json`. The `Agent` tool's `isolation` parameter disappears from the schema entirely, along with the usage-note bullet that describes it (so it costs the model no context and cannot be passed), and worktree creation is refused on every other path too: agent files, scheduled jobs, and cross-extension RPC. The `/agents` generator also stops offering `isolation:` when writing a new agent file. Use it on a repo large enough that a copy costs real time and disk. The schema and the description are both built at tool registration, so they appear or disappear in the next pi session; the refusal itself takes effect immediately.
|
|
1315
|
+
|
|
1316
|
+
Schema and prose are gated together on purpose. Leaving the bullet in would teach the model to pass a field that is no longer declared — accepted silently, then dropped — and since a refused worktree carries no note on the result, the model would have every reason to go on reporting a `pi-agent-*` branch that was never created. A custom tool description should use the `{{isolationGuideline}}` placeholder rather than hardcoding the bullet, for the same reason.
|
|
1317
|
+
|
|
1318
|
+
## Skill Preloading
|
|
1319
|
+
|
|
1320
|
+
Skills can be preloaded by name and injected into the agent's system prompt:
|
|
1321
|
+
|
|
1322
|
+
```yaml
|
|
1323
|
+
---
|
|
1324
|
+
skills: api-conventions, error-handling
|
|
1325
|
+
---
|
|
1326
|
+
```
|
|
1327
|
+
|
|
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.
|
|
1329
|
+
|
|
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.
|
|
1331
|
+
|
|
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.
|
|
1333
|
+
|
|
1334
|
+
## Tool Denylist
|
|
1335
|
+
|
|
1336
|
+
Block specific tools from an agent even if extensions provide them:
|
|
1337
|
+
|
|
1338
|
+
```yaml
|
|
1339
|
+
---
|
|
1340
|
+
tools: read, bash, grep, write
|
|
1341
|
+
disallowed_tools: write, edit
|
|
1342
|
+
---
|
|
1343
|
+
```
|
|
1344
|
+
|
|
1345
|
+
This is useful for creating agents that inherit extension tools but should not have write access.
|
|
1346
|
+
|
|
1347
|
+
## Architecture
|
|
1348
|
+
|
|
1349
|
+
`pi.extensions` loads `dist/index.ts`, a generated esbuild bundle of `src/` (`npm run build`; `npm run check:runtime` fails when it is stale). Edit `src/`, never `dist/`.
|
|
1350
|
+
|
|
1351
|
+
```
|
|
1352
|
+
docs/ # Long-form guides (shipped to npm; README links out to them)
|
|
1353
|
+
workflows.md # SubagentWorkflow: writing, editing, saving and re-running scripts
|
|
1354
|
+
rpc.md # Cross-extension integration: pi.events, subagents:rpc:*, manager registry
|
|
1355
|
+
home-injection.md # Injected home boundaries, call-time defaults and filesystem canaries
|
|
1356
|
+
examples/
|
|
1357
|
+
workflows/ # Runnable examples, executed by test/workflow-examples.test.ts
|
|
1358
|
+
agent-tool-description.md
|
|
1359
|
+
test/ # Full Vitest suite; four session scenario files also run under Bun
|
|
1360
|
+
helpers/scenario-session.ts # Exported core host, child/provider barriers and isolated homes
|
|
1361
|
+
src/
|
|
1362
|
+
index.ts # Extension entry: tool/command registration, /agents menu, rendering
|
|
1363
|
+
types.ts # Type definitions (AgentConfig, AgentRecord, etc.)
|
|
1364
|
+
|
|
1365
|
+
# Agent registry
|
|
1366
|
+
default-agents.ts # Embedded default agent configs (general-purpose, Explore, Plan)
|
|
1367
|
+
custom-agents.ts # Load user-defined agents from .pi/agents/, .agents/agents/, global agents, and pi packages
|
|
1368
|
+
startup-error.ts # Optional bridge to the host's ExtensionStartupError (strictAgentFiles)
|
|
1369
|
+
package-resources.ts # Agents/workflows declared by installed pi packages (pi.subagents manifest)
|
|
1370
|
+
agent-types.ts # Unified agent registry (defaults + user), tool name resolution
|
|
1371
|
+
agent-file-toggle.ts # Locate/edit an agent's .md: enabled: toggle, eject to frontmatter
|
|
1372
|
+
agent-color.ts # Claude Code/Agency Agents name color parsing and badge rendering
|
|
1373
|
+
|
|
1374
|
+
# Execution
|
|
1375
|
+
agent-runner.ts # Session creation, execution, graceful max_turns, steer/resume
|
|
1376
|
+
agent-manager.ts # Agent lifecycle, concurrency queue, completion notifications
|
|
1377
|
+
nested-tools.ts # Delegation tools handed to subagents (nested spawn/collect/steer)
|
|
1378
|
+
process-budget.ts # Process-wide running-agent budget shared by every manager (maxActiveSubagents)
|
|
1379
|
+
child-context.ts # AsyncLocalStorage flag marking work done for a child session
|
|
1380
|
+
plan-mode-children.ts # Plan mode for children spawned while the parent plans (#437)
|
|
1381
|
+
abortable.ts # Race a wait against Esc without cancelling the background child
|
|
1382
|
+
model-call.ts # In-flight model call tracking: thinking / writing / stalled?
|
|
1383
|
+
completion-outbox.ts # Deliver completion notifications at the parent's next boundary, coalesced
|
|
1384
|
+
notification-result.ts # How much of a result a notice carries: per-agent cap, batch budget, head+tail clip
|
|
1385
|
+
clock.ts # Shared scheduler/outbox/admission/retention/UI clock and native timers
|
|
1386
|
+
detached-results.ts # Session-scoped spool and registry for background agents surviving reload/switch
|
|
1387
|
+
group-join.ts # `group` join mode: one notification per turn's batch, with timeout
|
|
1388
|
+
status-note.ts # Honest status note + salvaged partial output for non-normal outcomes
|
|
1389
|
+
error-message.ts # The text a caught value reports: an Error's message, else String(value)
|
|
1390
|
+
usage.ts # Token usage shapes, accumulators, session-stats readers
|
|
1391
|
+
|
|
1392
|
+
# Invocation surface
|
|
1393
|
+
invocation-config.ts # Shared tool-parameter schemas (isolation, cwd, thinking, ...)
|
|
1394
|
+
model-selection.ts # Shared precedence, scope and resolution outcome; stored on the agent record
|
|
1395
|
+
model-tiers.ts # Exact static/routed tiers, admission, policy and durable disclosure
|
|
1396
|
+
model-resolver.ts # Model matching: exact provider/modelId, aliases, fuzzy fallback ranked by version
|
|
1397
|
+
enabled-models.ts # Read pi's enabledModels settings (project over global)
|
|
1398
|
+
model-scope.ts # scopeModels allowlist policy, shared by top-level and nested tools
|
|
1399
|
+
mention.ts # `@handle message` grammar: suggestion triggers and send parsing
|
|
1400
|
+
mention-clone.ts # Run a mention's turn in a cloned conversation, off the main chat
|
|
1401
|
+
cross-extension-rpc.ts # ping/spawn/stop/consume/steer RPC handlers over pi.events
|
|
1402
|
+
|
|
1403
|
+
# Scheduling
|
|
1404
|
+
schedule.ts # SubagentScheduler: cron / +10m / interval / ISO dispatch
|
|
1405
|
+
schedule-store.ts # PID-locked, session-scoped, atomic schedule persistence
|
|
1406
|
+
|
|
1407
|
+
# Context & environment
|
|
1408
|
+
home-paths.ts # Injected home normalization and Pi agent-directory defaults
|
|
1409
|
+
resource-loader.ts # Fresh scoped settings paths for Pi's real resource/package loader
|
|
1410
|
+
memory.ts # Persistent agent memory (resolve, read, build prompt blocks)
|
|
1411
|
+
skill-loader.ts # Preload named skills through Pi's configured resource discovery (trust-gated, inspect-only)
|
|
1412
|
+
output-file.ts # Streaming output file transcripts for agent sessions
|
|
1413
|
+
worktree.ts # Git worktree isolation (create, cleanup, prune)
|
|
1414
|
+
git-path.ts # Decode git's C-quoted paths (`unquoteGitPath`) for worktree cleanup
|
|
1415
|
+
prompts.ts # Config-driven system prompt builder
|
|
1416
|
+
context.ts # Parent conversation context for inherit_context
|
|
1417
|
+
settings.ts # Persistent settings (~/.pi/agent/subagents.json + .pi/subagents.json)
|
|
1418
|
+
env.ts # Environment detection (git, platform)
|
|
1419
|
+
|
|
1420
|
+
workflow/
|
|
1421
|
+
meta.ts # Extract and validate a script's pure-literal `meta` block
|
|
1422
|
+
worker-source.ts # The sandbox: vm context, determinism prelude, script globals
|
|
1423
|
+
runtime.ts # Worker lifecycle, RPC bridge, semaphore, caps, gate/resume
|
|
1424
|
+
progress.ts # Progress event log and every derived view of it (pure)
|
|
1425
|
+
host.ts # WorkflowHost adapter over AgentManager
|
|
1426
|
+
task.ts # local_workflow task record and batched progress updates
|
|
1427
|
+
tool-description.ts # Model-facing description carrying the orchestration patterns
|
|
1428
|
+
ui/
|
|
1429
|
+
agent-widget.ts # Persistent widget: spinners, activity, status icons, theming
|
|
1430
|
+
agent-row.ts # The Agent tool row: live one-line summary, expanded prompt/tags/transcript
|
|
1431
|
+
notification-view.ts # Completion notice renderer: one-line collapsed, stats/preview/link expanded
|
|
1432
|
+
fleet-list.ts # FleetView: one-line summary/disclosure and navigable list below the editor
|
|
1433
|
+
fleet-state.ts # Versioned journal checkpoints, bounded failure details and exact overflow counters
|
|
1434
|
+
conversation-viewer.ts # Live conversation overlay for viewing agent sessions
|
|
1435
|
+
viewer-keys.ts # Viewer scroll keys resolved through user keybindings
|
|
1436
|
+
agent-mention.ts # `@` roster (running, resumable, and startable agents) + popup rows
|
|
1437
|
+
schedule-menu.ts # /agents → Scheduled jobs submenu
|
|
1438
|
+
select-item.ts # Collision-safe ctx.ui.select wrapper (numbered rows)
|
|
1439
|
+
workflow-card.ts # Inline workflow card (tool result and session entry)
|
|
1440
|
+
workflow-dialog.ts # /agents → Workflows two-pane inspector
|
|
1441
|
+
```
|
|
1442
|
+
|
|
1443
|
+
## License
|
|
1444
|
+
|
|
1445
|
+
MIT — [tintinweb](https://github.com/tintinweb)
|