pi-fast-subagent 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
@@ -69,6 +69,7 @@ You are code exploration specialist. Read relevant files, trace data flow, summa
69
69
  | `description` | yes | One-line description shown in `/fast-subagent:agent` |
70
70
  | `model` | no | Model override, format `provider/model-id` (e.g. `anthropic/claude-haiku-4-5`) |
71
71
  | `tools` | no | Tool allowlist (see below) |
72
+ | `maxDepth` | no | Nested subagent depth this agent may spawn. Default `0` means this agent cannot call `subagent`. |
72
73
 
73
74
  ### `tools:` field
74
75
 
@@ -125,6 +126,28 @@ tools: all
125
126
 
126
127
  **YAML comments** (`# …`) are allowed inside the frontmatter — handy for documenting *why* a particular tool set was chosen. See `agents/general.md` and `agents/scout.md` for examples.
127
128
 
129
+ ### `maxDepth:` field
130
+
131
+ Subagents cannot spawn other subagents by default, even when `tools` exposes the `subagent` tool.
132
+
133
+ ```md
134
+ ---
135
+ name: planner
136
+ description: Can delegate one level deeper
137
+ maxDepth: 1
138
+ ---
139
+ ```
140
+
141
+ Depth counts nested generations from that agent:
142
+
143
+ | Value | Behavior |
144
+ |-------|----------|
145
+ | *(omitted)* / `0` | This agent cannot spawn subagents |
146
+ | `1` | This agent may spawn subagents, but those children cannot spawn again unless their own `maxDepth` allows it |
147
+ | `2` | Allows two nested generations, subject to each child agent's own `maxDepth` |
148
+
149
+ Aliases accepted: `max_depth`, `depth`, `subagentDepth`.
150
+
128
151
  ## Background Agents
129
152
 
130
153
  Every foreground subagent can be moved to background at any time. Background jobs run concurrently while you continue chatting. When a job finishes, pi automatically posts the result as a follow-up message.
package/agents/general.md CHANGED
@@ -11,6 +11,9 @@ model: anthropic/claude-haiku-4-5
11
11
  # comma-separated list → explicit allowlist, e.g. `read, grep, web_search`
12
12
  # General is meant to be a do-anything fallback, so it keeps everything explicit.
13
13
  tools: all
14
+
15
+ # Subagents cannot spawn subagents by default. Set maxDepth: 1+ to opt in.
16
+ maxDepth: 0
14
17
  ---
15
18
 
16
19
  You are general-purpose subagent.
package/agents/scout.md CHANGED
@@ -11,6 +11,9 @@ model: anthropic/claude-haiku-4-5
11
11
  # comma-separated list → explicit allowlist
12
12
  # Scout is read-only: no `edit`, no `write`, no extension tools. Keeps the agent from mutating the codebase.
13
13
  tools: read, bash, grep, find, ls
14
+
15
+ # Subagents cannot spawn subagents by default. Keep scout focused on exploration only.
16
+ maxDepth: 0
14
17
  ---
15
18
 
16
19
  You are code exploration specialist.
