@ferris1225/pi-subagents 0.7.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
@@ -4,7 +4,7 @@
4
4
  [![downloads](https://img.shields.io/npm/dm/@ferris1225/pi-subagents)](https://www.npmjs.com/package/@ferris1225/pi-subagents)
5
5
  [![license](https://img.shields.io/npm/l/@ferris1225/pi-subagents)](./LICENSE)
6
6
  ![platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
7
- ![pi](https://img.shields.io/badge/platform-pi-orange)
7
+ ![pi](https://img.shields.io/badge/pi-extension-orange)
8
8
 
9
9
  Focused background delegation for [pi](https://pi.dev). `pi-subagents` adds a small set of
10
10
  specialized agents that run in isolated child processes, report results back to the main
@@ -21,8 +21,8 @@ agent, and keep the workflow moving without manual polling.
21
21
  - **Parallel fan-out** — run independent tasks together, with a bounded background queue.
22
22
  - **Live progress** — a TUI widget shows each agent's status, activity, model, usage, and
23
23
  elapsed time; completion also produces a concise notification.
24
- - **Per-agent configuration** — enable agents, select models, set thinking strength, and
25
- choose discovery scope from `/subagents-setup`.
24
+ - **Per-agent configuration** — enable agents, select models, set thinking strength, tune
25
+ concurrency limits, and choose discovery scope from `/subagents-setup`.
26
26
  - **Leaf processes** — child agents cannot access the `subagent` tool, so delegation cannot
27
27
  recurse.
28
28
 
@@ -40,8 +40,7 @@ After installation, open the setup wizard in an interactive TUI session:
40
40
  /subagents-setup
41
41
  ```
42
42
 
43
- The default configuration enables `explore`, `worker`, and `reviewer`. `plan` is available
44
- but opt-in.
43
+ The default configuration enables `explore`, `worker`, and `reviewer`.
45
44
 
46
45
  ## Included agents
47
46
 
@@ -50,7 +49,6 @@ but opt-in.
50
49
  | `explore` | Yes | Read-only | Fast codebase reconnaissance and structured findings. |
51
50
  | `worker` | Yes | Full | Implements, fixes, refactors, and tests a self-contained task. |
52
51
  | `reviewer` | Yes | Read-only | Independent adversarial review of a diff before completion. |
53
- | `plan` | No | Read-only | Produces a separate implementation plan when one is useful. |
54
52
 
55
53
  Agents are Markdown files in `agents/`. Each file contains YAML frontmatter and a system
56
54
  prompt. User and project scopes can override a built-in agent with the same name.
@@ -72,8 +70,9 @@ main agent
72
70
  1. The main agent calls `subagent` with a self-contained brief.
73
71
  2. The tool returns immediately and ends that foreground tool turn, leaving the editor ready
74
72
  for input.
75
- 3. The child process works independently. Up to four queued runs execute at once; a single
76
- parallel request may contain up to eight tasks.
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`).
77
76
  4. On completion or failure, the extension sends a durable result message to the main
78
77
  session. That message automatically wakes the main agent, or waits until its current turn
79
78
  finishes.
@@ -131,7 +130,10 @@ Configuration is stored at `~/.pi/agent/pi-subagents.json`. The location follows
131
130
  },
132
131
  "thinkingLevel": "max",
133
132
  "proactiveInjection": true,
134
- "agentScope": "user"
133
+ "agentScope": "user",
134
+ "maxConcurrency": 4,
135
+ "maxParallelTasks": 8,
136
+ "maxSubagentDepth": 1
135
137
  }
136
138
  ```
137
139
 
@@ -142,6 +144,18 @@ Configuration is stored at `~/.pi/agent/pi-subagents.json`. The location follows
142
144
  | `thinkingLevel` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. |
143
145
  | `proactiveInjection` | Whether to add the delegation directive to the main system prompt. |
144
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.
145
159
 
146
160
  Model selection uses this precedence:
147
161
 
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.7.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": {
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,7 +149,7 @@ 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
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)."
@@ -164,6 +169,8 @@ export default function (pi: ExtensionAPI): void {
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 };
@@ -311,14 +318,17 @@ export default function (pi: ExtensionAPI): void {
311
318
  // Sub-agents intentionally detach from the foreground turn. This makes the
312
319
  // editor available immediately; completion messages later wake the main agent.
313
320
  if (params.tasks && params.tasks.length > 0) {
314
- if (params.tasks.length > MAX_PARALLEL_TASKS) {
315
- return {
316
- content: [
317
- { type: "text", text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.` },
318
- ],
319
- details: makeDetails("parallel", true)([]),
320
- };
321
- }
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
+ }
322
332
 
323
333
  const results = params.tasks.map((task) => startBackground(task.agent, task.task, task.cwd));
324
334
  const started = results.filter((result) => result.exitCode === -1).length;
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
  };
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/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.