@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 CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@narumitw/pi-subagents)](https://www.npmjs.com/package/@narumitw/pi-subagents) [![Pi extension](https://img.shields.io/badge/Pi-extension-blue)](https://pi.dev) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
4
4
 
5
- `@narumitw/pi-subagents` is a native [Pi coding agent](https://pi.dev) extension for delegating work to specialized agents. The blocking batch `subagent` tool keeps isolated Pi subprocesses, while four detached tools run reusable agents either as subprocess-backed logical sessions or as public-SDK in-process child sessions. Together they form a fixed five-tool surface.
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
- - Registers a `subagent` tool for single-agent, parallel, fan-in, and chained delegation.
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` always registers the primary batch tool and registers the stateful lifecycle tools unless `stateful.enabled` is `false`:
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 | One blocking `subagent` call (`tasks` for synchronous parallel work) |
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
- The always-available `subagent` tool advertises only blocking guidance. When stateful lifecycle tools
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. Keep
90
- final-answer-dependent delegated work on the blocking path because an idle root is not awakened.
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; if it does, use blocking `subagent`. 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.
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 separates current-session lifecycle state, transport, completion delivery, and active/retained counts from user settings that persist across sessions. Its actions open completion settings, per-agent tool settings, current-session agent inspection/clear, status, and help. Escape returns from a nested screen to a newly refreshed manager and then closes 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 commands and compatibility routes. 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:
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. Set `"enabled": false` to remove all four stateful tools. Otherwise, the extension keeps the following tool membership 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.
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-session agents** action in `/subagents` to inspect the indented agent tree, lifecycle state, unread count, and available actions, or to confirm clearing retained agents. `/subagents:agents list|clear` remains the compatibility command for the same current-session operations. 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.
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` and choose **Agent tool settings** in an interactive Pi session to edit the tools each subagent may use. `/subagents:config` remains a documented compatibility route to the same screen. These are user settings stored in `~/.pi/agent/pi-subagents.json` and affect future sessions.
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; use `/subagents:agents clear` before downgrade if the histories should be removed.
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.30.1",
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.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.3",
42
- "@earendil-works/pi-agent-core": "0.80.10",
43
- "@earendil-works/pi-ai": "0.80.10",
44
- "@earendil-works/pi-coding-agent": "0.80.10",
45
- "@earendil-works/pi-tui": "0.80.10",
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: "Read-only codebase reconnaissance; returns concise findings with paths and evidence.",
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) ? frontmatter.thinkingLevel : undefined,
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 = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
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:config) on top of
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 = override.thinkingLevel === null ? undefined : override.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(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {
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;