pi-subagents 0.8.0 → 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 +10 -0
- package/README.md +17 -0
- package/chain-execution.ts +3 -1
- package/execution.ts +2 -1
- package/index.ts +20 -0
- package/package.json +6 -1
- package/schemas.ts +1 -0
- package/settings.ts +2 -2
- package/subagent-runner.ts +3 -2
- package/types.ts +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
|
|
10
|
+
## [0.8.1] - 2026-02-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`chainDir` param** for persistent chain artifacts — specify a directory to keep artifacts beyond the default 24-hour `/tmp/` cleanup. Relative paths are resolved to absolute via `path.resolve()` for safe use in `{chain_dir}` template substitutions.
|
|
14
|
+
|
|
5
15
|
## [0.8.0] - 2026-02-09
|
|
6
16
|
|
|
7
17
|
### Added
|
package/README.md
CHANGED
|
@@ -474,6 +474,7 @@ Notes:
|
|
|
474
474
|
| `model` | string | agent default | Override model for single agent |
|
|
475
475
|
| `tasks` | `{agent, task, cwd?, skill?}[]` | - | Parallel tasks (sync only) |
|
|
476
476
|
| `chain` | ChainItem[] | - | Sequential steps with behavior overrides (see below) |
|
|
477
|
+
| `chainDir` | string | `/tmp/pi-chain-runs/` | Persistent directory for chain artifacts (default auto-cleaned after 24h) |
|
|
477
478
|
| `clarify` | boolean | true (chains) | Show TUI to preview/edit chain; implies sync mode |
|
|
478
479
|
| `agentScope` | `"user" \| "project" \| "both"` | `user` | Agent discovery scope |
|
|
479
480
|
| `async` | boolean | false | Background execution (requires `clarify: false` for chains) |
|
|
@@ -594,6 +595,22 @@ Press **Ctrl+O** to expand the full streaming view with complete output per step
|
|
|
594
595
|
|
|
595
596
|
> **Note:** Chain visualization (the `✓scout → ●planner` line) is only shown for sequential chains. Chains with parallel steps show per-step cards instead.
|
|
596
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
|
+
|
|
597
614
|
## Async observability
|
|
598
615
|
|
|
599
616
|
Async runs write a dedicated observability folder:
|
package/chain-execution.ts
CHANGED
|
@@ -76,6 +76,7 @@ export interface ChainExecutionParams {
|
|
|
76
76
|
clarify?: boolean;
|
|
77
77
|
onUpdate?: (r: AgentToolResult<Details>) => void;
|
|
78
78
|
chainSkills?: string[];
|
|
79
|
+
chainDir?: string;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
export interface ChainExecutionResult {
|
|
@@ -103,6 +104,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
103
104
|
clarify,
|
|
104
105
|
onUpdate,
|
|
105
106
|
chainSkills: chainSkillsParam,
|
|
107
|
+
chainDir: chainDirBase,
|
|
106
108
|
} = params;
|
|
107
109
|
const chainSkills = chainSkillsParam ?? [];
|
|
108
110
|
|
|
@@ -123,7 +125,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
123
125
|
?? (isParallelStep(firstStep) ? firstStep.parallel[0]!.task! : (firstStep as SequentialStep).task!);
|
|
124
126
|
|
|
125
127
|
// Create chain directory
|
|
126
|
-
const chainDir = createChainDir(runId);
|
|
128
|
+
const chainDir = createChainDir(runId, chainDirBase);
|
|
127
129
|
|
|
128
130
|
// Check if chain has any parallel steps
|
|
129
131
|
const hasParallelSteps = chainSteps.some(isParallelStep);
|
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;
|
|
@@ -377,6 +396,7 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
|
|
|
377
396
|
clarify: params.clarify,
|
|
378
397
|
onUpdate,
|
|
379
398
|
chainSkills,
|
|
399
|
+
chainDir: params.chainDir,
|
|
380
400
|
});
|
|
381
401
|
}
|
|
382
402
|
|
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/schemas.ts
CHANGED
|
@@ -75,6 +75,7 @@ export const SubagentParams = Type.Object({
|
|
|
75
75
|
})),
|
|
76
76
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task}, ...]" })),
|
|
77
77
|
chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: sequential pipeline where each step's response becomes {previous} for the next. Use {task}, {previous}, {chain_dir} in task templates." })),
|
|
78
|
+
chainDir: Type.Optional(Type.String({ description: "Persistent directory for chain artifacts. Default: /tmp/pi-chain-runs/ (auto-cleaned after 24h)" })),
|
|
78
79
|
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
|
79
80
|
agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'user')" })),
|
|
80
81
|
cwd: Type.Optional(Type.String()),
|
package/settings.ts
CHANGED
|
@@ -88,8 +88,8 @@ export function getStepAgents(step: ChainStep): string[] {
|
|
|
88
88
|
// Chain Directory Management
|
|
89
89
|
// =============================================================================
|
|
90
90
|
|
|
91
|
-
export function createChainDir(runId: string): string {
|
|
92
|
-
const chainDir = path.join(CHAIN_RUNS_DIR, runId);
|
|
91
|
+
export function createChainDir(runId: string, baseDir?: string): string {
|
|
92
|
+
const chainDir = path.join(baseDir ? path.resolve(baseDir) : CHAIN_RUNS_DIR, runId);
|
|
93
93
|
fs.mkdirSync(chainDir, { recursive: true });
|
|
94
94
|
return chainDir;
|
|
95
95
|
}
|
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
|