@narumitw/pi-subagents 0.30.1 → 0.33.0
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 +31 -15
- package/package.json +7 -7
- package/src/agents.ts +20 -15
- package/src/config-ui.ts +368 -66
- package/src/context.ts +13 -16
- package/src/execution.ts +360 -337
- package/src/limits.ts +8 -2
- package/src/params.ts +26 -9
- package/src/render.ts +2 -4
- package/src/settings.ts +84 -2
- package/src/stateful-prompt.ts +2 -5
- package/src/stateful.ts +25 -47
- package/src/subagents.ts +30 -12
- package/src/workspace.ts +7 -6
package/README.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@narumitw/pi-subagents) [](https://pi.dev) [](./LICENSE)
|
|
4
4
|
|
|
5
|
-
`@narumitw/pi-subagents` is a native [Pi coding agent](https://pi.dev) extension for delegating work to specialized agents.
|
|
5
|
+
`@narumitw/pi-subagents` is a native [Pi coding agent](https://pi.dev) extension for delegating work to specialized agents. By default, the blocking batch `subagent` tool uses isolated Pi subprocesses while four detached tools run reusable agents either as subprocess-backed logical sessions or as public-SDK in-process child sessions. Users can keep all five tools, choose async-only delegation, or retain only blocking batches.
|
|
6
6
|
|
|
7
7
|
Use it to split independent research, planning, implementation, and review work across focused workers. Under the default next-turn delivery policy, background delegation is for work the current response does not depend on. Opt-in auto-resume also supports final-answer-dependent background work by requesting a synthesis turn after completion.
|
|
8
8
|
|
|
9
9
|
## ✨ Features
|
|
10
10
|
|
|
11
|
-
-
|
|
11
|
+
- Offers all delegation methods by default, with goal-oriented presets for async-only or blocking-only workflows.
|
|
12
12
|
- Keeps batch workers isolated in `pi --mode json -p --no-session` subprocesses.
|
|
13
13
|
- Registers detached stateful lifecycle tools by default; completion can stay queued for the next turn or opt into an idle root synthesis turn.
|
|
14
14
|
- Supports an opt-in public-SDK `in-process` stateful transport with one reusable child `AgentSession` per `agentId`.
|
|
@@ -43,7 +43,17 @@ pi -e ./extensions/pi-subagents
|
|
|
43
43
|
|
|
44
44
|
## 🛠️ Pi tool
|
|
45
45
|
|
|
46
|
-
`pi-subagents`
|
|
46
|
+
`pi-subagents` registers all five tools by default. Run `/subagents`, choose **Change delegation**, review the concrete tool changes, then select **Save and reload** to apply one of these workflows:
|
|
47
|
+
|
|
48
|
+
| Workflow | Registered tools |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| **All delegation methods** (default) | Blocking `subagent` plus all four detached lifecycle tools |
|
|
51
|
+
| **Async only** | `subagent_spawn`, `subagent_send`, `subagent_manage`, and `subagent_mailbox`; blocking `subagent` is omitted |
|
|
52
|
+
| **Blocking only** | Blocking `subagent` only; equivalent to the existing `stateful.enabled: false` behavior |
|
|
53
|
+
|
|
54
|
+
The preview compares the selection with the tools registered in the current session, even when a manual settings edit is pending, and remains read-only until confirmation. Escape or **Cancel** leaves settings unchanged. Tool removal requires an extension reload because Pi does not expose extension tool unregistration. To avoid aborting work or removing isolated worktrees during `session_shutdown`, workflow changes are blocked while detached agents are retained; finish or clear them through **Current agents** first. Pi owns reload-error reporting and does not return a success result to extensions, so the save notification also tells users to run `/reload` if the tool surface does not refresh.
|
|
55
|
+
|
|
56
|
+
The available tools are:
|
|
47
57
|
|
|
48
58
|
- `subagent` — delegate blocking single, parallel, fan-in, or chained batch work. The main agent cannot process queued steering until the call returns.
|
|
49
59
|
- `subagent_spawn` and related lifecycle tools — when enabled, start reusable detached work, return immediately, and receive bounded completion messages automatically.
|
|
@@ -52,7 +62,7 @@ Choose the API by lifecycle:
|
|
|
52
62
|
|
|
53
63
|
| Need | Use |
|
|
54
64
|
| --- | --- |
|
|
55
|
-
| A delegated result is required before the root's next action under default next-turn delivery |
|
|
65
|
+
| A delegated result is required before the root's next action under default next-turn delivery | Use one blocking `subagent` call when registered. In **Async only**, complete the critical-path work directly or switch workflows before delegating it |
|
|
56
66
|
| Broad research/review the current response does not depend on | Prefer one `subagent_spawn` covering related branches, when lifecycle tools are enabled |
|
|
57
67
|
| Final-answer-dependent broad work with `completionDelivery: "auto-resume"` | Prefer one `subagent_spawn`; completion requests a synthesis turn |
|
|
58
68
|
| Reusable history, follow-ups, or mailboxes | `subagent_spawn` and lifecycle tools, when enabled |
|
|
@@ -75,7 +85,7 @@ For `subagent_spawn`, the root agent selects the lowest thinking level sufficien
|
|
|
75
85
|
|
|
76
86
|
## 🧭 Proactive use
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
When registered, the blocking `subagent` tool advertises only blocking guidance. When stateful lifecycle tools
|
|
79
89
|
are registered, `subagent_spawn` adds detached guidance for the active completion-delivery policy.
|
|
80
90
|
Changing the policy through `/subagents settings` refreshes that guidance immediately.
|
|
81
91
|
|
|
@@ -86,8 +96,10 @@ Count-selection guidance:
|
|
|
86
96
|
- `subagent` is deliberately blocking: while it runs, the main agent cannot answer queued steering.
|
|
87
97
|
Use it when delegated outputs are required before the next root action and waiting is intentional.
|
|
88
98
|
- With default `completionDelivery: "next-turn"`, prefer **one detached `subagent_spawn`** for broad
|
|
89
|
-
research or review only when the current response does not depend on its result.
|
|
90
|
-
|
|
99
|
+
research or review only when the current response does not depend on its result. When blocking
|
|
100
|
+
`subagent` is registered, use it for required delegated output because an idle root is not awakened.
|
|
101
|
+
In **Async only**, complete required work directly, opt into `auto-resume` when a later synthesis
|
|
102
|
+
turn is appropriate, or switch delegation workflows.
|
|
91
103
|
- With `completionDelivery: "auto-resume"`, prefer one detached `subagent_spawn` for broad related
|
|
92
104
|
research or review even when the final answer depends on it; completion requests a later synthesis
|
|
93
105
|
turn. Do not choose blocking parallel fan-out merely to keep delegation in one turn.
|
|
@@ -215,7 +227,7 @@ Run a chain where each step receives the previous output:
|
|
|
215
227
|
|
|
216
228
|
Stateful lifecycle tools are available by default. `subagent_spawn` is detached: it schedules work, returns immediately with an opaque `agentId`, and later injects a bounded `pi-subagent-completion` custom message. Completions that settle in the same dispatch window are batched, and the broker allows at most one in-flight root wake until that parent turn starts.
|
|
217
229
|
|
|
218
|
-
Detached work follows a non-polling policy. With default `next-turn` delivery, prefer one bounded `subagent_spawn` for related asynchronous research or review only when the current response does not depend on its result
|
|
230
|
+
Detached work follows a non-polling policy. With default `next-turn` delivery, prefer one bounded `subagent_spawn` for related asynchronous research or review only when the current response does not depend on its result. If it does, use blocking `subagent` when registered; in **Async only**, complete required work directly, opt into `auto-resume` when a later synthesis turn is appropriate, or switch workflows. With opt-in `auto-resume`, detached broad work may be final-answer-dependent because completion requests a synthesis turn after the root settles. In either mode, do useful non-overlapping main-agent work immediately, do not poll `subagent_manage` with `action: "list"` or `subagent_mailbox` with `action: "read"`, and do not duplicate delegated work. Add another detached agent only for truly independent work with safe workspace concurrency. Detached lifecycle work intentionally has no `subagent_wait` tool.
|
|
219
231
|
|
|
220
232
|
A detached agent additionally needs a concrete isolation or specialization benefit such as independent review, bounded context/output, a distinct model/tool profile, or workspace isolation. Simple work that the main agent can perform directly should not be delegated.
|
|
221
233
|
|
|
@@ -228,13 +240,17 @@ Auto-resume is best-effort because Pi's custom-message API is fire-and-forget. S
|
|
|
228
240
|
|
|
229
241
|
The default `subprocess` transport preserves compatibility: each turn starts a fresh isolated `pi --mode json -p --no-session` child and receives sanitized, bounded history. Set `transport` to `in-process` to retain one public Pi SDK `AgentSession` per stateful `agentId`, avoiding repeated process startup while preserving native child history in memory.
|
|
230
242
|
|
|
231
|
-
Run `/subagents` in TUI mode to open the primary manager. It
|
|
243
|
+
Run `/subagents` in TUI mode to open the primary manager. It leads with the current delegation workflow, human-readable async completion behavior, and active/retained counts. **Change delegation**, **Current agents**, and **Completion behavior** cover the common workflows; agent permissions, transport/runtime details, source, and settings path remain under **Advanced settings**. Escape returns from a nested screen to a newly refreshed manager and then closes it.
|
|
232
244
|
|
|
233
|
-
The direct routes remain predictable: `/subagents settings` changes user completion delivery and applies it immediately, including refreshing the model-facing spawn guidance; `/subagents status` reports current-session runtime values separately from the configured value, source, and path; `/subagents help` summarizes
|
|
245
|
+
The direct routes remain predictable: `/subagents settings` changes user completion delivery and applies it immediately, including refreshing the model-facing spawn guidance; `/subagents status` reports current-session runtime values separately from the configured value, source, and path; `/subagents help` summarizes the single-command interface. In RPC mode, bare `/subagents` emits the same bounded status through Pi's notification protocol instead of opening a custom TUI. JSON and print modes do not emit ad hoc command output. Manual edits use `~/.pi/agent/pi-subagents.json` and take effect after reloading Pi:
|
|
234
246
|
|
|
235
247
|
```json
|
|
236
248
|
{
|
|
249
|
+
"blocking": {
|
|
250
|
+
"enabled": false
|
|
251
|
+
},
|
|
237
252
|
"stateful": {
|
|
253
|
+
"enabled": true,
|
|
238
254
|
"transport": "in-process",
|
|
239
255
|
"completionDelivery": "auto-resume",
|
|
240
256
|
"maxAgents": 16,
|
|
@@ -250,7 +266,7 @@ The direct routes remain predictable: `/subagents settings` changes user complet
|
|
|
250
266
|
}
|
|
251
267
|
```
|
|
252
268
|
|
|
253
|
-
The settings UI patches the raw JSON atomically and preserves unknown fields; it refuses to overwrite malformed or invalid settings.
|
|
269
|
+
The settings UI patches the raw JSON atomically and preserves unknown fields; it refuses to overwrite malformed or invalid settings. `blocking.enabled` defaults to `true`; set it to `false` for async-only delegation. `stateful.enabled` also defaults to `true`; its existing `false` value remains the blocking-only workflow. When stateful tools are enabled, their membership stays fixed across spawn, completion, interrupt, close, and mailbox transitions. This avoids lifecycle-driven tool-schema churn and preserves a stable provider prompt prefix for KV caching.
|
|
254
270
|
|
|
255
271
|
| Tool | Purpose |
|
|
256
272
|
| --- | --- |
|
|
@@ -277,7 +293,7 @@ The action schemas are flat for provider compatibility and reject parameters tha
|
|
|
277
293
|
}
|
|
278
294
|
```
|
|
279
295
|
|
|
280
|
-
Use the **Current
|
|
296
|
+
Use the **Current agents** action in `/subagents` to inspect the indented agent tree, lifecycle state, unread count, and available actions, or to confirm clearing retained agents. Active turns are FIFO-limited by `maxActiveTurns`; excess retained work remains in `starting` state until a slot is available. `maxAgents` separately bounds running, queued, and idle records. `parentId` creates a bounded child relationship; subtree interrupt and close operate child-first.
|
|
281
297
|
|
|
282
298
|
### Migrating from the seven-tool lifecycle surface
|
|
283
299
|
|
|
@@ -366,7 +382,7 @@ Built-in agents inherit the active/default Pi model instead of forcing a provide
|
|
|
366
382
|
|
|
367
383
|
## ⚙️ Configure agent tools
|
|
368
384
|
|
|
369
|
-
Open `/subagents
|
|
385
|
+
Open `/subagents`, choose **Advanced settings**, then **Agent tool settings** in an interactive Pi session to edit the tools each subagent may use. These are user settings stored in `~/.pi/agent/pi-subagents.json` and affect future sessions.
|
|
370
386
|
|
|
371
387
|
Compatibility: a valid legacy `pi-subagents-config.json` is migrated automatically to `pi-subagents.json`. If both files exist, the new filename takes precedence.
|
|
372
388
|
|
|
@@ -491,7 +507,7 @@ The runner explicitly reports policy continuity in result details:
|
|
|
491
507
|
|
|
492
508
|
Treat project-local agent prompts like executable project configuration: only enable them in trusted repositories. Stateful project agents require Pi's project trust; interactive use also keeps confirmation enabled by default.
|
|
493
509
|
|
|
494
|
-
Stateful records are stored as versioned mode-0600 JSON under `~/.pi/agent/pi-subagents-state/` (or the configured Pi agent directory). Records contain sanitized logical history, never process IDs or credentials. Corrupt or unsupported state is quarantined, restored agents are always inert `idle` records, and no prior side effect is automatically resumed. Retention and count limits are configurable. Downgrading is safe: older extension versions ignore this separate state directory;
|
|
510
|
+
Stateful records are stored as versioned mode-0600 JSON under `~/.pi/agent/pi-subagents-state/` (or the configured Pi agent directory). Records contain sanitized logical history, never process IDs or credentials. Corrupt or unsupported state is quarantined, restored agents are always inert `idle` records, and no prior side effect is automatically resumed. Retention and count limits are configurable. Downgrading is safe: older extension versions ignore this separate state directory; clear **Current agents** from `/subagents` before downgrade if the histories should be removed.
|
|
495
511
|
|
|
496
512
|
## 🗂️ Package layout
|
|
497
513
|
|
|
@@ -509,7 +525,7 @@ extensions/pi-subagents/
|
|
|
509
525
|
└── package.json
|
|
510
526
|
```
|
|
511
527
|
|
|
512
|
-
`index.ts` is the Pi entrypoint and forwards to `subagents.ts`; the other source modules are internal. The package exposes its Pi extension through `package.json`:
|
|
528
|
+
`index.ts` is the Pi entrypoint and forwards to `subagents.ts`; the other source modules are internal. Workflow settings remain backward compatible: older files without `blocking.enabled` keep the five-tool default, existing `stateful.enabled: false` files remain blocking-only, and older package releases ignore the new `blocking` object. The package exposes its Pi extension through `package.json`:
|
|
513
529
|
|
|
514
530
|
```json
|
|
515
531
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Pi extension for delegating work to specialized isolated subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"typebox": "^1.3.
|
|
32
|
+
"typebox": "^1.3.8"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@earendil-works/pi-agent-core": "*",
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
"@earendil-works/pi-tui": "*"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@biomejs/biome": "2.5.
|
|
42
|
-
"@earendil-works/pi-agent-core": "0.
|
|
43
|
-
"@earendil-works/pi-ai": "0.
|
|
44
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
45
|
-
"@earendil-works/pi-tui": "0.
|
|
41
|
+
"@biomejs/biome": "2.5.5",
|
|
42
|
+
"@earendil-works/pi-agent-core": "0.82.1",
|
|
43
|
+
"@earendil-works/pi-ai": "0.82.1",
|
|
44
|
+
"@earendil-works/pi-coding-agent": "0.82.1",
|
|
45
|
+
"@earendil-works/pi-tui": "0.82.1",
|
|
46
46
|
"typescript": "7.0.2"
|
|
47
47
|
},
|
|
48
48
|
"repository": {
|
package/src/agents.ts
CHANGED
|
@@ -6,15 +6,7 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
|
-
export const THINKING_LEVELS = [
|
|
10
|
-
"off",
|
|
11
|
-
"minimal",
|
|
12
|
-
"low",
|
|
13
|
-
"medium",
|
|
14
|
-
"high",
|
|
15
|
-
"xhigh",
|
|
16
|
-
"max",
|
|
17
|
-
] as const;
|
|
9
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
18
10
|
|
|
19
11
|
export type SubagentThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
20
12
|
|
|
@@ -49,6 +41,10 @@ export type SubagentTransportKind = "subprocess" | "in-process";
|
|
|
49
41
|
|
|
50
42
|
export type CompletionDelivery = "next-turn" | "auto-resume";
|
|
51
43
|
|
|
44
|
+
export interface SubagentBlockingSettings {
|
|
45
|
+
enabled?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
52
48
|
export interface SubagentRuntimeSettings {
|
|
53
49
|
enabled?: boolean;
|
|
54
50
|
transport?: SubagentTransportKind;
|
|
@@ -66,13 +62,15 @@ export interface SubagentRuntimeSettings {
|
|
|
66
62
|
|
|
67
63
|
export interface SubagentSettings {
|
|
68
64
|
agents?: Record<string, SubagentAgentConfig>;
|
|
65
|
+
blocking?: SubagentBlockingSettings;
|
|
69
66
|
stateful?: SubagentRuntimeSettings;
|
|
70
67
|
}
|
|
71
68
|
|
|
72
69
|
const BUILT_IN_AGENTS: AgentConfig[] = [
|
|
73
70
|
{
|
|
74
71
|
name: "scout",
|
|
75
|
-
description:
|
|
72
|
+
description:
|
|
73
|
+
"Read-only codebase reconnaissance; returns concise findings with paths and evidence.",
|
|
76
74
|
tools: ["read", "grep", "find", "ls", "bash"],
|
|
77
75
|
source: "built-in",
|
|
78
76
|
filePath: "built-in:scout",
|
|
@@ -185,7 +183,9 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
|
|
|
185
183
|
description: frontmatter.description,
|
|
186
184
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
187
185
|
model: frontmatter.model,
|
|
188
|
-
thinkingLevel: isThinkingLevel(frontmatter.thinkingLevel)
|
|
186
|
+
thinkingLevel: isThinkingLevel(frontmatter.thinkingLevel)
|
|
187
|
+
? frontmatter.thinkingLevel
|
|
188
|
+
: undefined,
|
|
189
189
|
systemPrompt: body,
|
|
190
190
|
source,
|
|
191
191
|
filePath,
|
|
@@ -228,7 +228,8 @@ export function discoverAgents(
|
|
|
228
228
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
229
229
|
|
|
230
230
|
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
231
|
-
const projectAgents =
|
|
231
|
+
const projectAgents =
|
|
232
|
+
scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
232
233
|
|
|
233
234
|
const agentMap = new Map<string, AgentConfig>();
|
|
234
235
|
|
|
@@ -246,7 +247,7 @@ export function discoverAgents(
|
|
|
246
247
|
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
247
248
|
}
|
|
248
249
|
|
|
249
|
-
// Apply user-configured overrides (from /subagents
|
|
250
|
+
// Apply user-configured overrides (from /subagents → Agent tool settings) on top of
|
|
250
251
|
// the final resolved agent map, regardless of agent source.
|
|
251
252
|
for (const [name, override] of Object.entries(config?.agents ?? {})) {
|
|
252
253
|
const agent = agentMap.get(name);
|
|
@@ -258,7 +259,8 @@ export function discoverAgents(
|
|
|
258
259
|
nextAgent.model = override.model === null ? undefined : override.model;
|
|
259
260
|
}
|
|
260
261
|
if (hasOwn(override, "thinkingLevel")) {
|
|
261
|
-
nextAgent.thinkingLevel =
|
|
262
|
+
nextAgent.thinkingLevel =
|
|
263
|
+
override.thinkingLevel === null ? undefined : override.thinkingLevel;
|
|
262
264
|
}
|
|
263
265
|
if (hasOwn(override, "timeoutMs")) {
|
|
264
266
|
nextAgent.timeoutMs = override.timeoutMs === null ? undefined : override.timeoutMs;
|
|
@@ -269,7 +271,10 @@ export function discoverAgents(
|
|
|
269
271
|
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
270
272
|
}
|
|
271
273
|
|
|
272
|
-
export function formatAgentList(
|
|
274
|
+
export function formatAgentList(
|
|
275
|
+
agents: AgentConfig[],
|
|
276
|
+
maxItems: number,
|
|
277
|
+
): { text: string; remaining: number } {
|
|
273
278
|
if (agents.length === 0) return { text: "none", remaining: 0 };
|
|
274
279
|
const listed = agents.slice(0, maxItems);
|
|
275
280
|
const remaining = agents.length - listed.length;
|