pi-subagents 0.8.1 → 0.8.2
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/CHANGELOG.md +5 -0
- package/README.md +16 -0
- package/execution.ts +2 -1
- package/index.ts +19 -0
- package/package.json +6 -1
- package/subagent-runner.ts +3 -2
- package/types.ts +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.8.2] - 2026-02-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Recursion depth guard (`PI_SUBAGENT_MAX_DEPTH`) to prevent runaway nested subagent spawning. Default max depth is 2 (main -> subagent -> sub-subagent). Deeper calls are blocked with guidance to the calling agent.
|
|
9
|
+
|
|
5
10
|
## [0.8.1] - 2026-02-10
|
|
6
11
|
|
|
7
12
|
### Added
|
package/README.md
CHANGED
|
@@ -595,6 +595,22 @@ Press **Ctrl+O** to expand the full streaming view with complete output per step
|
|
|
595
595
|
|
|
596
596
|
> **Note:** Chain visualization (the `✓scout → ●planner` line) is only shown for sequential chains. Chains with parallel steps show per-step cards instead.
|
|
597
597
|
|
|
598
|
+
## Nested subagent recursion guard
|
|
599
|
+
|
|
600
|
+
Subagents can themselves call the `subagent` tool, which risks unbounded recursive spawning (slow, expensive, hard to observe). A depth guard prevents this.
|
|
601
|
+
|
|
602
|
+
By default nesting is limited to **2 levels**: `main session → subagent → sub-subagent`. Any deeper `subagent` calls are blocked and return an error with guidance to the calling agent.
|
|
603
|
+
|
|
604
|
+
Override the limit with `PI_SUBAGENT_MAX_DEPTH` **set before starting `pi`**:
|
|
605
|
+
|
|
606
|
+
```bash
|
|
607
|
+
export PI_SUBAGENT_MAX_DEPTH=3 # allow one more level (use with caution)
|
|
608
|
+
export PI_SUBAGENT_MAX_DEPTH=1 # only allow direct subagents, no nesting
|
|
609
|
+
export PI_SUBAGENT_MAX_DEPTH=0 # disable the subagent tool entirely
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
`PI_SUBAGENT_DEPTH` is an internal variable propagated automatically to child processes -- don't set it manually.
|
|
613
|
+
|
|
598
614
|
## Async observability
|
|
599
615
|
|
|
600
616
|
Async runs write a dedicated observability folder:
|
package/execution.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type SingleResult,
|
|
21
21
|
DEFAULT_MAX_OUTPUT,
|
|
22
22
|
truncateOutput,
|
|
23
|
+
getSubagentDepthEnv,
|
|
23
24
|
} from "./types.js";
|
|
24
25
|
import {
|
|
25
26
|
writePrompt,
|
|
@@ -150,7 +151,7 @@ export async function runSync(
|
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
const spawnEnv = { ...process.env };
|
|
154
|
+
const spawnEnv = { ...process.env, ...getSubagentDepthEnv() };
|
|
154
155
|
const mcpDirect = agent.mcpDirectTools;
|
|
155
156
|
if (mcpDirect?.length) {
|
|
156
157
|
spawnEnv.MCP_DIRECT_TOOLS = mcpDirect.join(",");
|
package/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
POLL_INTERVAL_MS,
|
|
39
39
|
RESULTS_DIR,
|
|
40
40
|
WIDGET_KEY,
|
|
41
|
+
checkSubagentDepth,
|
|
41
42
|
} from "./types.js";
|
|
42
43
|
import { readStatus, findByPrefix, getFinalOutput, mapConcurrent } from "./utils.js";
|
|
43
44
|
import { runSync } from "./execution.js";
|
|
@@ -189,6 +190,24 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
|
|
|
189
190
|
}
|
|
190
191
|
return handleManagementAction(params.action, params, ctx);
|
|
191
192
|
}
|
|
193
|
+
|
|
194
|
+
const { blocked, depth, maxDepth } = checkSubagentDepth();
|
|
195
|
+
if (blocked) {
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: "text",
|
|
200
|
+
text:
|
|
201
|
+
`Nested subagent call blocked (depth=${depth}, max=${maxDepth}). ` +
|
|
202
|
+
"You are running at the maximum subagent nesting depth. " +
|
|
203
|
+
"Complete your current task directly without delegating to further subagents.",
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
isError: true,
|
|
207
|
+
details: { mode: "single" as const, results: [] },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
192
211
|
const scope: AgentScope = params.agentScope ?? "user";
|
|
193
212
|
currentSessionId = ctx.sessionManager.getSessionFile() ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
194
213
|
const agents = discoverAgents(ctx.cwd, scope).agents;
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-subagents",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
|
|
5
5
|
"author": "Nico Bailon",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|
|
9
10
|
"url": "git+https://github.com/nicobailon/pi-subagents.git"
|
|
@@ -26,10 +27,14 @@
|
|
|
26
27
|
},
|
|
27
28
|
"files": [
|
|
28
29
|
"*.ts",
|
|
30
|
+
"!*.test.ts",
|
|
29
31
|
"*.mjs",
|
|
30
32
|
"README.md",
|
|
31
33
|
"CHANGELOG.md"
|
|
32
34
|
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node --experimental-strip-types --test recursion-guard.test.ts"
|
|
37
|
+
},
|
|
33
38
|
"pi": {
|
|
34
39
|
"extensions": [
|
|
35
40
|
"./index.ts",
|
package/subagent-runner.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
DEFAULT_MAX_OUTPUT,
|
|
12
12
|
type MaxOutputConfig,
|
|
13
13
|
truncateOutput,
|
|
14
|
+
getSubagentDepthEnv,
|
|
14
15
|
} from "./types.js";
|
|
15
16
|
|
|
16
17
|
interface SubagentStep {
|
|
@@ -102,8 +103,8 @@ function runPiStreaming(
|
|
|
102
103
|
): Promise<{ stdout: string; exitCode: number | null }> {
|
|
103
104
|
return new Promise((resolve) => {
|
|
104
105
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
105
|
-
const spawnEnv =
|
|
106
|
-
const child = spawn("pi", args, { cwd, stdio: ["ignore", "pipe", "pipe"],
|
|
106
|
+
const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv() };
|
|
107
|
+
const child = spawn("pi", args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
|
|
107
108
|
let stdout = "";
|
|
108
109
|
|
|
109
110
|
child.stdout.on("data", (chunk: Buffer) => {
|
package/types.ts
CHANGED
|
@@ -242,6 +242,27 @@ export const ASYNC_DIR = "/tmp/pi-async-subagent-runs";
|
|
|
242
242
|
export const WIDGET_KEY = "subagent-async";
|
|
243
243
|
export const POLL_INTERVAL_MS = 250;
|
|
244
244
|
export const MAX_WIDGET_JOBS = 4;
|
|
245
|
+
export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
|
|
246
|
+
|
|
247
|
+
// ============================================================================
|
|
248
|
+
// Recursion Depth Guard
|
|
249
|
+
// ============================================================================
|
|
250
|
+
|
|
251
|
+
export function checkSubagentDepth(): { blocked: boolean; depth: number; maxDepth: number } {
|
|
252
|
+
const depth = Number(process.env.PI_SUBAGENT_DEPTH ?? "0");
|
|
253
|
+
const maxDepth = Number(process.env.PI_SUBAGENT_MAX_DEPTH ?? String(DEFAULT_SUBAGENT_MAX_DEPTH));
|
|
254
|
+
const blocked = Number.isFinite(depth) && Number.isFinite(maxDepth) && depth >= maxDepth;
|
|
255
|
+
return { blocked, depth, maxDepth };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function getSubagentDepthEnv(): Record<string, string> {
|
|
259
|
+
const parentDepth = Number(process.env.PI_SUBAGENT_DEPTH ?? "0");
|
|
260
|
+
const nextDepth = Number.isFinite(parentDepth) ? parentDepth + 1 : 1;
|
|
261
|
+
return {
|
|
262
|
+
PI_SUBAGENT_DEPTH: String(nextDepth),
|
|
263
|
+
PI_SUBAGENT_MAX_DEPTH: process.env.PI_SUBAGENT_MAX_DEPTH ?? String(DEFAULT_SUBAGENT_MAX_DEPTH),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
245
266
|
|
|
246
267
|
// ============================================================================
|
|
247
268
|
// Utility Functions
|