@ferris1225/pi-subagents 0.6.0 → 0.8.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
@@ -1,174 +1,191 @@
1
- # pi-subagents
2
-
3
- [![npm version](https://img.shields.io/npm/v/@ferris1225/pi-subagents?color=blue)](https://www.npmjs.com/package/@ferris1225/pi-subagents)
4
- [![downloads](https://img.shields.io/npm/dm/@ferris1225/pi-subagents)](https://www.npmjs.com/package/@ferris1225/pi-subagents)
5
- [![license](https://img.shields.io/npm/l/@ferris1225/pi-subagents)](./LICENSE)
6
- ![platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
7
- ![pi](https://img.shields.io/badge/pi-extension-orange)
8
-
9
- English | [中文](./README-zh.md)
10
-
11
- A focused [pi](https://pi.dev) extension that gives the main model **sub-agents it will
12
- actually use**: `explore`, `worker`, and `reviewer` (plus an opt-in `plan`), each running
13
- in an isolated `pi` process. The differentiator is not the agents themselves — it is the
14
- **proactive dispatch injection** that makes the model delegate on its own, so you can
15
- delete the dispatch/review rules from your global `AGENTS.md`.
16
-
17
- ## Why pi-subagents?
18
-
19
- Pi ships no sub-agents on purpose. The community fills the gap two ways, and both miss:
20
-
21
- - **Too heavy** — frameworks with 9 agents, chain pipelines, worktree swarms, and a
22
- slash-command for everything. Powerful, but a lot of machinery to carry.
23
- - **Too quiet** a bare `subagent` tool that the model *rarely calls*, because pi only
24
- shows the parent model the tool, never the per-agent descriptions. So the agents sit
25
- idle unless you force them in a global prompt.
26
-
27
- `pi-subagents` takes the middle path:
28
-
29
- | Advantage | What it means for you |
30
- |-----------|----------------------|
31
- | **Actually gets used** | A `before_agent_start` hook injects the agent catalog + a dispatch/review directive into the system prompt every turn, reinforced by tool `promptGuidelines` and `Use PROACTIVELY when …` descriptions. This is the lever the heavy frameworks rely on too — we just make it the default. |
32
- | **Right-sized** | 3 focused agents (+1 opt-in), not 9. No chain/worktree/swarm machinery. Single and parallel modes only. |
33
- | **Replaces your AGENTS.md rules** | The injected directive is a self-contained replacement for the "Sub-agent Dispatch" and "Review, Verification & Commit" sections. Install it, then delete those sections. |
34
- | **True isolation** | Each agent is a separate `pi` process (`--no-session`), so delegated work never pollutes the main context. |
35
- | **Read-only where it matters** | `explore`, `plan`, and `reviewer` are read-only. The `reviewer` runs in a *separate* context to avoid self-confirmation bias. |
36
- | **Selection-only setup** | No typing of values: a checkbox module picker and a fuzzy-filter, paginated model picker. |
37
- | **Sensible model defaults** | Per-agent model override; if you skip one, it uses the **main session's current model**. Unavailable saved overrides are repaired and persisted automatically. |
38
- | **Leaf sub-agents** | Child processes never receive the `subagent` tool, so delegation cannot recurse or run away. |
39
- | **Zero runtime deps** | Pure pi extension, peer dependencies only, no build step. |
40
-
41
- ## Install
42
-
43
- ```bash
44
- pi install npm:@ferris1225/pi-subagents
45
- ```
46
-
47
- Requires pi **≥ 0.80.6** sub-agent thinking levels use the `--thinking` values
48
- introduced by that version.
49
-
50
- Then run the setup wizard (selection-only):
51
-
52
- ```text
53
- /subagents-setup
54
- ```
55
-
56
- ## Agents
57
-
58
- | Agent | Default | Tools | Role |
59
- |-------|:-------:|-------|------|
60
- | `explore` | ✅ | read-only | Fast codebase reconnaissance; returns compressed findings for handoff. |
61
- | `worker` | ✅ | all | Implements / fixes / refactors / tests a self-contained task. **Plans internally.** |
62
- | `reviewer` | ✅ | read-only | Adversarial pre-commit review in a separate context. |
63
- | `plan` | opt-in | read-only | A separate, human-reviewable implementation plan. A worker already plans internally, so this is only for when you want the plan as its own artifact. |
64
-
65
- Each agent is a Markdown file (`agents/*.md`: YAML frontmatter + body as system prompt).
66
- Override any of them by dropping a file with the same `name` into `~/.pi/agent/agents/`
67
- (user) or `.pi/agents/` (project).
68
-
69
- ## How proactive dispatch works
70
-
71
- Pi never shows the parent model the per-agent descriptions it only sees the `subagent`
72
- tool. Three levers fix that:
73
-
74
- 1. **`before_agent_start` injection** every turn, the enabled agents plus a
75
- dispatch/review directive are appended to the parent system prompt.
76
- 2. **Tool `promptSnippet` / `promptGuidelines`** reinforce "when to delegate" whenever
77
- the tool is active.
78
- 3. **`Use PROACTIVELY when …`** descriptions — the trigger phrasing proven across the
79
- Claude Code agent ecosystem.
80
-
81
- The directive encourages a clean flow: **`explore` → `worker` → `reviewer`**, parallel
82
- fan-out for independent tasks, and trust-but-verify handoffs. Because runs are backgrounded,
83
- start dependent steps only after the preceding result is delivered.
84
-
85
- ## Configuration
86
-
87
- Stored at `~/.pi/agent/pi-subagents.json` (honors `PI_CODING_AGENT_DIR`):
88
-
89
- ```json
90
- {
91
- "enabledAgents": ["explore", "worker", "reviewer"],
92
- "agentModels": { "explore": "anthropic/claude-haiku-4-5" },
93
- "thinkingLevel": "max",
94
- "proactiveInjection": true,
95
- "agentScope": "user"
96
- }
97
- ```
98
-
99
- - `enabledAgents` which agents are discoverable and injected.
100
- - `agentModels` — per-agent model override (`"provider/model-id"`). If a saved model is unavailable, it is replaced with the current main-window model and written back to this file.
101
- - `thinkingLevel` — sub-agent reasoning strength: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` (default).
102
- - `proactiveInjection` — toggle the system-prompt injection.
103
- - `agentScope` `"user"` (default), `"project"`, or `"both"`.
104
-
105
- **Model precedence** for each agent:
106
-
107
- ```
108
- available agentModels[name] → current session model → the agent's frontmatter default
109
- ```
110
-
111
- If a configured model is no longer available, it is switched to the current main-window model and persisted before the next run.
112
-
113
- ## Usage
114
-
115
- The main model calls `subagent` on its own, but you can also ask directly:
116
-
117
- ```text
118
- # single
119
- Use the explore sub-agent to map how authentication is wired up.
120
-
121
- # parallel (independent tasks)
122
- Run these in parallel sub-agents: explore the API layer, and explore the DB layer.
123
- ```
124
-
125
- Tool shape:
126
-
127
- ```jsonc
128
- // single
129
- { "agent": "worker", "task": "<self-contained brief>" }
130
- // parallel
131
- { "tasks": [ { "agent": "explore", "task": "..." }, { "agent": "explore", "task": "..." } ] }
132
- ```
133
-
134
- Every run starts in the background. The tool immediately ends the current main-agent turn,
135
- so the editor is ready for another request without pressing Escape. The completed output is
136
- shown and added to the context before a later user prompt. Escape only interrupts foreground
137
- work after launch; session switch, reload, or exit cancels remaining background processes.
138
-
139
- ## Live status & notifications
140
-
141
- While sub-agents run, a widget above the editor shows one line per run — status
142
- icon, agent, model, token usage, elapsed time plus a second, indented line
143
- with what the agent is doing right now: `thinking`, `responding`,
144
- `read src/index.ts`, `bash npm test`, (never a raw JSON args blob).
145
- `responding` means the model is streaming normal text, **not** writing to the filesystem.
146
-
147
- When a run finishes (done **or** failed), its row disappears from the widget and
148
- the main window gets a notification with the final summary
149
- (`✓ worker · openai/gpt-5 · ↑12.4k ↓3.1k · 47s`). Its completed result message
150
- is the durable record in the conversation and context for a later request.
151
-
152
- Sub-agents use the configured thinking level (default `--thinking max`);
153
- pi clamps it adaptively to what the resolved model supports
154
- (`max → xhigh → high → … → off`), so weaker models degrade gracefully.
155
- The task is sent through stdin; only the agent system prompt uses a short-lived
156
- file. Child output is streamed in memory. Runs have no default time limit;
157
- explicit cancellation cleans up the process tree.
158
-
159
- ## Development
160
-
161
- ```bash
162
- npm install
163
- npm run check # tsc --noEmit
164
- npm test # vitest
165
- ```
166
-
167
- ## See also
168
-
169
- - [pi-querit-search](https://www.npmjs.com/package/pi-querit-search) live web search &
170
- page fetching for pi, by the same author.
171
-
172
- ## License
173
-
174
- MIT
1
+ # pi-subagents
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@ferris1225/pi-subagents?color=blue)](https://www.npmjs.com/package/@ferris1225/pi-subagents)
4
+ [![downloads](https://img.shields.io/npm/dm/@ferris1225/pi-subagents)](https://www.npmjs.com/package/@ferris1225/pi-subagents)
5
+ [![license](https://img.shields.io/npm/l/@ferris1225/pi-subagents)](./LICENSE)
6
+ ![platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
7
+ ![pi](https://img.shields.io/badge/pi-extension-orange)
8
+
9
+ Focused background delegation for [pi](https://pi.dev). `pi-subagents` adds a small set of
10
+ specialized agents that run in isolated child processes, report results back to the main
11
+ agent, and keep the workflow moving without manual polling.
12
+
13
+ ## Highlights
14
+
15
+ - **Automatic delegation guidance** — injects the enabled agent catalog and routing rules into
16
+ the main agent's system prompt.
17
+ - **Isolated execution** — every sub-agent runs in its own `pi` process with `--no-session`.
18
+ - **Automatic continuation** — a completed result is sent to the main session as a custom
19
+ message and automatically starts a follow-up turn. If the main agent is busy, the result
20
+ waits in the follow-up queue.
21
+ - **Parallel fan-out** — run independent tasks together, with a bounded background queue.
22
+ - **Live progress** a TUI widget shows each agent's status, activity, model, usage, and
23
+ elapsed time; completion also produces a concise notification.
24
+ - **Per-agent configuration** enable agents, select models, set thinking strength, tune
25
+ concurrency limits, and choose discovery scope from `/subagents-setup`.
26
+ - **Leaf processes** — child agents cannot access the `subagent` tool, so delegation cannot
27
+ recurse.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pi install npm:@ferris1225/pi-subagents
33
+ ```
34
+
35
+ Requires pi **>= 0.80.6**.
36
+
37
+ After installation, open the setup wizard in an interactive TUI session:
38
+
39
+ ```text
40
+ /subagents-setup
41
+ ```
42
+
43
+ The default configuration enables `explore`, `worker`, and `reviewer`.
44
+
45
+ ## Included agents
46
+
47
+ | Agent | Default | Access | Purpose |
48
+ | --- | :---: | --- | --- |
49
+ | `explore` | Yes | Read-only | Fast codebase reconnaissance and structured findings. |
50
+ | `worker` | Yes | Full | Implements, fixes, refactors, and tests a self-contained task. |
51
+ | `reviewer` | Yes | Read-only | Independent adversarial review of a diff before completion. |
52
+
53
+ Agents are Markdown files in `agents/`. Each file contains YAML frontmatter and a system
54
+ prompt. User and project scopes can override a built-in agent with the same name.
55
+
56
+ ## Workflow
57
+
58
+ A typical flow is:
59
+
60
+ ```text
61
+ main agent
62
+
63
+ ├─ subagent(explore / worker / reviewer)
64
+ │ └─ isolated pi child process
65
+ │ └─ result message
66
+
67
+ └─ automatic follow-up turn with the result
68
+ ```
69
+
70
+ 1. The main agent calls `subagent` with a self-contained brief.
71
+ 2. The tool returns immediately and ends that foreground tool turn, leaving the editor ready
72
+ for input.
73
+ 3. The child process works independently. By default up to four queued runs execute at
74
+ once and a single parallel request may contain up to eight tasks; both limits are
75
+ configurable (`maxConcurrency` / `maxParallelTasks`).
76
+ 4. On completion or failure, the extension sends a durable result message to the main
77
+ session. That message automatically wakes the main agent, or waits until its current turn
78
+ finishes.
79
+ 5. The main agent uses the result to verify the work and continue dependent steps. No later
80
+ user prompt is required to collect a result.
81
+
82
+ Switching sessions, reloading, or shutting down cancels remaining background runs.
83
+
84
+ ## Usage
85
+
86
+ The main agent is encouraged to delegate automatically, but you can also ask directly:
87
+
88
+ ```text
89
+ Use explore to map how authentication is wired up.
90
+ Ask worker to implement the API change after the exploration is complete.
91
+ Run reviewer on the final diff before reporting completion.
92
+ ```
93
+
94
+ ### Single task
95
+
96
+ ```json
97
+ {
98
+ "agent": "worker",
99
+ "task": "Implement the requested change. Inspect the existing conventions, update tests, and report the files changed and checks run."
100
+ }
101
+ ```
102
+
103
+ Optional `cwd` selects the working directory for that child.
104
+
105
+ ### Parallel tasks
106
+
107
+ Use parallel mode only for independent work:
108
+
109
+ ```json
110
+ {
111
+ "tasks": [
112
+ { "agent": "explore", "task": "Map the API layer and its tests." },
113
+ { "agent": "explore", "task": "Map the database layer and its tests." }
114
+ ]
115
+ }
116
+ ```
117
+
118
+ Start dependent work after the relevant result has been delivered to the main agent.
119
+
120
+ ## Configuration
121
+
122
+ Configuration is stored at `~/.pi/agent/pi-subagents.json`. The location follows
123
+ `PI_CODING_AGENT_DIR` when set.
124
+
125
+ ```json
126
+ {
127
+ "enabledAgents": ["explore", "worker", "reviewer"],
128
+ "agentModels": {
129
+ "explore": "anthropic/claude-haiku-4-5"
130
+ },
131
+ "thinkingLevel": "max",
132
+ "proactiveInjection": true,
133
+ "agentScope": "user",
134
+ "maxConcurrency": 4,
135
+ "maxParallelTasks": 8,
136
+ "maxSubagentDepth": 1
137
+ }
138
+ ```
139
+
140
+ | Field | Description |
141
+ | --- | --- |
142
+ | `enabledAgents` | Agent names exposed to discovery and prompt injection. An empty array disables all agents. |
143
+ | `agentModels` | Optional `provider/model-id` override per agent. |
144
+ | `thinkingLevel` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. |
145
+ | `proactiveInjection` | Whether to add the delegation directive to the main system prompt. |
146
+ | `agentScope` | `user`, `project`, or `both`; controls which user/project agent directories are discovered. |
147
+ | `maxConcurrency` | How many sub-agent processes run at once (1–16, default 4). Extra work waits in the queue. |
148
+ | `maxParallelTasks` | Maximum tasks accepted by one parallel `subagent` call (1–32, default 8). |
149
+ | `maxSubagentDepth` | Depth at which the `subagent` tool is no longer registered (default 1: the main session delegates, children are leaf processes). `0` disables the tool entirely. Read once at extension load. |
150
+
151
+ ### Configuration migration
152
+
153
+ The config file migrates itself on load no manual steps after an upgrade:
154
+
155
+ - **Schema upgrades** a config written by an older version (missing newer keys or
156
+ holding invalid values) is normalized and saved back with the new fields filled in.
157
+ - **Removed agents** agents no longer shipped (e.g. the old `plan` agent) are stripped
158
+ from `enabledAgents` and `agentModels` automatically.
159
+
160
+ Model selection uses this precedence:
161
+
162
+ ```text
163
+ configured agent model current main-session model → agent frontmatter model
164
+ ```
165
+
166
+ Unavailable configured models are replaced with a usable current-session model when possible
167
+ and the repaired configuration is saved.
168
+
169
+ ## Agent discovery and overrides
170
+
171
+ - Built-in agents are shipped with the package.
172
+ - User agents live in `~/.pi/agent/agents/`.
173
+ - Project agents live in the nearest `.pi/agents/` directory.
174
+ - For duplicate names, project overrides user and user overrides built-in.
175
+
176
+ Use a matching Markdown filename and `name` field to replace a built-in agent. Keep the task
177
+ brief explicit: include the goal, relevant paths, constraints, and expected handoff.
178
+
179
+ ## Development
180
+
181
+ ```bash
182
+ npm install
183
+ npm run check
184
+ npm test
185
+ ```
186
+
187
+ The package has no runtime dependencies beyond pi peer dependencies.
188
+
189
+ ## License
190
+
191
+ MIT
package/agents/explore.md CHANGED
@@ -26,7 +26,7 @@ You are an explore agent: a fast, read-only reconnaissance specialist. You inves
26
26
  - Thorough: trace dependencies across modules; check tests and types.
27
27
 
28
28
  ## Collaboration
29
- - Your output feeds `plan` or `worker`. Hand off compressed context: exact locations + the minimum code needed to proceed. Flag anything ambiguous so the caller can decide.
29
+ - Your output feeds `worker` (or the main agent directly). Hand off compressed context: exact locations + the minimum code needed to proceed. Flag anything ambiguous so the caller can decide.
30
30
 
31
31
  ## Output format
32
32
  ## Files Retrieved
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ferris1225/pi-subagents",
3
- "version": "0.6.0",
4
- "description": "Focused sub-agent delegation for pi: explore / plan / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
3
+ "version": "0.8.0",
4
+ "description": "Focused sub-agent delegation for pi: explore / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "publishConfig": {
@@ -18,7 +18,6 @@
18
18
  "src",
19
19
  "agents",
20
20
  "README.md",
21
- "README-zh.md",
22
21
  "LICENSE"
23
22
  ],
24
23
  "pi": {
package/src/background.ts CHANGED
@@ -14,7 +14,7 @@ interface PendingTask {
14
14
  }
15
15
 
16
16
  export class BackgroundTaskQueue {
17
- private readonly concurrency: number;
17
+ private concurrency: number;
18
18
  private readonly pending: PendingTask[] = [];
19
19
  private readonly active = new Set<AbortController>();
20
20
  private stopped = false;
@@ -23,6 +23,16 @@ export class BackgroundTaskQueue {
23
23
  this.concurrency = Math.max(1, concurrency);
24
24
  }
25
25
 
26
+ /**
27
+ * Update the concurrency limit (e.g. after a config change). Raising it
28
+ * immediately starts more queued work; lowering it takes effect as running
29
+ * tasks finish — already-running tasks are never interrupted.
30
+ */
31
+ setConcurrency(concurrency: number): void {
32
+ this.concurrency = Math.max(1, concurrency);
33
+ this.drain();
34
+ }
35
+
26
36
  enqueue(task: BackgroundTask, onCancelled?: () => void): AbortController {
27
37
  const controller = new AbortController();
28
38
  if (this.stopped) {
package/src/config.ts CHANGED
@@ -5,19 +5,31 @@
5
5
  * and honors PI_CODING_AGENT_DIR). Parsing is defensive: invalid fields fall back
6
6
  * to defaults instead of throwing, so a hand-edited or partially-written file can
7
7
  * never break the extension at runtime.
8
+ *
9
+ * Schema upgrades happen transparently on load: a config written by an older
10
+ * version (missing newer keys, holding removed agents, or containing invalid
11
+ * values) is normalized and persisted back with the new fields filled in.
8
12
  */
9
13
 
10
14
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
15
+ import { readFileSync } from "node:fs";
11
16
  import { dirname, join } from "node:path";
12
17
  import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
13
18
 
14
19
  /** Full catalog of agents shipped with the package (selectable in /subagents-setup). */
15
- export const BUILTIN_AGENT_NAMES = ["explore", "plan", "worker", "reviewer"] as const;
20
+ export const BUILTIN_AGENT_NAMES = ["explore", "worker", "reviewer"] as const;
16
21
  export type BuiltinAgentName = (typeof BUILTIN_AGENT_NAMES)[number];
17
22
 
18
- /** Agents enabled out of the box. `plan` ships but is opt-in: a worker plans internally. */
23
+ /** Agents enabled out of the box. */
19
24
  export const DEFAULT_ENABLED_AGENTS: readonly string[] = ["explore", "worker", "reviewer"];
20
25
 
26
+ /**
27
+ * Agents that used to ship but were removed. normalizeConfig strips them from
28
+ * enabledAgents/agentModels so upgraded installs clean their config automatically
29
+ * (the schema-upgrade save in loadConfig then persists the cleanup).
30
+ */
31
+ export const REMOVED_AGENT_NAMES: readonly string[] = ["plan"];
32
+
21
33
  export const AGENT_SCOPE_VALUES = ["user", "project", "both"] as const;
22
34
  export type AgentScope = (typeof AGENT_SCOPE_VALUES)[number];
23
35
 
@@ -28,6 +40,22 @@ export const DEFAULT_THINKING_LEVEL: ThinkingLevel = "max";
28
40
 
29
41
  export const CONFIG_FILE_NAME = "pi-subagents.json";
30
42
 
43
+ /** How many sub-agent processes may run at once. Default: 4. */
44
+ export const DEFAULT_MAX_CONCURRENCY = 4;
45
+ /** Upper bound accepted for maxConcurrency (defensive clamp). */
46
+ export const MAX_CONCURRENCY_LIMIT = 16;
47
+ /** How many tasks a single parallel `subagent` call may contain. Default: 8. */
48
+ export const DEFAULT_MAX_PARALLEL_TASKS = 8;
49
+ /** Upper bound accepted for maxParallelTasks (defensive clamp). */
50
+ export const MAX_PARALLEL_TASKS_LIMIT = 32;
51
+ /**
52
+ * Depth at which the subagent tool stops being available. 1 = the main session
53
+ * delegates and child processes are leaves; 0 disables the tool entirely.
54
+ */
55
+ export const DEFAULT_MAX_SUBAGENT_DEPTH = 1;
56
+ /** Upper bound accepted for maxSubagentDepth (defensive clamp). */
57
+ export const MAX_SUBAGENT_DEPTH_LIMIT = 4;
58
+
31
59
  export interface SubagentsConfig {
32
60
  /** Agent names that are discoverable and injected. Default: explore, worker, reviewer. */
33
61
  enabledAgents: string[];
@@ -39,6 +67,12 @@ export interface SubagentsConfig {
39
67
  proactiveInjection: boolean;
40
68
  /** Which agent directories to discover from. Default: "user". */
41
69
  agentScope: AgentScope;
70
+ /** Max sub-agent processes running at once (extra work queues). Default: 4. */
71
+ maxConcurrency: number;
72
+ /** Max tasks accepted by one parallel `subagent` call. Default: 8. */
73
+ maxParallelTasks: number;
74
+ /** Depth at which the subagent tool is no longer registered. Default: 1. */
75
+ maxSubagentDepth: number;
42
76
  }
43
77
 
44
78
  export const DEFAULT_CONFIG: SubagentsConfig = {
@@ -47,6 +81,9 @@ export const DEFAULT_CONFIG: SubagentsConfig = {
47
81
  thinkingLevel: DEFAULT_THINKING_LEVEL,
48
82
  proactiveInjection: true,
49
83
  agentScope: "user",
84
+ maxConcurrency: DEFAULT_MAX_CONCURRENCY,
85
+ maxParallelTasks: DEFAULT_MAX_PARALLEL_TASKS,
86
+ maxSubagentDepth: DEFAULT_MAX_SUBAGENT_DEPTH,
50
87
  };
51
88
 
52
89
  export function getConfigPath(agentDir: string = getAgentDir()): string {
@@ -68,6 +105,12 @@ function isModelReference(value: unknown): value is string {
68
105
  return slash > 0 && slash < normalized.length - 1 && !/\s/u.test(normalized);
69
106
  }
70
107
 
108
+ /** Clamp a raw value to a positive integer within [1, upper]; undefined when invalid. */
109
+ function clampCount(value: unknown, upper: number): number | undefined {
110
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
111
+ return Math.max(1, Math.min(upper, Math.round(value)));
112
+ }
113
+
71
114
  /**
72
115
  * Merge a raw parsed JSON value over the defaults, dropping invalid fields.
73
116
  * Exported for tests.
@@ -81,6 +124,9 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
81
124
  thinkingLevel: DEFAULT_CONFIG.thinkingLevel,
82
125
  proactiveInjection: DEFAULT_CONFIG.proactiveInjection,
83
126
  agentScope: DEFAULT_CONFIG.agentScope,
127
+ maxConcurrency: DEFAULT_CONFIG.maxConcurrency,
128
+ maxParallelTasks: DEFAULT_CONFIG.maxParallelTasks,
129
+ maxSubagentDepth: DEFAULT_CONFIG.maxSubagentDepth,
84
130
  };
85
131
 
86
132
  if (Array.isArray(raw.enabledAgents)) {
@@ -88,11 +134,15 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
88
134
  (name): name is string => typeof name === "string" && name.trim().length > 0,
89
135
  );
90
136
  // An explicitly empty array is honored (disables all agents); otherwise keep valid names.
91
- config.enabledAgents = [...new Set(names.map((name) => name.trim()))];
137
+ // Agents removed from the package (e.g. plan) are stripped from upgraded configs.
138
+ config.enabledAgents = [...new Set(names.map((name) => name.trim()))].filter(
139
+ (name) => !REMOVED_AGENT_NAMES.includes(name),
140
+ );
92
141
  }
93
142
 
94
143
  if (isRecord(raw.agentModels)) {
95
144
  for (const [key, value] of Object.entries(raw.agentModels)) {
145
+ if (REMOVED_AGENT_NAMES.includes(key.trim())) continue;
96
146
  if (isModelReference(value)) config.agentModels[key.trim()] = value.trim();
97
147
  }
98
148
  }
@@ -109,33 +159,71 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
109
159
  config.agentScope = raw.agentScope;
110
160
  }
111
161
 
162
+ const maxConcurrency = clampCount(raw.maxConcurrency, MAX_CONCURRENCY_LIMIT);
163
+ if (maxConcurrency !== undefined) config.maxConcurrency = maxConcurrency;
164
+
165
+ const maxParallelTasks = clampCount(raw.maxParallelTasks, MAX_PARALLEL_TASKS_LIMIT);
166
+ if (maxParallelTasks !== undefined) config.maxParallelTasks = maxParallelTasks;
167
+
168
+ // 0 is meaningful here (disables the tool), so clamp to [0, limit] instead.
169
+ if (typeof raw.maxSubagentDepth === "number" && Number.isFinite(raw.maxSubagentDepth)) {
170
+ config.maxSubagentDepth = Math.max(0, Math.min(MAX_SUBAGENT_DEPTH_LIMIT, Math.round(raw.maxSubagentDepth)));
171
+ }
172
+
112
173
  return config;
113
174
  }
114
175
 
176
+ function defaultConfig(): SubagentsConfig {
177
+ return { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_CONFIG.enabledAgents], agentModels: {} };
178
+ }
179
+
115
180
  /**
116
181
  * Load config. A missing file is a normal state and yields the defaults (not an error).
117
182
  * A corrupt file also falls back to defaults rather than throwing, so startup never breaks.
183
+ * A file from an older version (missing newer keys or holding removed agents) is
184
+ * normalized and persisted back, so the on-disk config stays current.
118
185
  */
119
186
  export async function loadConfig(configPath: string = getConfigPath()): Promise<SubagentsConfig> {
120
187
  let text: string;
121
188
  try {
122
189
  text = await readFile(configPath, "utf8");
123
- } catch (error) {
124
- if (isNodeError(error) && error.code === "ENOENT") {
125
- return { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_CONFIG.enabledAgents] };
126
- }
127
- // Unreadable for another reason: fall back to defaults but do not crash startup.
128
- return { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_CONFIG.enabledAgents] };
190
+ } catch {
191
+ // Missing or unreadable: fall back to defaults but do not crash startup.
192
+ return defaultConfig();
129
193
  }
130
194
 
131
195
  let parsed: unknown;
132
196
  try {
133
197
  parsed = JSON.parse(text);
134
198
  } catch {
135
- return { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_CONFIG.enabledAgents] };
199
+ return defaultConfig();
200
+ }
201
+
202
+ const config = normalizeConfig(parsed);
203
+
204
+ // Schema upgrade: persist the normalized shape when the file gained fields
205
+ // (new version) or dropped invalid/removed ones.
206
+ if (JSON.stringify(config) !== JSON.stringify(parsed)) {
207
+ try {
208
+ await saveConfig(config, configPath);
209
+ } catch {
210
+ // Non-fatal: keep the in-memory config for this run.
211
+ }
136
212
  }
137
213
 
138
- return normalizeConfig(parsed);
214
+ return config;
215
+ }
216
+
217
+ /**
218
+ * Synchronous load for the extension's init-time decisions (e.g. the recursion
219
+ * guard). Runs before any async context is available; never migrates or saves.
220
+ */
221
+ export function loadConfigSync(configPath: string = getConfigPath()): SubagentsConfig {
222
+ try {
223
+ return normalizeConfig(JSON.parse(readFileSync(configPath, "utf8")));
224
+ } catch {
225
+ return defaultConfig();
226
+ }
139
227
  }
140
228
 
141
229
  /**
@@ -159,10 +247,6 @@ export async function saveConfig(
159
247
  });
160
248
  }
161
249
 
162
- function isNodeError(error: unknown): error is NodeJS.ErrnoException {
163
- return error instanceof Error;
164
- }
165
-
166
250
  export function errorMessage(error: unknown): string {
167
251
  return error instanceof Error ? error.message : String(error);
168
252
  }
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * pi-subagents — focused sub-agent delegation for pi.
3
3
  *
4
4
  * Registers:
5
- * - a `subagent` tool that runs explore/plan/worker/reviewer agents as isolated
5
+ * - a `subagent` tool that runs explore/worker/reviewer agents as isolated
6
6
  * `pi` child processes (single or parallel),
7
7
  * - a `/subagents-setup` command for selection-only configuration,
8
8
  * - a `before_agent_start` hook that injects a delegation directive into the
@@ -18,14 +18,11 @@ import { Text, truncateToWidth } from "@earendil-works/pi-tui";
18
18
  import { Type } from "typebox";
19
19
  import { discoverAgents, type AgentConfig } from "./agents.ts";
20
20
  import { BackgroundTaskQueue } from "./background.ts";
21
- import { getConfigPath, loadConfig, saveConfig } from "./config.ts";
21
+ import { getConfigPath, loadConfig, loadConfigSync, saveConfig } from "./config.ts";
22
22
  import { repairUnavailableModelOverrides } from "./models.ts";
23
23
  import { buildDelegationDirective } from "./prompt.ts";
24
24
  import { runSetup } from "./setup.ts";
25
25
  import {
26
- MAX_CONCURRENCY,
27
- MAX_PARALLEL_TASKS,
28
- MAX_SUBAGENT_DEPTH,
29
26
  currentSubagentDepth,
30
27
  getFinalOutput,
31
28
  getResultOutput,
@@ -112,15 +109,23 @@ function formatUsage(usage: UsageStats): string {
112
109
 
113
110
  export default function (pi: ExtensionAPI): void {
114
111
  const configPath = getConfigPath(getAgentDir());
115
- const backgroundQueue = new BackgroundTaskQueue(MAX_CONCURRENCY);
112
+ // Init-time decisions need the config synchronously; the full (migrating)
113
+ // async load runs per tool call.
114
+ const initialConfig = loadConfigSync(configPath);
115
+ const backgroundQueue = new BackgroundTaskQueue(initialConfig.maxConcurrency);
116
116
  let sessionActive = true;
117
117
 
118
- // Recursion guard: child sub-agents are leaf processes and cannot delegate again.
119
- if (currentSubagentDepth() >= MAX_SUBAGENT_DEPTH) {
118
+ // Recursion guard: sub-agents at the configured depth are leaf processes and
119
+ // cannot delegate again. maxSubagentDepth 0 disables the tool entirely.
120
+ if (currentSubagentDepth() >= initialConfig.maxSubagentDepth) {
121
+ const reason =
122
+ initialConfig.maxSubagentDepth === 0
123
+ ? "disabled by maxSubagentDepth 0 in pi-subagents.json"
124
+ : "disabled in nested sub-agent processes";
120
125
  pi.registerCommand("subagents-setup", {
121
- description: "Configure pi-subagents (disabled in nested sub-agent processes)",
126
+ description: `Configure pi-subagents (${reason})`,
122
127
  handler: async (_args, ctx) => {
123
- ctx.ui.notify("pi-subagents setup is unavailable inside a nested sub-agent.", "warning");
128
+ ctx.ui.notify(`pi-subagents setup is unavailable here (${reason}).`, "warning");
124
129
  },
125
130
  });
126
131
  return;
@@ -144,26 +149,28 @@ export default function (pi: ExtensionAPI): void {
144
149
  label: "Subagent",
145
150
  description: [
146
151
  "Delegate a discrete, self-contained task to a specialized sub-agent running in an ISOLATED context window.",
147
- "Agents: explore (read-only codebase recon), plan (implementation plan, opt-in), worker (implement/fix/refactor/test, full tools), reviewer (adversarial pre-commit review, read-only).",
152
+ "Agents: explore (read-only codebase recon), worker (implement/fix/refactor/test, full tools), reviewer (adversarial pre-commit review, read-only).",
148
153
  "Modes: single ({agent, task}) or parallel ({tasks: [{agent, task}, ...]}).",
149
- "It starts agents in the background and immediately returns control to the main window; completed results arrive in a later user prompt.",
154
+ "It starts agents in the background and immediately returns control to the main window; completion messages automatically wake the main agent to continue.",
150
155
  "Each agent has no memory of this conversation — brief it fully (goal, exact paths, constraints, expected output)."
151
156
  ].join(" "),
152
157
  promptSnippet:
153
- "Start background subagents: explore (read-only search), worker (implement), reviewer (adversarial review); completed results arrive in a later prompt.",
158
+ "Start background subagents: explore (read-only search), worker (implement), reviewer (adversarial review); completion automatically resumes the main agent.",
154
159
  promptGuidelines: [
155
160
  "Use subagent to delegate discrete, self-contained tasks so the main context stays clean; do orchestration and verification yourself.",
156
161
  "Use subagent with agent 'explore' for broad or open-ended code search before large changes.",
157
162
  "Use subagent with agent 'worker' to implement a well-scoped task; it plans internally.",
158
163
  "Use subagent with agent 'reviewer' for a fresh read-only review before reporting work done or committing.",
159
- "subagent launches work in the background and ends the current turn; do not assume a result is available until a later user prompt.",
160
- "Run independent tasks in parallel by passing a tasks array to subagent; start dependent work only after its result arrives.",
164
+ "subagent launches work in the background and ends the current turn; when a result arrives, the main agent is automatically resumed with it.",
165
+ "Run independent tasks in parallel by passing a tasks array to subagent; let the automatically resumed main agent start dependent work after results arrive.",
161
166
  ],
162
167
  parameters: SubagentParams,
163
168
 
164
169
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
165
170
  monitor.beginTurn();
166
171
  let config = await loadConfig(configPath);
172
+ // Pick up concurrency changes from /subagents-setup without a restart.
173
+ backgroundQueue.setConcurrency(config.maxConcurrency);
167
174
  const repairedModels = repairUnavailableModelOverrides(ctx, config.agentModels);
168
175
  if (repairedModels.changed) {
169
176
  config = { ...config, agentModels: repairedModels.agentModels };
@@ -183,8 +190,8 @@ export default function (pi: ExtensionAPI): void {
183
190
  }
184
191
  }
185
192
 
186
- // Finished runs leave the widget immediately. Their final findings arrive
187
- // as a custom message before the next foreground prompt.
193
+ // Finished runs leave the widget immediately. Their final findings are sent
194
+ // back as a custom message that automatically starts a follow-up turn.
188
195
  const finishRun = (runId: number, status: "done" | "failed"): void => {
189
196
  monitor.setStatus(runId, status); // stamps endedAt for the elapsed time
190
197
  const run = monitor.removeRun(runId);
@@ -297,7 +304,9 @@ export default function (pi: ExtensionAPI): void {
297
304
  content: `### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}\n\n${getResultOutput(result)}`,
298
305
  display: true,
299
306
  },
300
- { deliverAs: "nextTurn" },
307
+ // The result is both durable context and a wake-up signal. If the
308
+ // main agent is busy, followUp queues it until the current turn ends.
309
+ { deliverAs: "followUp", triggerTurn: true },
301
310
  );
302
311
  },
303
312
  () => finishRun(runId, "failed"),
@@ -307,16 +316,19 @@ export default function (pi: ExtensionAPI): void {
307
316
  };
308
317
 
309
318
  // Sub-agents intentionally detach from the foreground turn. This makes the
310
- // editor available immediately; completed findings arrive before the next prompt.
319
+ // editor available immediately; completion messages later wake the main agent.
311
320
  if (params.tasks && params.tasks.length > 0) {
312
- if (params.tasks.length > MAX_PARALLEL_TASKS) {
313
- return {
314
- content: [
315
- { type: "text", text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.` },
316
- ],
317
- details: makeDetails("parallel", true)([]),
318
- };
319
- }
321
+ if (params.tasks.length > config.maxParallelTasks) {
322
+ return {
323
+ content: [
324
+ {
325
+ type: "text",
326
+ text: `Too many parallel tasks (${params.tasks.length}). Max is ${config.maxParallelTasks} (configurable via /subagents-setup).`,
327
+ },
328
+ ],
329
+ details: makeDetails("parallel", true)([]),
330
+ };
331
+ }
320
332
 
321
333
  const results = params.tasks.map((task) => startBackground(task.agent, task.task, task.cwd));
322
334
  const started = results.filter((result) => result.exitCode === -1).length;
@@ -327,7 +339,7 @@ export default function (pi: ExtensionAPI): void {
327
339
  type: "text",
328
340
  text:
329
341
  started > 0
330
- ? `Started ${started} background subagent${started === 1 ? "" : "s"}. Completed results will be added before a later user prompt.`
342
+ ? `Started ${started} background subagent${started === 1 ? "" : "s"}. Results will automatically resume the main agent when ready.`
331
343
  : failures.map((result) => getResultOutput(result)).join("\n"),
332
344
  },
333
345
  ],
@@ -346,7 +358,7 @@ export default function (pi: ExtensionAPI): void {
346
358
  };
347
359
  }
348
360
  return {
349
- content: [{ type: "text", text: `Started ${result.agent} in the background. Its completed result will be added before a later user prompt.` }],
361
+ content: [{ type: "text", text: `Started ${result.agent} in the background. Its result will automatically resume the main agent when ready.` }],
350
362
  details: makeDetails("single", true)([result]),
351
363
  terminate: true,
352
364
  };
package/src/prompt.ts CHANGED
@@ -17,7 +17,6 @@ import { formatCatalogEntry } from "./agents.ts";
17
17
  /** Compact role routing hints, emitted only for roles that are enabled. */
18
18
  const ROLE_ROUTING: Record<string, string> = {
19
19
  explore: "explore — broad/open-ended code search, \"where is X\", multi-file lookups (read-only, cheap).",
20
- plan: "plan — a separate, human-reviewable implementation plan before any code (read-only).",
21
20
  worker: "worker — implement/fix/refactor/test a well-scoped task (full tools; plans internally).",
22
21
  reviewer: "reviewer — adversarial pre-commit review of a diff (read-only; independent context).",
23
22
  };
@@ -38,18 +37,18 @@ export function buildDelegationDirective(agents: AgentConfig[]): string {
38
37
  ## Sub-agent delegation (pi-subagents)
39
38
 
40
39
  You have a \`subagent\` tool that starts specialized agents in ISOLATED background processes.
41
- It immediately ends the current main-agent turn so the user can keep working. Completed
42
- findings are added before a later user prompt; do not assume a launched result is available
43
- in the same turn.
40
+ It immediately ends the current main-agent turn so the user can keep working. When a child
41
+ finishes, its result is sent back as a message that automatically resumes the main agent;
42
+ if the main agent is busy, the result waits as a follow-up.
44
43
 
45
44
  Available agents:
46
45
  ${catalog}
47
46
 
48
47
  ${routing ? `Routing:\n${routing}\n` : ""}Dispatch discipline:
49
- - Default to delegating every discrete task to a sub-agent; use completed findings in a later user turn for orchestration and verification.
48
+ - Default to delegating every discrete task to a sub-agent; use the automatically delivered findings for orchestration and verification.
50
49
  - Only handle inline: pure Q&A, a single trivial edit/lookup, or when the user explicitly says to do it directly. When in doubt, delegate.
51
50
  - For an already-known or trivial target, use a direct search/read tool (e.g. grep/find/read) — do not over-delegate a one-line lookup.
52
- ${hasMultiple ? "- Run INDEPENDENT tasks in parallel: one subagent call with a `tasks` array, and track them with your todo list. Launch dependent work only after its prerequisite result arrives (e.g. explore, then worker, then reviewer).\n" : ""}- Brief each sub-agent as self-contained: goal, exact paths, constraints, expected output. It has NO memory of this conversation.
51
+ ${hasMultiple ? "- Run INDEPENDENT tasks in parallel: one subagent call with a `tasks` array, and track them with your todo list. Let the automatically resumed main agent launch dependent work only after its prerequisite result arrives (e.g. explore, then worker, then reviewer).\n" : ""}- Brief each sub-agent as self-contained: goal, exact paths, constraints, expected output. It has NO memory of this conversation.
53
52
  - Treat delegated agents as leaf workers: do not ask a sub-agent to dispatch another sub-agent; child processes do not have this tool.
54
53
  - Trust but verify: a sub-agent's summary describes intent, not outcome. Check the actual changes/results before reporting work done.
55
54
 
package/src/setup.ts CHANGED
@@ -14,6 +14,8 @@ import {
14
14
  BUILTIN_AGENT_NAMES,
15
15
  DEFAULT_CONFIG,
16
16
  DEFAULT_ENABLED_AGENTS,
17
+ DEFAULT_MAX_CONCURRENCY,
18
+ DEFAULT_MAX_PARALLEL_TASKS,
17
19
  THINKING_LEVEL_VALUES,
18
20
  type AgentScope,
19
21
  type SubagentsConfig,
@@ -31,7 +33,6 @@ const INHERIT = "__inherit__";
31
33
  /** Short, selection-friendly descriptions for the built-in agents. */
32
34
  const MODULE_HINTS: Record<string, string> = {
33
35
  explore: "read-only codebase recon (fast model)",
34
- plan: "implementation plan before code (opt-in)",
35
36
  worker: "implement / fix / refactor / test (full tools)",
36
37
  reviewer: "adversarial pre-commit review (read-only)",
37
38
  };
@@ -129,6 +130,29 @@ async function pickInjection(ctx: ExtensionCommandContext, current: boolean): Pr
129
130
  return choice.startsWith("On");
130
131
  }
131
132
 
133
+ /** Preset steps offered for the two numeric limits (selection-only wizard). */
134
+ const CONCURRENCY_STEPS = [1, 2, 3, 4, 6, 8, 12, 16];
135
+ const PARALLEL_TASK_STEPS = [2, 4, 6, 8, 12, 16, 24, 32];
136
+
137
+ async function pickCount(
138
+ ctx: ExtensionCommandContext,
139
+ title: string,
140
+ steps: readonly number[],
141
+ current: number,
142
+ defaultValue: number,
143
+ ): Promise<number | undefined> {
144
+ const values = [...new Set([...steps, current])].sort((a, b) => a - b);
145
+ const options = values.map((value) => {
146
+ const tags = [value === current ? "current" : "", value === defaultValue ? "default" : ""]
147
+ .filter(Boolean)
148
+ .join(", ");
149
+ return tags ? `${value} (${tags})` : String(value);
150
+ });
151
+ const choice = await ctx.ui.select(title, options);
152
+ if (choice === undefined) return undefined;
153
+ return Number.parseInt(choice, 10);
154
+ }
155
+
132
156
  async function pickScope(ctx: ExtensionCommandContext, current: AgentScope): Promise<AgentScope | undefined> {
133
157
  const labels: Record<AgentScope, string> = {
134
158
  user: "user — built-in + ~/.pi/agent/agents (default)",
@@ -190,12 +214,33 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
190
214
  const scope = await pickScope(ctx, base.agentScope);
191
215
  if (scope === undefined) return notifyCancelled(ctx);
192
216
 
217
+ const maxConcurrency = await pickCount(
218
+ ctx,
219
+ "Max sub-agents running at once? (extra work queues)",
220
+ CONCURRENCY_STEPS,
221
+ base.maxConcurrency,
222
+ DEFAULT_MAX_CONCURRENCY,
223
+ );
224
+ if (maxConcurrency === undefined) return notifyCancelled(ctx);
225
+
226
+ const maxParallelTasks = await pickCount(
227
+ ctx,
228
+ "Max tasks in one parallel subagent call?",
229
+ PARALLEL_TASK_STEPS,
230
+ base.maxParallelTasks,
231
+ DEFAULT_MAX_PARALLEL_TASKS,
232
+ );
233
+ if (maxParallelTasks === undefined) return notifyCancelled(ctx);
234
+
193
235
  const next: SubagentsConfig = {
194
236
  enabledAgents: enabled,
195
237
  agentModels: repairStaleModels(ctx, models),
196
238
  thinkingLevel,
197
239
  proactiveInjection: injection,
198
240
  agentScope: scope,
241
+ maxConcurrency,
242
+ maxParallelTasks,
243
+ maxSubagentDepth: base.maxSubagentDepth,
199
244
  };
200
245
  await saveConfig(next, configPath);
201
246
  ctx.ui.notify(`pi-subagents configured. Saved to ${configPath}`, "info");
@@ -208,6 +253,8 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
208
253
  "Change thinking strength",
209
254
  "Toggle proactive injection",
210
255
  "Change agent scope",
256
+ "Change max concurrent sub-agents",
257
+ "Change max parallel tasks",
211
258
  "Full re-setup",
212
259
  ]);
213
260
  if (choice === undefined) return notifyCancelled(ctx);
@@ -236,6 +283,26 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
236
283
  const scope = await pickScope(ctx, config.agentScope);
237
284
  if (scope === undefined) return notifyCancelled(ctx);
238
285
  next.agentScope = scope;
286
+ } else if (choice.startsWith("Change max concurrent")) {
287
+ const maxConcurrency = await pickCount(
288
+ ctx,
289
+ "Max sub-agents running at once? (extra work queues)",
290
+ CONCURRENCY_STEPS,
291
+ config.maxConcurrency,
292
+ DEFAULT_MAX_CONCURRENCY,
293
+ );
294
+ if (maxConcurrency === undefined) return notifyCancelled(ctx);
295
+ next.maxConcurrency = maxConcurrency;
296
+ } else if (choice.startsWith("Change max parallel")) {
297
+ const maxParallelTasks = await pickCount(
298
+ ctx,
299
+ "Max tasks in one parallel subagent call?",
300
+ PARALLEL_TASK_STEPS,
301
+ config.maxParallelTasks,
302
+ DEFAULT_MAX_PARALLEL_TASKS,
303
+ );
304
+ if (maxParallelTasks === undefined) return notifyCancelled(ctx);
305
+ next.maxParallelTasks = maxParallelTasks;
239
306
  }
240
307
 
241
308
  await saveConfig(next, configPath);
package/src/spawn.ts CHANGED
@@ -20,12 +20,13 @@ import type { Message } from "@earendil-works/pi-ai";
20
20
  import type { AgentConfig, AgentSource } from "./agents.ts";
21
21
  import { DEFAULT_THINKING_LEVEL, type ThinkingLevel } from "./config.ts";
22
22
 
23
- export const MAX_PARALLEL_TASKS = 8;
24
- export const MAX_CONCURRENCY = 4;
23
+ /**
24
+ * Limits are configurable: see maxConcurrency / maxParallelTasks /
25
+ * maxSubagentDepth in config.ts (defaults 4 / 8 / 1, via /subagents-setup
26
+ * or pi-subagents.json).
27
+ */
25
28
  /** Default thinking level for sub-agents. pi clamps it to the resolved model's support. */
26
29
  export const SUBAGENT_THINKING_LEVEL: ThinkingLevel = DEFAULT_THINKING_LEVEL;
27
- /** Child processes are leaf agents: they never receive the subagent tool. */
28
- export const MAX_SUBAGENT_DEPTH = 1;
29
30
  export const DEPTH_ENV_VAR = "PI_SUBAGENT_DEPTH";
30
31
  /** No default deadline: sub-agents may run until completion or explicit cancellation. */
31
32
  export const SUBAGENT_TIMEOUT_MS = 0;
package/README-zh.md DELETED
@@ -1,158 +0,0 @@
1
- # pi-subagents
2
-
3
- [![npm version](https://img.shields.io/npm/v/@ferris1225/pi-subagents?color=blue)](https://www.npmjs.com/package/@ferris1225/pi-subagents)
4
- [![downloads](https://img.shields.io/npm/dm/@ferris1225/pi-subagents)](https://www.npmjs.com/package/@ferris1225/pi-subagents)
5
- [![license](https://img.shields.io/npm/l/@ferris1225/pi-subagents)](./LICENSE)
6
- ![platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
7
- ![pi](https://img.shields.io/badge/pi-extension-orange)
8
-
9
- [English](./README.md) | 中文
10
-
11
- 一个聚焦的 [pi](https://pi.dev) 扩展,给主模型提供**它真的会去用**的 sub-agent:
12
- `explore`、`worker`、`reviewer`(外加可选的 `plan`),每个都跑在独立的 `pi` 进程里。
13
- 真正的差异点不是 agent 本身,而是**主动派发注入**——让模型自己主动去委派任务,
14
- 于是你可以把全局 `AGENTS.md` 里那两段派发/审查规则删掉。
15
-
16
- ## 为什么选 pi-subagents?
17
-
18
- pi 故意不内置 sub-agent。社区的补位方案分两种,但都没踩中:
19
-
20
- - **太重** —— 9 个 agent、链式流水线、worktree 集群、到处都是 slash 命令。强大,但机器太多。
21
- - **太安静** —— 只给一个 `subagent` 工具,模型**很少主动调用**,因为 pi 只把工具本身展示给主模型,
22
- 从不展示每个 agent 的描述。于是除非你在全局提示词里强制,否则这些 agent 一直吃灰。
23
-
24
- `pi-subagents` 走中间路线:
25
-
26
- | 优势 | 对你意味着什么 |
27
- |------|----------------|
28
- | **真的会被用** | `before_agent_start` hook 每轮把 agent 清单 + 派发/审查指令注入系统提示词,再由 tool `promptGuidelines` 和 `Use PROACTIVELY when …` 描述加强。这正是重型框架依赖的那根杠杆——我们只是把它变成默认行为。 |
29
- | **体量合适** | 3 个聚焦的 agent(+1 可选),不是 9 个。没有链式/worktree/集群机器。只有 single 和 parallel 两种模式。 |
30
- | **替代你的 AGENTS.md 规则** | 注入的指令是 "Sub-agent Dispatch" 和 "Review, Verification & Commit" 两段的自包含替代。装上它,然后把那两段删掉。 |
31
- | **真隔离** | 每个 agent 都是独立 `pi` 进程(`--no-session`),委派出去的活绝不污染主上下文。 |
32
- | **该只读就只读** | `explore`、`plan`、`reviewer` 都是只读。`reviewer` 跑在**独立**上下文,避免自我确认偏差。 |
33
- | **纯选择式配置** | 不用手敲值:勾选式模块选择器 + 模糊过滤、可翻页的模型选择器。 |
34
- | **合理的模型默认** | 每 agent 可单独覆盖模型;不选就**用主窗口当前 session 的模型**。配置中的不可用模型会自动修复并写回。 |
35
- | **子代理是叶节点** | 子进程不会获得 `subagent` 工具,因此不会递归派发或无限运行。 |
36
- | **零运行时依赖** | 纯 pi 扩展,仅 peer 依赖,无需构建步骤。 |
37
-
38
- ## 安装
39
-
40
- ```bash
41
- pi install npm:@ferris1225/pi-subagents
42
- ```
43
-
44
- 要求 pi **≥ 0.80.6**——子代理思考强度使用该版本引入的 `--thinking` 参数值。
45
-
46
- 然后运行配置向导(纯选择):
47
-
48
- ```text
49
- /subagents-setup
50
- ```
51
-
52
- ## Agent 一览
53
-
54
- | Agent | 默认启用 | 工具 | 职责 |
55
- |-------|:--------:|------|------|
56
- | `explore` | ✅ | 只读 | 快速代码侦察;返回压缩后的发现以便交接。 |
57
- | `worker` | ✅ | 全部 | 实现/修复/重构/测试一个自包含任务。**内部先规划后动手。** |
58
- | `reviewer` | ✅ | 只读 | 在独立上下文做对抗式提交前审查。 |
59
- | `plan` | 可选 | 只读 | 产出可人工审阅的独立实现计划。worker 本就会内部规划,所以只在你需要把计划作为独立产物时才用它。 |
60
-
61
- 每个 agent 都是一个 Markdown 文件(`agents/*.md`:YAML frontmatter + 正文作为 system prompt)。
62
- 想覆盖任意一个,只需把同名 `name` 的文件放进 `~/.pi/agent/agents/`(用户级)或 `.pi/agents/`(项目级)。
63
-
64
- ## 主动派发是怎么工作的
65
-
66
- pi 从不把每个 agent 的描述展示给主模型——它只看到 `subagent` 这个工具。三根杠杆解决这一点:
67
-
68
- 1. **`before_agent_start` 注入** —— 每一轮,把启用的 agent 加上一段派发/审查指令追加进父模型系统提示词。
69
- 2. **tool `promptSnippet` / `promptGuidelines`** —— 在工具激活时持续强化「何时该委派」。
70
- 3. **`Use PROACTIVELY when …` 描述** —— 在 Claude Code agent 生态被验证过的触发措辞。
71
-
72
- 这段指令会引导出一条干净的流程:**`explore` → `worker` → `reviewer`**,独立任务并行扇出,
73
- 以及「信任但需验证」的交接。由于子代理在后台运行,依赖前序结果的步骤必须等结果送达后再启动。
74
-
75
- ## 配置
76
-
77
- 存放在 `~/.pi/agent/pi-subagents.json`(尊重 `PI_CODING_AGENT_DIR`):
78
-
79
- ```json
80
- {
81
- "enabledAgents": ["explore", "worker", "reviewer"],
82
- "agentModels": { "explore": "anthropic/claude-haiku-4-5" },
83
- "thinkingLevel": "max",
84
- "proactiveInjection": true,
85
- "agentScope": "user"
86
- }
87
- ```
88
-
89
- - `enabledAgents` —— 哪些 agent 可被发现并注入。
90
- - `agentModels` —— 每 agent 的模型覆盖(`"provider/model-id"`)。如果已保存的模型不可用,会切换到主窗口当前模型并写回此文件。
91
- - `thinkingLevel` —— 子代理思考强度:`off`、`minimal`、`low`、`medium`、`high`、`xhigh` 或 `max`(默认)。
92
- - `proactiveInjection` —— 开关系统提示词注入。
93
- - `agentScope` —— `"user"`(默认)、`"project"` 或 `"both"`。
94
-
95
- **每个 agent 的模型优先级**:
96
-
97
- ```
98
- 可用的 agentModels[name] → 当前 session 模型 → agent frontmatter 里的默认
99
- ```
100
-
101
- 如果配置的模型不再可用,会切换到主窗口当前模型,并在下一次运行前写回配置文件。
102
-
103
- ## 使用
104
-
105
- 主模型会自己调用 `subagent`,你也可以直接要求:
106
-
107
- ```text
108
- # 单个
109
- 用 explore sub-agent 梳理一下认证是怎么接起来的。
110
-
111
- # 并行(独立任务)
112
- 用并行 sub-agent 跑这两件:探索 API 层,以及探索 DB 层。
113
- ```
114
-
115
- 工具参数形态:
116
-
117
- ```jsonc
118
- // 单个
119
- { "agent": "worker", "task": "<自包含的任务简报>" }
120
- // 并行
121
- { "tasks": [ { "agent": "explore", "task": "..." }, { "agent": "explore", "task": "..." } ] }
122
- ```
123
-
124
- 所有子代理默认在后台启动。工具会立即结束当前主代理回合,编辑器可直接继续输入,
125
- 无需按 Escape;完成输出会显示出来,并在后续用户请求前加入上下文。启动完成后,
126
- Escape 只中止前台工作;切换会话、`/reload` 或退出才会清理仍在运行的后台进程。
127
-
128
- ## 实时状态与通知
129
-
130
- 子代理运行期间,编辑器上方的挂件为每个运行显示一行状态(图标、agent、模型、
131
- token 用量、耗时),其下缩进一行显示它正在做什么:`thinking`、`responding`、
132
- `read src/index.ts`、`bash npm test`……(不会是一坨 JSON 参数)。
133
- `responding` 表示模型正在流式输出普通文本,**不**表示向文件系统写入。
134
-
135
- 运行结束(成功**或**失败)时,该行立即从挂件消失,主窗口收到一条通知,
136
- 给出最终摘要(`✓ worker · openai/gpt-5 · ↑12.4k ↓3.1k · 47s`)。完成结果消息
137
- 才是对话中持久保留、并会供后续请求使用的记录。
138
-
139
- 子代理使用配置的思考强度(默认 `--thinking max`);pi 会按目标模型实际支持
140
- 的级别自适应降级(`max → xhigh → high → … → off`),弱模型也能平稳运行。
141
- 任务内容通过 stdin 传递,只有 agent system prompt 使用短生命周期临时文件。
142
- 子进程输出在内存中流式处理;默认不设运行时限,只有显式中止时才清理整个进程树。
143
-
144
- ## 开发
145
-
146
- ```bash
147
- npm install
148
- npm run check # tsc --noEmit
149
- npm test # vitest
150
- ```
151
-
152
- ## 相关项目
153
-
154
- - [pi-querit-search](https://www.npmjs.com/package/pi-querit-search) —— 为 pi 提供实时网络搜索与网页抓取,同一作者。
155
-
156
- ## 许可证
157
-
158
- MIT
package/agents/plan.md DELETED
@@ -1,41 +0,0 @@
1
- ---
2
- name: plan
3
- description: Implementation planning for non-trivial changes (opt-in). Use when a task needs a human-reviewable design before any code, or one plan must fan out to several workers — turns requirements (and optional explore findings) into a concrete, step-by-step plan with files, risks, and acceptance criteria. Read-only; never edits. Note - a worker also plans internally, so this agent is only needed when you want the plan as a separate artifact.
4
- tools: read, grep, find, ls, bash
5
- model: claude-sonnet-4-5
6
- # Model selection: REASONING + STRUCTURE. Use a strong reasoning model.
7
- ---
8
-
9
- You are a planning specialist. You receive requirements — sometimes plus findings from an `explore` agent — and produce a clear implementation plan that a `worker` will execute verbatim. You have NOT got the caller's conversation history.
10
-
11
- ## Hard constraints
12
- - You must NOT make any changes. Only read, analyze, and plan.
13
- - Bash is read-only: `grep`, `find`, `ls`, `cat`, `git log/show/diff`. No installs, builds, or edits.
14
- - Assume tool permissions are not perfectly enforceable; keep every command strictly read-only by intent.
15
-
16
- ## When invoked
17
- 1. Restate the goal in one sentence. If the request is materially ambiguous, list the specific decisions that must be made instead of guessing.
18
- 2. Inspect existing code and conventions before designing; prefer the smallest coherent root-cause change over a grand rewrite.
19
- 3. Produce small, ordered, independently-verifiable steps. Each step names the file/function to touch and the change.
20
- 4. Call out risks explicitly: edge cases, migrations, concurrency, encoding/Unicode boundaries, backward compatibility.
21
-
22
- ## Collaboration
23
- - Consumes `explore` output when provided; if context is missing, say what an explore should retrieve.
24
- - Feeds `worker`: keep steps concrete enough to execute without re-deriving the design.
25
-
26
- ## Output format
27
- ## Goal
28
- One sentence.
29
- ## Plan
30
- 1. Step — specific file/function to modify and what changes.
31
- ## Files to Modify
32
- - `path/to/file.ts` — what changes and why.
33
- ## New Files (if any)
34
- - `path/to/new.ts` — responsibility.
35
- ## Risks
36
- What to watch out for, and how to mitigate.
37
- ## Acceptance
38
- How to verify correctness: commands, tests, expected behavior.
39
-
40
- ## Quality standards
41
- Concrete and minimal. No prose to fill space. Every step is actionable and verifiable.