@nredd/pi-subagents 0.22.0-nredd.7 → 0.22.0-nredd.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -37
- package/package.json +8 -8
- package/src/agent-file-toggle.ts +6 -4
- package/src/agent-manager.ts +131 -28
- package/src/agent-runner.ts +155 -62
- package/src/cross-extension-rpc.ts +11 -36
- package/src/custom-agents.ts +9 -5
- package/src/default-agents.ts +2 -4
- package/src/enabled-models.ts +8 -7
- package/src/enterprise-local.ts +5 -5
- package/src/home-paths.ts +27 -0
- package/src/index.ts +191 -90
- package/src/invocation-config.ts +3 -16
- package/src/memory.ts +9 -9
- package/src/mention-clone.ts +9 -5
- package/src/model-resolver.ts +41 -11
- package/src/model-scope.ts +2 -1
- package/src/model-selection.ts +159 -0
- package/src/model-tiers.ts +171 -0
- package/src/nested-tools.ts +22 -26
- package/src/output-file.ts +23 -1
- package/src/package-resources.ts +25 -10
- package/src/process-budget.ts +6 -3
- package/src/quota-admission.ts +21 -9
- package/src/resource-loader.ts +51 -0
- package/src/schedule.ts +36 -18
- package/src/settings.ts +42 -12
- package/src/skill-loader.ts +8 -7
- package/src/stale-ctx.ts +16 -0
- package/src/types.ts +27 -0
- package/src/ui/agent-row.ts +13 -3
- package/src/ui/agent-widget.ts +15 -4
- package/src/ui/conversation-viewer.ts +6 -3
- package/src/ui/fallback-flag.ts +90 -0
- package/src/ui/fleet-list.ts +363 -53
- package/src/ui/fleet-state.ts +154 -0
- package/src/ui/notification-view.ts +18 -2
- package/src/ui/workflow-card.ts +5 -1
- package/src/ui/workflow-dialog.ts +10 -0
- package/src/workflow/host.ts +43 -45
- package/src/workflow/journal.ts +13 -1
- package/src/workflow/progress.ts +4 -0
- package/src/workflow/runtime.ts +25 -8
- package/src/workflow/saved.ts +19 -17
- package/src/workflow/task.ts +10 -0
- package/src/workflow/worker-source.ts +156 -38
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
|
|
|
17
17
|
- **Claude Code look & feel** — same tool names, calling conventions, and UI patterns (`Agent`, `get_subagent_result`, `steer_subagent`) — feels native
|
|
18
18
|
- **Parallel background agents** — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 10) and completion notifications that reach the parent at its next boundary, several at once when they finish together
|
|
19
19
|
- **Live widget UI** — persistent above-editor widget with animated spinners, live tool activity, token counts, and colored status icons. Configurable via `/agents → Settings → Widget`: `all` (every agent), `background` (default — hides foreground runs, which already render inline as the `Agent` tool result), or `off`
|
|
20
|
-
- **FleetView** — Claude Code-style navigable list of `main` + every running subagent
|
|
20
|
+
- **FleetView** — one-line summary below the editor by default; `ctrl+shift+a` expands the Claude Code-style navigable list of `main` + every running subagent (earliest-launched first). Press `↓` (or `←`) at an empty prompt to jump in, `↑`/`↓` to move the selection, `Enter` to open the selected agent's live, auto-updating conversation, `Esc` to return. Finished agents linger briefly before dropping out, and a viewer stays open through completion so you can read the final output. Toggle via `/agents → Settings → Fleet view`
|
|
21
21
|
- **Conversation viewer** — select any agent in `/agents` to open a live-scrolling overlay of its full conversation (auto-follows new content, scroll up to pause). In pi's fullscreen mode it is a borderless, full-terminal observer with mouse support. Steer a running agent inline by pressing `Enter` to open a composer, typing, then `Enter` to send (`Esc` or an empty submit returns) — the message appears as a user message and redirects the agent after its current tool. Press `b` on a running foreground `Agent` call to hand that run to the background (see [Sending a foreground agent to the background](#sending-a-foreground-agent-to-the-background)). Stop a still-running agent by pressing `x` (then `x` again to confirm) — steer and stop both work for background agents too. Assistant text renders as Markdown; `m` cycles that between off, assistant-only and everything (see [Viewer markdown](#persistent-settings))
|
|
22
22
|
- **Custom agent types** — define agents in `.pi/agents/<name>.md` or `.agents/agents/<name>.md` (project) or globally, with YAML frontmatter: custom system prompts, model selection, thinking levels, tool restrictions, and Claude Code-compatible colored name badges
|
|
23
23
|
- **Package-shipped agents** — a pi package can declare its own subagents (and workflow scripts) with `"pi": { "subagents": { "agents": ["./agents"] } }` in its `package.json`, so `pi install` is the whole setup: no copying files into `.pi/agents/` on every machine or container start. Only packages pi has actually installed are read (never `node_modules`), package definitions rank below every agent you wrote, and `/agents` shows them read-only with Eject and Disable. Narrow or switch it off with `packageAgents` / `packageWorkflows`. **[Full guide](https://github.com/tintinweb/pi-subagents/blob/master/docs/packages.md)**
|
|
@@ -54,7 +54,7 @@ pi -e ./src/index.ts
|
|
|
54
54
|
|
|
55
55
|
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.
|
|
56
56
|
|
|
57
|
-
### In-process factory and
|
|
57
|
+
### In-process factory, clock and home
|
|
58
58
|
|
|
59
59
|
`src/index.ts` exports `createSubagentsExtension(options: SubagentsExtensionOptions = {}): ExtensionFactory`.
|
|
60
60
|
The default export is `createSubagentsExtension()` and keeps native time and timer reference behavior.
|
|
@@ -70,8 +70,12 @@ declare const clock: Clock;
|
|
|
70
70
|
const extension = createSubagentsExtension({ clock });
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
`SubagentsExtensionOptions`
|
|
74
|
-
|
|
73
|
+
`SubagentsExtensionOptions` accepts `clock?: Clock` and `homeDir?: string`. An explicit home binds
|
|
74
|
+
user-scoped discovery, settings and child-session paths without mutating `HOME`; omitted homes keep
|
|
75
|
+
call-time host defaults. Explicit agent/session directory overrides still win. The boundary audit and
|
|
76
|
+
test seams are documented [here](docs/home-injection.md).
|
|
77
|
+
|
|
78
|
+
The exported clock contract matches session-kit's scheduling boundary:
|
|
75
79
|
|
|
76
80
|
```ts
|
|
77
81
|
interface TimerHandle { cancel(): void; unref?(): void }
|
|
@@ -101,12 +105,13 @@ start gate when present; legacy queued resumes retain the clock-driven fallback.
|
|
|
101
105
|
|
|
102
106
|
`npm run check` runs Biome, TypeScript, the full Vitest suite and `test:bun`. The four session scenario
|
|
103
107
|
files (`test/{session-scenarios,fleet-session,viewer-session,worktree-isolation-e2e}.test.ts`) run under
|
|
104
|
-
both Vitest and Bun 1.
|
|
108
|
+
both Vitest and Bun 1.4.4 on core `v1.3.0-nredd.8` release tarballs. They use exported `createHarness`
|
|
105
109
|
and `createInteractiveHarness`/`VirtualTerminal`, real child sessions and git, one injected extension
|
|
106
110
|
clock, promise-held tools/streams and raw SGR mouse input. Homes, settings and provider credentials
|
|
107
111
|
are fixtures; no installed pi, tmux or real-clock sleeps are needed. Existing old-host strict-mode
|
|
108
112
|
coverage explicitly removes the optional SDK export; the shared faux runtime uses the host's real
|
|
109
|
-
`withBeforeProviderRequest` facade. The full older suite remains Vitest-only.
|
|
113
|
+
`withBeforeProviderRequest` facade. The full older suite remains Vitest-only. Maintainer guidance on public index callback
|
|
114
|
+
coverage and retained safety guards is [here](../../docs/subagents-index-continuation.md).
|
|
110
115
|
|
|
111
116
|
### Other hosts
|
|
112
117
|
|
|
@@ -163,9 +168,9 @@ Schedule formats:
|
|
|
163
168
|
- **One-shot relative** — `"+10m"`, `"+2h"`, `"+1d"`. Fires once at that future time.
|
|
164
169
|
- **One-shot absolute** — full ISO timestamp, e.g. `"2026-12-25T09:00:00.000Z"`.
|
|
165
170
|
|
|
166
|
-
When a schedule fires, the spawn runs in background and its completion notification arrives the same way as a manually-spawned background agent's — your parent agent reasons about the result the same way. The job stores the resolved thinking level when you create it: an explicit `Agent({ thinking })` wins over the agent definition then, while an omitted value keeps the definition's default. Editing the agent file later does not replace that stored level.
|
|
171
|
+
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.
|
|
167
172
|
|
|
168
|
-
**Session switches.** `/new`, `/resume`, `/fork` and `/reload` do not stop running background agents. They keep showing in the fleet list of the reloaded session while they run (open, stop and steer work from there), finish on their own, and the session that spawned them gets the completion notification when you return to it (immediately if it is already open). Held notices survive the switch too: unread results are transferred to the session spool and re-enter the same boundary outbox on return, without repeating their history entries. The preview is in the notification; the full output is in the transcript file it names, since `get_subagent_result` cannot reach an agent from a previous session. Quitting pi still aborts everything.
|
|
173
|
+
**Session switches.** `/new`, `/resume`, `/fork` and `/reload` do not stop running background agents. They keep showing in the fleet list of the reloaded session while they run (open, stop and steer work from there), finish on their own, and the session that spawned them gets the completion notification when you return to it (immediately if it is already open). Held notices survive the switch too: unread results are transferred to the session spool and re-enter the same boundary outbox on return, without repeating their history entries. The preview is in the notification; the full output is in the transcript file it names, since `get_subagent_result` cannot reach an agent from a previous session. Quitting pi still aborts everything. A step that routes through the old session's virtual model (`router/*`) while the switch is in flight is re-prompted up to three times with backoff; if it never recovers the agent ends `error` with `interrupted by a session reload` and its last tool calls and output as the result, not `No output`. 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.
|
|
169
174
|
|
|
170
175
|
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.
|
|
171
176
|
|
|
@@ -179,13 +184,13 @@ Restrictions:
|
|
|
179
184
|
|
|
180
185
|
### Quota admission
|
|
181
186
|
|
|
182
|
-
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`
|
|
187
|
+
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).
|
|
183
188
|
|
|
184
189
|
- Admitted, or no router answers within 100ms: the agent starts as usual
|
|
185
190
|
- 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
|
|
186
191
|
- Refused otherwise (foreground, `inherit_context`, scheduling off, unknown reset, or `wait: false`): the call fails with the reset time and no agent starts
|
|
187
192
|
|
|
188
|
-
|
|
193
|
+
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.
|
|
189
194
|
|
|
190
195
|
## UI
|
|
191
196
|
|
|
@@ -219,20 +224,41 @@ The status, elapsed time and tokens are read from the agent record on every rend
|
|
|
219
224
|
|
|
220
225
|
### FleetView
|
|
221
226
|
|
|
222
|
-
|
|
227
|
+
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.
|
|
228
|
+
|
|
229
|
+
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.
|
|
230
|
+
|
|
231
|
+
```text
|
|
232
|
+
agents: 3 running, 2 queued, 4 done · ~$28.70 · ctrl+shift+a to expand
|
|
233
|
+
[1 failed, 2 parked, 1 fallback] agents: 3 running, 1 failed, 2 parked · ctrl+shift+a to expand
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
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.
|
|
237
|
+
|
|
238
|
+
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.
|
|
239
|
+
|
|
240
|
+
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.
|
|
241
|
+
|
|
242
|
+
Enabled quota-parked schedules awaiting dispatch count as parked even with no agent record, and clear when dispatched, disabled or removed.
|
|
243
|
+
|
|
244
|
+
Failed/parked/paused-workflow/fallback counts lead in a shared, uncut flag; at narrow widths these compact to `[E1 P2 B1 F1]` (failed, parked, blocked/paused, fallback). Compact counts above 99 read `99+`; spaces yield before alerts, so all four still fit at 20 columns (`[E99+P99+B99+F99+]`). Below 20 columns alerts yield in reverse priority (fallback, paused, parked), never ahead of the failure flag; if even its count cannot fit, the marker is `!`, never the misleading `[fb]`. Cost uses record-borne estimates only when `showCost` is on; it is the sum for the agents currently summarized, not a session ledger, and excludes workflow-container spend. Narrow lines drop trailing detail and the hint before leading alerts. An empty fleet still hides entirely; the standalone summary renderer has a one-line zero-count form.
|
|
245
|
+
|
|
246
|
+
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.
|
|
247
|
+
|
|
248
|
+
Expanded, the existing navigable list renders **below** the editor:
|
|
223
249
|
|
|
224
250
|
```
|
|
225
|
-
esc to interrupt · ← for agents · ↓ to manage
|
|
251
|
+
esc to interrupt · ← for agents · ↓ to manage · ctrl+shift+a to collapse
|
|
226
252
|
|
|
227
253
|
● main
|
|
228
254
|
○ workflow audit-src 12/40 agents · 32s · ↓ 26.4k tokens
|
|
229
|
-
○ general-purpose Sleep then report 1
|
|
230
|
-
○ general-purpose Sleep then report 2
|
|
231
|
-
|
|
255
|
+
○ general-purpose Sleep then report 1 ▶ running · 11s · ↓ 13.1k tokens
|
|
256
|
+
○ general-purpose Sleep then report 2 ▶ running · 11s · ↓ 13.1k tokens
|
|
257
|
+
↓ 3 more: 2 running, 1 failed
|
|
232
258
|
```
|
|
233
259
|
|
|
234
|
-
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 are shown (pending/queued agents with no session yet appear once they start). At an **empty prompt**, press `↓` (or `←`) to move focus from the prompt into the list — the selected row is marked `●`, the rest `○`. The row block stays at five lines once the roster reaches five rows; overflowing lists show three rows plus `↑ N more` / `↓ N more` indicators (blank at an edge), without moving the content below it. The selected row renders in the theme's primary text color rather than the muted/dim treatment of the others; an agent with a configured `color` shows its badge there too, bolded. `↑`/`↓` move the selection, `Enter` opens the selected agent's live conversation overlay (it auto-updates as the agent works), and `Esc` (or `↑` above `main`) returns to the prompt. Selecting `main` returns to the normal view. Inside the overlay, press `Enter` to steer the running agent — type a message and `Enter` to send it (`Esc` or an empty submit returns), and it redirects the agent the same way the `steer_subagent` tool does. On a running foreground `Agent` call, press `b` to hand that run to the background; the hint only shows where the handoff is available. A viewer stays open when its agent finishes so you can read the final output, and finished agents linger in the list for a few seconds before dropping out. Typing anything at a non-empty prompt behaves normally — the list only captures arrow keys when the prompt is empty. Disable it entirely via `/agents → Settings → Fleet view`.
|
|
235
|
-
With [`showModel`](#persistent-settings) on, each row names the model and thinking level it runs at (
|
|
260
|
+
Running [workflows](#subagentworkflow) appear as a single `workflow` row above the agents, carrying their agent counts in place of a description. `Enter` on one opens the same two-pane inspector `/agents → Workflows` does, rather than a conversation overlay. A run's own agents are *not* listed separately — they belong to the run, which reports for them, so they are filtered out of the fleet list, the above-editor widget, the `/agents` menus and `@handle` resolution exactly as nested children are. They are also outside the `maxConcurrent` pool: the run has its own concurrency cap, and routing a fan-out through the session pool as well would let one workflow starve everything else. The agents are ordered earliest-launched first, and only agents you can actually open, or outstanding failures you can inspect, are shown (pending/queued agents with no session yet appear once they start). At an **empty prompt**, press `↓` (or `←`) to move focus from the prompt into the list — the selected row is marked `●`, the rest `○`. The row block stays at five lines once the roster reaches five rows; overflowing lists show three rows plus `↑ N more: 2 running` / `↓ N more: 1 queued, 1 failed` indicators that count the hidden rows by state (blank at an edge), without moving the content below it. 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. 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`.
|
|
261
|
+
With [`showModel`](#persistent-settings) on, each row names the model and thinking level it runs at (`▶ running · sonnet 4.6 · think:high · 11s · ↓ 13.1k tokens`; the above-editor widget spells the level `thinking: high`), 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.
|
|
236
262
|
|
|
237
263
|
### Fullscreen conversation observer
|
|
238
264
|
|
|
@@ -366,7 +392,7 @@ Expanded, a notice adds `↻3 · 3 tools · 12.4k token · 4.1s`, the result pre
|
|
|
366
392
|
| Type | Tools | Model | Prompt Mode | Description |
|
|
367
393
|
|------|-------|-------|-------------|-------------|
|
|
368
394
|
| `general-purpose` | all 7 | inherit | `append` (parent twin) | Inherits the parent's full system prompt — same rules, CLAUDE.md, project conventions |
|
|
369
|
-
| `Explore` | read, bash, grep, find, ls |
|
|
395
|
+
| `Explore` | read, bash, 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` |
|
|
370
396
|
| `Plan` | read, bash, grep, find, ls | inherit | `replace` (standalone) | Software architect for implementation planning (read-only) |
|
|
371
397
|
|
|
372
398
|
The `general-purpose` agent is a **parent twin** — it receives the parent's entire system prompt plus a sub-agent context bridge, so it follows the same rules the parent does. Explore and Plan use standalone prompts tailored to their read-only roles.
|
|
@@ -399,7 +425,7 @@ An unreadable or unparseable agent file is skipped, not fatal — a warning name
|
|
|
399
425
|
color: red
|
|
400
426
|
description: Security Code Reviewer
|
|
401
427
|
tools: read, grep, find, bash
|
|
402
|
-
model: claude-opus-4-6
|
|
428
|
+
model: anthropic/claude-opus-4-6
|
|
403
429
|
thinking: high
|
|
404
430
|
max_turns: 30
|
|
405
431
|
---
|
|
@@ -436,7 +462,7 @@ All fields are optional — sensible defaults for everything.
|
|
|
436
462
|
| `memory` | — | Persistent agent memory scope: `project`, `local`, or `user`. Auto-detects read-only agents |
|
|
437
463
|
| `disallowed_tools` | — | Comma-separated tools to deny even if extensions provide them |
|
|
438
464
|
| `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` |
|
|
439
|
-
| `model` | inherit parent | Default
|
|
465
|
+
| `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 |
|
|
440
466
|
| `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 |
|
|
441
467
|
| `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited |
|
|
442
468
|
| `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` |
|
|
@@ -449,15 +475,15 @@ All fields are optional — sensible defaults for everything.
|
|
|
449
475
|
| `isolated` | `false` | Hermetic specialist mode: forces `extensions: false` + `skills: false` + drops `ext:` selectors. Only built-in tools. Distinct from `isolation: worktree` (filesystem) |
|
|
450
476
|
| `enabled` | `true` | Set to `false` to disable an agent (useful for hiding a default agent per-project) |
|
|
451
477
|
|
|
452
|
-
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,
|
|
478
|
+
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:
|
|
453
479
|
|
|
454
480
|
- `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).
|
|
455
481
|
- `isolation` holds on every path except workflows: as in Claude Code, only the script's `agent()` option decides there.
|
|
456
482
|
- `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`.
|
|
457
483
|
|
|
458
|
-
**
|
|
484
|
+
**Explicit non-tier tool inputs can use fuzzy names.** These pick the newest model; configured pins and tier/alias members remain exact. A bare family name such as `"opus"` matches several ids; exact ids and [aliases](#machine-local-model-aliases) still win first, otherwise the candidates are narrowed to pi's `enabledModels` (when any candidate is enabled), then ranked by parsed version, newest first, then by match score. Versions are read from `claude-<family>-<n>[-<n>]` (`claude-opus-4-6`, `claude-opus-5`; a trailing `-YYYYMMDD` is a date, not a minor) and `gpt-<n>[.<n>]-<variant>` (`gpt-6.1-sol`); ids with no recognised scheme rank after every versioned one. Previously the shortest id won, so `"opus"` resolved to `claude-opus-5` even with `claude-opus-4-6` enabled and `claude-opus-5` not.
|
|
459
485
|
|
|
460
|
-
**
|
|
486
|
+
**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.
|
|
461
487
|
|
|
462
488
|
### Nested subagents
|
|
463
489
|
|
|
@@ -547,6 +573,10 @@ A few rules the examples don't make obvious:
|
|
|
547
573
|
| `none` with `isolated: true` or `extensions: false` | no | `none` |
|
|
548
574
|
| `none`, or only `ext:` entries | yes | `no built-ins, extension tools only` |
|
|
549
575
|
|
|
576
|
+
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.
|
|
577
|
+
|
|
578
|
+
`Agent` has no per-call tool-override flag. Use a shell-capable type for command work; do not ask a restricted researcher to acquire `bash`.
|
|
579
|
+
|
|
550
580
|
The last two rows are separate because zero built-ins is not zero tools: `tools: none` alongside `extensions:` still surfaces every extension tool, so calling it `none` would understate what the agent can do. Without the `+ extension tools` flag an agent that can search the web through an extension (e.g. `Plan`) read as web-less.
|
|
551
581
|
|
|
552
582
|
### Shipping agents in a pi package
|
|
@@ -795,6 +825,48 @@ When background agents complete, they notify the main agent. As in Claude Code,
|
|
|
795
825
|
**Configuration:**
|
|
796
826
|
- Configure join mode in `/agents` → Settings → Join mode
|
|
797
827
|
|
|
828
|
+
## Model tiers
|
|
829
|
+
|
|
830
|
+
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.
|
|
831
|
+
|
|
832
|
+
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`.
|
|
833
|
+
|
|
834
|
+
Routed personal setup (configure the router profiles separately):
|
|
835
|
+
|
|
836
|
+
```json
|
|
837
|
+
{
|
|
838
|
+
"modelTiers": { "cheap": ["router/cheap"], "default": ["router/default"], "deep": ["router/deep"], "review": ["router/review"] },
|
|
839
|
+
"modelResolution": { "onUnmapped": "error", "onUnavailable": "error" },
|
|
840
|
+
"agentModels": { "*": "@default", "Explore": "@cheap", "code-reviewer": "@review" }
|
|
841
|
+
}
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
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.
|
|
845
|
+
|
|
846
|
+
GPT-only setup, with no Anthropic dependency or router required:
|
|
847
|
+
|
|
848
|
+
```json
|
|
849
|
+
{ "modelTiers": { "cheap": ["openai/gpt-6-luna"] }, "agentModels": { "Explore": "@cheap" } }
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
Enterprise manifest (machine-local, permissions `0600`):
|
|
853
|
+
|
|
854
|
+
```json
|
|
855
|
+
{ "version": 1, "subagents": { "modelTiers": { "cheap": ["example-provider/example-wire-model"] }, "agentModels": { "Explore": "@cheap" } } }
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
Ordered static list:
|
|
859
|
+
|
|
860
|
+
```json
|
|
861
|
+
{ "modelTiers": { "cheap": ["provider-a/fast-model", "provider-b/fast-model"] } }
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
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`.
|
|
865
|
+
|
|
866
|
+
`modelResolution.onUnmapped` and `onUnavailable` each accept `error` or `inherit`; both library defaults are `inherit`, with a visible note. Neither policy applies to quota exhaustion. Whitespace-only model inputs mean unset; `@` alone and unknown/case-mismatched tier names are visibly unmapped.
|
|
867
|
+
|
|
868
|
+
Results disclose `Model: @cheap -> provider/id via router/cheap` before the first response using admission's `resolvedModel`, then follow the answering model. Mid-run changes say `switched ... within tier`. Shared leading `[@cheap]` flags identify healthy tiers neutrally; warning flags and grouped alert counts cover only actual switches, parent inheritance, unavailable or unmapped outcomes. Structured `modelTier` outcomes survive records, events, transcripts, eviction and workflow journals. Reopened evicted sessions select/admit anew and retain old outcomes separately as `previousModelTier` and `previousModelFallback`; live resumes keep the existing session's selection. Scheduled tier jobs store `@cheap`, not today's physical pick, and resolve it again when they fire.
|
|
869
|
+
|
|
798
870
|
## Machine-local model aliases
|
|
799
871
|
|
|
800
872
|
`~/.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.
|
|
@@ -810,17 +882,21 @@ When background agents complete, they notify the main agent. As in Claude Code,
|
|
|
810
882
|
}
|
|
811
883
|
```
|
|
812
884
|
|
|
813
|
-
Alias candidates are resolved in order against authenticated models
|
|
885
|
+
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.
|
|
814
886
|
|
|
815
887
|
## Per-agent models
|
|
816
888
|
|
|
817
|
-
`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
|
|
889
|
+
`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.
|
|
818
890
|
|
|
819
891
|
```json
|
|
820
|
-
{ "agentModels": { "Plan": "
|
|
892
|
+
{ "agentModels": { "Plan": "@deep", "Explore": "@cheap", "*": "@default" } }
|
|
821
893
|
```
|
|
822
894
|
|
|
823
|
-
Precedence, highest first: the `Agent` tool's `model` parameter, `agentModels[type]`, `agentModels["*"]`, the agent definition's `model:`, the parent's model.
|
|
895
|
+
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.
|
|
896
|
+
|
|
897
|
+
- **Intentional blank-input change:** empty and whitespace-only model strings mean unset at every precedence level and on every entry path, falling through to the next default without recording a `param` or `definition` request. Previously RPC/workflow explicit blanks and runner-based configured blanks reached fuzzy resolution (and could select an available model); Agent/nested/scheduled paths skipped `""` but resolved whitespace. Nonblank inputs retain their original spelling and matching.
|
|
898
|
+
|
|
899
|
+
**Known limits.** At terminal widths below about 60 the workflow dialog's agent-level list pane may show only a fragment of the `[fallback]` flag (every other row shows it first and never clips it, and `withLeadingFlag` degrades to `[fb]` then `!`), and a very long fallback note in the dialog's detail pane is clamped to the pane capacity. The full note stays in the result text, the record, the run log and `get_subagent_result`. Tracked in [#158](https://github.com/nredd/pi-harness/issues/158).
|
|
824
900
|
|
|
825
901
|
## Model Scope
|
|
826
902
|
|
|
@@ -893,10 +969,11 @@ The `~` marks it as pi's estimate rather than a billed figure. **A cost is shown
|
|
|
893
969
|
|
|
894
970
|
Independent of `reportUsage`: this one is what you read, that one is what your session counts. Toggle via `/agents → Settings → Show cost`; applied live.
|
|
895
971
|
|
|
896
|
-
**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:
|
|
972
|
+
**Show model** (`showModel`, default `false`): whether the widget's running rows and the [FleetView](#fleetview) rows name the model driving each agent and the thinking level it is running at. Toggling it refreshes both. The widget row, then the FleetView row, which writes the level `think:high` so it cannot read as a duplicate of the live `thinking · 4s` activity beside it:
|
|
897
973
|
|
|
898
974
|
```text
|
|
899
975
|
├─ ⠹ Explore inspect code · sonnet 4.6 · thinking: high · ↻3 · 8.2k token · 4.1s
|
|
976
|
+
○ Explore inspect code ▶ running · sonnet 4.6 · think:high · thinking · 4s · 41s · ↓ 8.2k tokens
|
|
900
977
|
```
|
|
901
978
|
|
|
902
979
|
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`.
|
|
@@ -966,20 +1043,24 @@ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions ca
|
|
|
966
1043
|
|
|
967
1044
|
| Event | When | Key fields |
|
|
968
1045
|
|-------|------|------------|
|
|
969
|
-
| `subagents:created` | `Agent`-tool background spawn, a detached resume, or a foreground `Agent` call handed to the background (after its `started`) — **not** cross-extension RPC, scheduler, or `@handle` spawns, which are first seen at `subagents:started` | `id`, `type`, `description`, `isBackground` (always `true`) |
|
|
970
|
-
| `subagents:started` | Agent transitions to running (including queued→running) | `id`, `type`, `description` |
|
|
971
|
-
| `subagents:completed` | Agent finished successfully (background and foreground) | `id`, `type`, `description`, `status`, `durationMs`, `tokens` (display total, `{ input, output, total }` — see the note below), `usage` (the run's spend as a pi `Usage`: token components including `cacheRead`, plus `cost.total` in USD; absent when nothing was spent), `toolUses`, `result` |
|
|
1046
|
+
| `subagents:created` | `Agent`-tool background spawn, a detached resume, or a foreground `Agent` call handed to the background (after its `started`) — **not** cross-extension RPC, scheduler, or `@handle` spawns, which are first seen at `subagents:started` | `id`, `type`, `description`, `isBackground` (always `true`); plus `modelFallback`/`modelNote` when already known, see below |
|
|
1047
|
+
| `subagents:started` | Agent transitions to running (including queued→running) | `id`, `type`, `description`; plus `modelFallback`/`modelNote` only when the spawn path already knew it, see below |
|
|
1048
|
+
| `subagents:completed` | Agent finished successfully (background and foreground) | `id`, `type`, `description`, `status`, `durationMs`, `tokens` (display total, `{ input, output, total }` — see the note below), `usage` (the run's spend as a pi `Usage`: token components including `cacheRead`, plus `cost.total` in USD; absent when nothing was spent), `toolUses`, `result`; plus `modelFallback` (`{ input, used, also?, via? }`: `also` and `via` retain older disclosure details; `via` is `"parent"` or `"agent"`) and `modelNote` when a configured model did not resolve and the agent ran on another |
|
|
972
1049
|
| `subagents:failed` | Agent errored, stopped, or aborted (background and foreground) | identical payload to `subagents:completed` — both are built by the same formatter, so `error` and `status` are present on that row too, just empty |
|
|
973
1050
|
| `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` |
|
|
974
|
-
| `subagents:usage` | Every assistant message any agent spends on — top-level, nested and workflow children alike, live while it runs | 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 |
|
|
975
|
-
| `subagents:compacted` | Agent's session successfully compacted | `id`, `type`, `description`, `reason` (`"manual"` / `"threshold"` / `"overflow"`), `tokensBefore`, `compactionCount` |
|
|
976
|
-
| `subagents:disposed` | After actual shutdown disposal of this activation's manager and child sessions. Not emitted when a session switch detaches surviving background work
|
|
1051
|
+
| `subagents:usage` | Every assistant message any agent spends on — top-level, nested and workflow children alike, live while it runs (not for an agent detached by `/new`, `/resume`, `/fork` or `/reload`: its observers are dropped after the switch) | Always: `id`, `type`, `description`, `depth`, `usage` (that one message's spend as a pi `Usage`; never emitted for a message that spent nothing). Omitted when unknown, never `undefined`: `model` (`provider/id`) and `thinking` (the effective level) until the session has reported them; `requestedThinking`, only when the agent did not get the level it asked for; `parentAgentId`, nested agents only; `workflowId`, a workflow's agents only; `modelFallback` and `modelNote` when a configured model did not resolve |
|
|
1052
|
+
| `subagents:compacted` | Agent's session successfully compacted (not emitted for an agent detached by a session switch) | `id`, `type`, `description`, `reason` (`"manual"` / `"threshold"` / `"overflow"`), `tokensBefore`, `compactionCount`; plus `modelFallback`/`modelNote` when known |
|
|
1053
|
+
| `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 | `{}` |
|
|
977
1054
|
| `subagents:scheduled` | Schedule lifecycle change | `{ type: "added" \| "removed" \| "updated" \| "fired" \| "error", … }` (job/agentId/error fields per type) |
|
|
978
1055
|
| `subagents:scheduler_ready` | Scheduler bound to session, enabled jobs armed | `sessionId`, `jobCount` |
|
|
979
1056
|
| `subagents:ready` | RPC handlers registered and armed — fired on session start; not emitted in a session that excludes pi-subagents | `{}` (empty object) |
|
|
980
1057
|
| `subagents:settings_loaded` | Persisted settings applied at extension init | `settings` (merged global + project) |
|
|
981
1058
|
| `subagents:settings_changed` | `/agents` → Settings mutation was applied | `settings`, `persisted` (`boolean` — `false` on write failure) |
|
|
982
1059
|
|
|
1060
|
+
**Tier provenance on events.** Payloads that disclose fallback provenance also carry `modelTier` and `modelNote` for tier selections. Outcomes are `routed`, `static`, `unmapped`, `unavailable` or `exhausted`; successful outcomes include the tier, bound profile/member, admitted model and latest answering model, and static outcomes list skipped members and whether a quota signal was used. `subagents:record` history entries and detached completion spools retain the same outcome. Persisted `modelTier.model` contains identity only (`provider`, `id`, `name`, `api`), never provider URLs, headers or other credential-bearing model overlays.
|
|
1061
|
+
|
|
1062
|
+
**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.
|
|
1063
|
+
|
|
983
1064
|
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.
|
|
984
1065
|
|
|
985
1066
|
`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.
|
|
@@ -1043,7 +1124,7 @@ pi.events.emit("subagents:rpc:spawn", {
|
|
|
1043
1124
|
|
|
1044
1125
|
`options` is the manager's spawn-option object, not the `Agent` tool's parameter schema — the background flag is `isBackground`, and the tool's snake_case `run_in_background` is forwarded verbatim and ignored. Every RPC spawn returns its id immediately and runs detached either way; `isBackground: true` is what makes the agent occupy one of the `maxConcurrent` slots (and queue behind them when they are full). It does not affect `subagents:created`, which is never emitted for an RPC spawn at all — the first event you see for one is `subagents:started`. 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.
|
|
1045
1126
|
|
|
1046
|
-
`options.model` accepts either a `Model` object (e.g. `ctx.model`) or a `"provider/modelId"` string — strings are
|
|
1127
|
+
`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.
|
|
1047
1128
|
|
|
1048
1129
|
`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.
|
|
1049
1130
|
|
|
@@ -1192,6 +1273,7 @@ This is useful for creating agents that inherit extension tools but should not h
|
|
|
1192
1273
|
docs/ # Long-form guides (shipped to npm; README links out to them)
|
|
1193
1274
|
workflows.md # SubagentWorkflow: writing, editing, saving and re-running scripts
|
|
1194
1275
|
rpc.md # Cross-extension integration: pi.events, subagents:rpc:*, manager registry
|
|
1276
|
+
home-injection.md # Injected home boundaries, call-time defaults and filesystem canaries
|
|
1195
1277
|
examples/
|
|
1196
1278
|
workflows/ # Runnable examples, executed by test/workflow-examples.test.ts
|
|
1197
1279
|
agent-tool-description.md
|
|
@@ -1227,7 +1309,9 @@ src/
|
|
|
1227
1309
|
|
|
1228
1310
|
# Invocation surface
|
|
1229
1311
|
invocation-config.ts # Shared tool-parameter schemas (isolation, join, thinking, ...)
|
|
1230
|
-
model-
|
|
1312
|
+
model-selection.ts # Shared precedence, scope and resolution outcome; stored on the agent record
|
|
1313
|
+
model-tiers.ts # Exact static/routed tiers, admission, policy and durable disclosure
|
|
1314
|
+
model-resolver.ts # Model matching: exact provider/modelId, aliases, fuzzy fallback ranked by version
|
|
1231
1315
|
enabled-models.ts # Read pi's enabledModels settings (project over global)
|
|
1232
1316
|
model-scope.ts # scopeModels allowlist policy, shared by top-level and nested tools
|
|
1233
1317
|
mention.ts # `@handle message` grammar: suggestion triggers and send parsing
|
|
@@ -1239,6 +1323,8 @@ src/
|
|
|
1239
1323
|
schedule-store.ts # PID-locked, session-scoped, atomic schedule persistence
|
|
1240
1324
|
|
|
1241
1325
|
# Context & environment
|
|
1326
|
+
home-paths.ts # Injected home normalization and Pi agent-directory defaults
|
|
1327
|
+
resource-loader.ts # Fresh scoped settings paths for Pi's real resource/package loader
|
|
1242
1328
|
memory.ts # Persistent agent memory (resolve, read, build prompt blocks)
|
|
1243
1329
|
skill-loader.ts # Preload skills (Pi-standard + Agent Skills spec layouts)
|
|
1244
1330
|
output-file.ts # Streaming output file transcripts for agent sessions
|
|
@@ -1260,7 +1346,8 @@ src/
|
|
|
1260
1346
|
agent-widget.ts # Persistent widget: spinners, activity, status icons, theming
|
|
1261
1347
|
agent-row.ts # The Agent tool row: live one-line summary, expanded prompt/tags/transcript
|
|
1262
1348
|
notification-view.ts # Completion notice renderer: one-line collapsed, stats/preview/link expanded
|
|
1263
|
-
fleet-list.ts # FleetView: navigable
|
|
1349
|
+
fleet-list.ts # FleetView: one-line summary/disclosure and navigable list below the editor
|
|
1350
|
+
fleet-state.ts # Versioned journal checkpoints, bounded failure details and exact overflow counters
|
|
1264
1351
|
conversation-viewer.ts # Live conversation overlay for viewing agent sessions
|
|
1265
1352
|
viewer-keys.ts # Viewer scroll keys resolved through user keybindings
|
|
1266
1353
|
agent-mention.ts # `@` roster (running, resumable, and startable agents) + popup rows
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nredd/pi-subagents",
|
|
3
|
-
"version": "0.22.0-nredd.
|
|
3
|
+
"version": "0.22.0-nredd.8",
|
|
4
4
|
"description": "A pi extension that brings Claude Code-like sub-agents and workflow orchestration to pi — parallel execution, live widget, fleet view, custom agent types, mid-run steering, dynamic workflows, Claude Code compatibility, look and feel.",
|
|
5
5
|
"author": "tintinweb",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,30 +42,30 @@
|
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
|
|
44
44
|
"prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build",
|
|
45
|
-
"test": "vitest run",
|
|
45
|
+
"test": "vitest run --coverage",
|
|
46
46
|
"test:watch": "vitest",
|
|
47
47
|
"test:e2e": "vitest run e2e --reporter=verbose",
|
|
48
48
|
"typecheck": "tsc --noEmit",
|
|
49
49
|
"lint": "biome check src/ test/",
|
|
50
50
|
"lint:fix": "biome check --fix src/ test/",
|
|
51
51
|
"check": "npm run lint && npm run typecheck && npm run test && npm run test:bun",
|
|
52
|
-
"test:bun": "bun test test/session-scenarios.test.ts test/fleet-session.test.ts test/viewer-session.test.ts test/worktree-isolation-e2e.test.ts",
|
|
52
|
+
"test:bun": "bun test test/session-scenarios.test.ts test/fleet-session.test.ts test/viewer-session.test.ts test/worktree-isolation-e2e.test.ts --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir=coverage/bun && PI_EXPECT_BUN_RUNTIME=1 node ../../scripts/run-vitest.ts bun test/resource-loader-home.test.ts --coverage.enabled=false",
|
|
53
53
|
"test:coverage": "vitest run --coverage",
|
|
54
54
|
"bench": "vitest bench",
|
|
55
55
|
"bench:ab": "node test/perf/ab.mjs"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@biomejs/biome": "^2.4.14",
|
|
59
|
-
"@earendil-works/pi-ai": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.
|
|
60
|
-
"@earendil-works/pi-coding-agent": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.
|
|
61
|
-
"@earendil-works/pi-tui": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.
|
|
59
|
+
"@earendil-works/pi-ai": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.8/earendil-works-pi-ai-1.3.0.tgz",
|
|
60
|
+
"@earendil-works/pi-coding-agent": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.8/earendil-works-pi-coding-agent-1.3.0.tgz",
|
|
61
|
+
"@earendil-works/pi-tui": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.8/earendil-works-pi-tui-1.3.0.tgz",
|
|
62
62
|
"@xterm/headless": "5.5.0",
|
|
63
63
|
"@sinclair/typebox": "^0.34.49",
|
|
64
64
|
"@types/node": "^25.5.0",
|
|
65
|
-
"@vitest/coverage-istanbul": "
|
|
65
|
+
"@vitest/coverage-istanbul": "5.0.3",
|
|
66
66
|
"typebox": "^1.3.27",
|
|
67
67
|
"typescript": "^6.0.0",
|
|
68
|
-
"vitest": "
|
|
68
|
+
"vitest": "5.0.3"
|
|
69
69
|
},
|
|
70
70
|
"pi": {
|
|
71
71
|
"extensions": [
|
package/src/agent-file-toggle.ts
CHANGED
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
|
|
28
28
|
import { existsSync } from "node:fs";
|
|
29
29
|
import { join, sep } from "node:path";
|
|
30
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
31
30
|
import { parseAgentFrontmatter } from "./custom-agents.js";
|
|
31
|
+
import { resolveAgentDir } from "./home-paths.js";
|
|
32
32
|
import type { AgentConfig } from "./types.js";
|
|
33
33
|
|
|
34
34
|
export type AgentFileLocation = "project" | "workspace" | "personal";
|
|
35
35
|
|
|
36
36
|
export const projectAgentsDir = (cwd: string = process.cwd()) => join(cwd, ".pi", "agents");
|
|
37
37
|
export const workspaceAgentsDir = (cwd: string = process.cwd()) => join(cwd, ".agents", "agents");
|
|
38
|
-
export const personalAgentsDir = () => join(
|
|
38
|
+
export const personalAgentsDir = (homeDir?: string) => join(resolveAgentDir(homeDir), "agents");
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Find the file path of a custom agent by name, in discovery-precedence order
|
|
@@ -46,12 +46,13 @@ export const personalAgentsDir = () => join(getAgentDir(), "agents");
|
|
|
46
46
|
export function findAgentFile(
|
|
47
47
|
name: string,
|
|
48
48
|
cwd: string = process.cwd(),
|
|
49
|
+
homeDir?: string,
|
|
49
50
|
): { path: string; location: AgentFileLocation } | undefined {
|
|
50
51
|
const projectPath = join(projectAgentsDir(cwd), `${name}.md`);
|
|
51
52
|
if (existsSync(projectPath)) return { path: projectPath, location: "project" };
|
|
52
53
|
const workspacePath = join(workspaceAgentsDir(cwd), `${name}.md`);
|
|
53
54
|
if (existsSync(workspacePath)) return { path: workspacePath, location: "workspace" };
|
|
54
|
-
const personalPath = join(personalAgentsDir(), `${name}.md`);
|
|
55
|
+
const personalPath = join(personalAgentsDir(homeDir), `${name}.md`);
|
|
55
56
|
if (existsSync(personalPath)) return { path: personalPath, location: "personal" };
|
|
56
57
|
return undefined;
|
|
57
58
|
}
|
|
@@ -83,11 +84,12 @@ export function locateAgentFile(
|
|
|
83
84
|
sourcePath: string | undefined,
|
|
84
85
|
cwd: string = process.cwd(),
|
|
85
86
|
source?: AgentConfig["source"],
|
|
87
|
+
homeDir?: string,
|
|
86
88
|
): { path: string; location: AgentFileLocation } | undefined {
|
|
87
89
|
if (source !== "package" && sourcePath && existsSync(sourcePath)) {
|
|
88
90
|
return { path: sourcePath, location: classifyAgentDir(sourcePath, cwd) };
|
|
89
91
|
}
|
|
90
|
-
return findAgentFile(name, cwd);
|
|
92
|
+
return findAgentFile(name, cwd, homeDir);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
/**
|