@nredd/pi-subagents 0.22.0-nredd.5 → 0.22.0-nredd.7
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 +38 -8
- package/package.json +1 -1
- package/src/agent-manager.ts +307 -110
- package/src/index.ts +40 -17
- package/src/invocation-config.ts +5 -19
- package/src/nested-tools.ts +9 -0
- package/src/process-budget.ts +343 -0
- package/src/schedule.ts +5 -1
- package/src/settings.ts +75 -2
- package/src/types.ts +6 -0
package/README.md
CHANGED
|
@@ -163,7 +163,7 @@ Schedule formats:
|
|
|
163
163
|
- **One-shot relative** — `"+10m"`, `"+2h"`, `"+1d"`. Fires once at that future time.
|
|
164
164
|
- **One-shot absolute** — full ISO timestamp, e.g. `"2026-12-25T09:00:00.000Z"`.
|
|
165
165
|
|
|
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.
|
|
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.
|
|
167
167
|
|
|
168
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.
|
|
169
169
|
|
|
@@ -437,7 +437,7 @@ All fields are optional — sensible defaults for everything.
|
|
|
437
437
|
| `disallowed_tools` | — | Comma-separated tools to deny even if extensions provide them |
|
|
438
438
|
| `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
439
|
| `model` | inherit parent | Default model — `provider/modelId`, [machine-local alias](#machine-local-model-aliases), or fuzzy name (`"haiku"`, `"sonnet"`). An explicit `Agent({ model })` selection overrides this default. Resolved tolerantly (`.`/`-` and a trailing date stamp are interchangeable) and falls back to the same model under another provider if the named one doesn't have it |
|
|
440
|
-
| `thinking` | inherit | off, minimal, low, medium, high, xhigh, max —
|
|
440
|
+
| `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
441
|
| `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited |
|
|
442
442
|
| `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` |
|
|
443
443
|
| `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:` |
|
|
@@ -449,7 +449,7 @@ All fields are optional — sensible defaults for everything.
|
|
|
449
449
|
| `isolated` | `false` | Hermetic specialist mode: forces `extensions: false` + `skills: false` + drops `ext:` selectors. Only built-in tools. Distinct from `isolation: worktree` (filesystem) |
|
|
450
450
|
| `enabled` | `true` | Set to `false` to disable an agent (useful for hiding a default agent per-project) |
|
|
451
451
|
|
|
452
|
-
Frontmatter is authoritative for `
|
|
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, then the parent session supplies the fallback; 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
453
|
|
|
454
454
|
- `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
455
|
- `isolation` holds on every path except workflows: as in Claude Code, only the script's `agent()` option decides there.
|
|
@@ -479,7 +479,18 @@ allowed_subagents: support-file-finder, support-callsite-tracer # or `all`
|
|
|
479
479
|
|
|
480
480
|
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.
|
|
481
481
|
|
|
482
|
-
Nested children occupy no
|
|
482
|
+
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.
|
|
483
|
+
|
|
484
|
+
**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:
|
|
485
|
+
|
|
486
|
+
```yaml
|
|
487
|
+
---
|
|
488
|
+
# ~/.pi/agent/agents/implementer.md
|
|
489
|
+
allowed_subagents: code-reviewer
|
|
490
|
+
---
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
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.
|
|
483
494
|
|
|
484
495
|
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.
|
|
485
496
|
|
|
@@ -587,7 +598,7 @@ Launch a sub-agent.
|
|
|
587
598
|
| `thinking` | string | no | Thinking level: off, minimal, low, medium, high, xhigh, max (availability depends on pi version and model) |
|
|
588
599
|
| `max_turns` | number | no | Max agentic turns. Omit for unlimited (default) |
|
|
589
600
|
| `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 |
|
|
590
|
-
| `resume` | string | no | Agent ID to resume a previous session. The resumed run gets a fresh `max_turns` budget |
|
|
601
|
+
| `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 |
|
|
591
602
|
| `isolated` | boolean | no | No extension/MCP tools |
|
|
592
603
|
| `isolation` | `"off"` \| `"worktree"` | no | `worktree` runs in an isolated git worktree; `off` (the default) does not. Absent from the schema entirely when `worktreeIsolation: false` |
|
|
593
604
|
| `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` |
|
|
@@ -748,10 +759,26 @@ There are two independent pools.
|
|
|
748
759
|
|
|
749
760
|
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.
|
|
750
761
|
|
|
751
|
-
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. A *background* resume does take a background slot and queues behind them like any other background agent.
|
|
762
|
+
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.
|
|
752
763
|
|
|
753
764
|
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.
|
|
754
765
|
|
|
766
|
+
### Process budget
|
|
767
|
+
|
|
768
|
+
**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.
|
|
769
|
+
|
|
770
|
+
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.
|
|
771
|
+
|
|
772
|
+
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.
|
|
773
|
+
|
|
774
|
+
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.
|
|
775
|
+
|
|
776
|
+
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.
|
|
777
|
+
|
|
778
|
+
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.
|
|
779
|
+
|
|
780
|
+
### Sending to the background
|
|
781
|
+
|
|
755
782
|
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.
|
|
756
783
|
|
|
757
784
|
## Join Strategies
|
|
@@ -827,6 +854,8 @@ Runtime tuning values set via `/agents` → Settings (max concurrency, max foreg
|
|
|
827
854
|
|
|
828
855
|
**Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `10`, max foreground concurrency `0` = unlimited, default max turns unlimited, grace turns `5`, nested depth `2`, join mode `smart`, defaults enabled).
|
|
829
856
|
|
|
857
|
+
**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.
|
|
858
|
+
|
|
830
859
|
**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.
|
|
831
860
|
|
|
832
861
|
**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.
|
|
@@ -872,7 +901,7 @@ Independent of `reportUsage`: this one is what you read, that one is what your s
|
|
|
872
901
|
|
|
873
902
|
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`.
|
|
874
903
|
|
|
875
|
-
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
|
|
904
|
+
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:
|
|
876
905
|
|
|
877
906
|
```text
|
|
878
907
|
↳ anthropic/claude-haiku-4-5 · thinking: low (asked max) · background
|
|
@@ -1012,7 +1041,7 @@ pi.events.emit("subagents:rpc:spawn", {
|
|
|
1012
1041
|
});
|
|
1013
1042
|
```
|
|
1014
1043
|
|
|
1015
|
-
`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 the
|
|
1044
|
+
`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.
|
|
1016
1045
|
|
|
1017
1046
|
`options.model` accepts either a `Model` object (e.g. `ctx.model`) or a `"provider/modelId"` string — strings are resolved against `ctx.modelRegistry` at the RPC boundary, so cross-extension callers can forward serializable values without losing auth context. 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.
|
|
1018
1047
|
|
|
@@ -1185,6 +1214,7 @@ src/
|
|
|
1185
1214
|
agent-runner.ts # Session creation, execution, graceful max_turns, steer/resume
|
|
1186
1215
|
agent-manager.ts # Agent lifecycle, concurrency queue, completion notifications
|
|
1187
1216
|
nested-tools.ts # Delegation tools handed to subagents (nested spawn/collect/steer)
|
|
1217
|
+
process-budget.ts # Process-wide running-agent budget shared by every manager (maxActiveSubagents)
|
|
1188
1218
|
child-context.ts # AsyncLocalStorage flag marking work done for a child session
|
|
1189
1219
|
abortable.ts # Race a wait against Esc without cancelling the background child
|
|
1190
1220
|
model-call.ts # In-flight model call tracking: thinking / writing / stalled?
|
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.7",
|
|
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",
|