package/agents.ts CHANGED
@@ -31,6 +31,8 @@ export interface AgentConfig {
31
31
  description: string;
32
32
  model?: string;
33
33
  tools: AgentTools;
34
+ /** Number of nested subagent generations this agent may spawn. Default: 0. */
35
+ maxDepth: number;
34
36
  systemPrompt: string;
35
37
  source: "user" | "project";
36
38
  filePath: string;
@@ -58,6 +60,13 @@ function parseToolsField(raw: unknown): AgentTools {
58
60
  return list.length ? list : "all";
59
61
  }
60
62
 
63
+ function parseMaxDepthField(raw: unknown): number {
64
+ if (raw === undefined || raw === null || raw === "") return 0;
65
+ const n = Number(raw);
66
+ if (!Number.isFinite(n) || n < 0) return 0;
67
+ return Math.floor(n);
68
+ }
69
+
61
70
  function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
62
71
  if (!fs.existsSync(dir)) return [];
63
72
  let entries: fs.Dirent[];
@@ -77,11 +86,15 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
77
86
  const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
78
87
  if (!frontmatter?.name || !frontmatter?.description) continue;
79
88
  const tools = parseToolsField(frontmatter.tools);
89
+ const maxDepth = parseMaxDepthField(
90
+ frontmatter.maxDepth ?? frontmatter.max_depth ?? frontmatter.depth ?? frontmatter.subagentDepth,
91
+ );
80
92
  agents.push({
81
93
  name: frontmatter.name,
82
94
  description: frontmatter.description,
83
95
  model: frontmatter.model,
84
96
  tools,
97
+ maxDepth,
85
98
  systemPrompt: body.trim(),
86
99
  source,
87
100
  filePath,
package/index.ts CHANGED
@@ -257,8 +257,9 @@ const _fgJobs = new Map<string, ForegroundDetachEntry>();
257
257
 
258
258
  // ─── In-process runner ───────────────────────────────────────────────────────
259
259
 
260
- const MAX_DEPTH = 2;
260
+ const DEFAULT_MAX_DEPTH = 0;
261
261
  const DEPTH_ENV = "PI_FAST_SUBAGENT_DEPTH";
262
+ const MAX_DEPTH_ENV = "PI_FAST_SUBAGENT_MAX_DEPTH";
262
263
 
263
264
  interface ToolCallEntry {
264
265
  id: string;
@@ -330,8 +331,9 @@ function formatBgJobDetails(job: BackgroundSubagentJob, now = Date.now()): strin
330
331
  return lines.join("\n");
331
332
  }
332
333
 
333
- // Module-level depth counter avoids process.env race conditions in parallel mode
334
+ // Module-level depth counters for nested in-process subagent calls.
334
335
  let _currentDepth = 0;
336
+ let _currentMaxDepth = DEFAULT_MAX_DEPTH;
335
337
 
336
338
  async function runAgent(
337
339
  agent: AgentConfig,
@@ -343,11 +345,12 @@ async function runAgent(
343
345
  parentDepth?: number,
344
346
  ): Promise<RunResult> {
345
347
  const depth = parentDepth ?? _currentDepth;
346
- if (depth >= MAX_DEPTH) {
348
+ const isNestedCall = depth > 0;
349
+ if (isNestedCall && depth > _currentMaxDepth) {
347
350
  return {
348
351
  output: "",
349
352
  exitCode: 1,
350
- error: `Max subagent depth (${MAX_DEPTH}) exceeded. Increase PI_FAST_SUBAGENT_DEPTH env to allow deeper nesting.`,
353
+ error: `Nested subagents are disabled by default. Set maxDepth: ${depth} (or higher) in the parent agent frontmatter to allow this call.`,
351
354
  toolCalls: [],
352
355
  usage: { input: 0, output: 0, cost: 0, turns: 0 },
353
356
  };
@@ -543,10 +546,17 @@ async function runAgent(
543
546
  });
544
547
  });
545
548
 
546
- // Propagate depth to nested calls use module counter (safe for parallel) + env for subprocess compat
549
+ // Propagate depth to nested calls. `maxDepth` is per-agent and defaults to 0,
550
+ // so subagents cannot spawn subagents unless their frontmatter opts in.
547
551
  const prevEnvDepth = process.env[DEPTH_ENV];
548
- process.env[DEPTH_ENV] = String(depth + 1);
552
+ const prevEnvMaxDepth = process.env[MAX_DEPTH_ENV];
553
+ const prevDepth = _currentDepth;
554
+ const prevMaxDepth = _currentMaxDepth;
555
+ const maxDepth = Math.max(DEFAULT_MAX_DEPTH, agent.maxDepth ?? DEFAULT_MAX_DEPTH);
549
556
  _currentDepth = depth + 1;
557
+ _currentMaxDepth = depth + maxDepth;
558
+ process.env[DEPTH_ENV] = String(_currentDepth);
559
+ process.env[MAX_DEPTH_ENV] = String(_currentMaxDepth);
550
560
 
551
561
  let exitCode = 0;
552
562
  let error: string | undefined;
@@ -572,7 +582,10 @@ async function runAgent(
572
582
  loaderLease.release();
573
583
  if (prevEnvDepth === undefined) delete process.env[DEPTH_ENV];
574
584
  else process.env[DEPTH_ENV] = prevEnvDepth;
575
- _currentDepth = depth;
585
+ if (prevEnvMaxDepth === undefined) delete process.env[MAX_DEPTH_ENV];
586
+ else process.env[MAX_DEPTH_ENV] = prevEnvMaxDepth;
587
+ _currentDepth = prevDepth;
588
+ _currentMaxDepth = prevMaxDepth;
576
589
  }
577
590
 
578
591
  return { output: lastOutput, exitCode, error, model: detectedModel, toolCalls, usage };
@@ -769,6 +782,7 @@ export default function (pi: ExtensionAPI) {
769
782
  `Description: ${agent.description}`,
770
783
  agent.model ? `Model: ${agent.model}` : "",
771
784
  `Tools: ${formatTools(agent.tools)}`,
785
+ `Max subagent depth: ${agent.maxDepth}`,
772
786
  agent.systemPrompt ? `\nSystem prompt:\n${agent.systemPrompt}` : "",
773
787
  ].filter(Boolean).join("\n");
774
788
  ctx.ui.notify(lines, "info");
@@ -781,7 +795,7 @@ export default function (pi: ExtensionAPI) {
781
795
  "Add .md files to:\n" +
782
796
  " ~/.pi/agent/agents/ (user-level)\n" +
783
797
  " .pi/agents/ (project-level)\n" +
784
- "\nFrontmatter required: name, description. Optional: model, tools.",
798
+ "\nFrontmatter required: name, description. Optional: model, tools, maxDepth.",
785
799
  "info"
786
800
  );
787
801
  return;
@@ -1181,6 +1195,7 @@ export default function (pi: ExtensionAPI) {
1181
1195
  `**Description:** ${agent.description}`,
1182
1196
  agent.model ? `**Model:** ${agent.model}` : null,
1183
1197
  `**Tools:** ${formatTools(agent.tools)}`,
1198
+ `**Max subagent depth:** ${agent.maxDepth}`,
1184
1199
  agent.systemPrompt ? `\n**System prompt:**\n${agent.systemPrompt}` : null,
1185
1200
  ].filter(Boolean).join("\n");
1186
1201
  return { content: [{ type: "text", text: info }] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fast-subagent",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "In-process subagent delegation for pi with single, parallel, and background modes",
5
5
  "type": "module",
6
6
  "keywords": [