pi-fast-subagent 0.6.1 → 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,15 +69,17 @@ 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
 
75
- Controls which tools the subagent has access to. Subagents inherit parent extensions (web_search, fetch_content, mcp, …) by default.
76
+ Controls which tools the subagent has access to. The default is **all tools** — builtins plus parent extensions (web_search, fetch_content, mcp, playwright, …). Agents opt into lean mode with `tools: builtins` or an explicit built-in allowlist.
76
77
 
77
78
  | Value | Behavior |
78
79
  |-------|----------|
79
- | *(omitted)* | Inherit everything — all builtins + all parent extensions (**default**) |
80
+ | *(omitted)* | Builtins + every parent extension (**default**) |
80
81
  | `all` | Same as omitted — explicit "everything" |
82
+ | `builtins` | Builtins only — `read, bash, edit, write, grep, find, ls` |
81
83
  | `none` | No tools at all — pure reasoning agent |
82
84
  | comma list | Allowlist; extensions auto-load only if any listed tool is non-builtin |
83
85
 
@@ -95,9 +97,10 @@ tools: none
95
97
 
96
98
  ```md
97
99
  ---
98
- name: coder
99
- description: Lean code-editing agent, no extensions
100
- tools: read, bash, edit, write, grep, find, ls
100
+ name: scout
101
+ description: Read-only code explorer
102
+ # drop `edit` and `write` so the agent cannot mutate the codebase
103
+ tools: read, bash, grep, find, ls
101
104
  ---
102
105
  ```
103
106
 
@@ -105,11 +108,45 @@ tools: read, bash, edit, write, grep, find, ls
105
108
  ---
106
109
  name: researcher
107
110
  description: Web research agent
111
+ # listing `web_search` triggers extension loading; `read` + `write` keep the rest local
108
112
  tools: read, write, web_search, fetch_content
109
113
  ---
110
114
  ```
111
115
 
112
- > **Performance note:** inheriting all extensions adds startup cost (extension init) and token cost (larger system prompt). For tight, focused agents, list tools explicitly — extensions are only loaded when the allowlist actually needs them.
116
+ ```md
117
+ ---
118
+ name: general
119
+ description: Do-anything helper
120
+ # `tools` omitted means all tools; `tools: all` is equivalent
121
+ tools: all
122
+ ---
123
+ ```
124
+
125
+ > **Performance note:** omitted `tools` / `tools: all` loads every installed pi extension into the subagent session. That adds startup cost (extension init, possibly MCP server spawn, playwright runtime, …) and token cost (bigger system prompt). Use `tools: builtins` or list specific tools for tight, focused agents.
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.
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`.
113
150
 
114
151
  ## Background Agents
115
152
 
package/agents/general.md CHANGED
@@ -2,6 +2,18 @@
2
2
  name: general
3
3
  description: General-purpose helper for coding, analysis, writing, debugging, and task execution
4
4
  model: anthropic/claude-haiku-4-5
5
+
6
+ # tools: which tools this agent can use.
7
+ # (omit) → all tools: builtins + every parent extension (default)
8
+ # all → same as omitted — explicit "everything"
9
+ # builtins → read, bash, edit, write, grep, find, ls only (fast startup)
10
+ # none → no tools — pure reasoning
11
+ # comma-separated list → explicit allowlist, e.g. `read, grep, web_search`
12
+ # General is meant to be a do-anything fallback, so it keeps everything explicit.
13
+ tools: all
14
+
15
+ # Subagents cannot spawn subagents by default. Set maxDepth: 1+ to opt in.
16
+ maxDepth: 0
5
17
  ---
6
18
 
7
19
  You are general-purpose subagent.
package/agents/scout.md CHANGED
@@ -2,7 +2,18 @@
2
2
  name: scout
3
3
  description: Explores codebases, maps structure, traces data flow, answers how things work across many files
4
4
  model: anthropic/claude-haiku-4-5
5
- tools: read, bash, edit, write, grep, find, ls
5
+
6
+ # tools: which tools this agent can use.
7
+ # (omit) → all tools: builtins + every parent extension (default)
8
+ # all → same as omitted — explicit "everything"
9
+ # builtins → read, bash, edit, write, grep, find, ls only (fast startup)
10
+ # none → no tools — pure reasoning
11
+ # comma-separated list → explicit allowlist
12
+ # Scout is read-only: no `edit`, no `write`, no extension tools. Keeps the agent from mutating the codebase.
13
+ tools: read, bash, grep, find, ls
14
+
15
+ # Subagents cannot spawn subagents by default. Keep scout focused on exploration only.
16
+ maxDepth: 0
6
17
  ---
7
18
 
8
19
  You are code exploration specialist.
package/agents.ts CHANGED
@@ -10,19 +10,19 @@ import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
10
10
 
11
11
  /**
12
12
  * tools frontmatter semantics:
13
- * unset → inherit everything (builtins + extensions)same as `all`
14
- * `all` → all builtins + all extension tools (web_search, fetch_content, mcp, …)
13
+ * unset → all builtins + all parent extensions — DEFAULT
14
+ * `all` → all builtins + all parent extensions (web_search, fetch_content, mcp, …)
15
+ * `builtins` → built-in coding tools only (read, bash, edit, write, grep, find, ls)
15
16
  * `none` → no tools at all
16
17
  * comma list → allowlist; extensions auto-loaded if any listed tool is non-builtin
17
- * for lean "builtins-only" mode, list them explicitly:
18
- * tools: read, bash, edit, write, grep, find, ls
19
18
  *
20
19
  * Represented as:
21
- * "all" everything (default when frontmatter omits `tools`)
20
+ * "builtins" only built-in coding tools
21
+ * "all" → everything (default)
22
22
  * "none" → no tools
23
23
  * string[] → allowlist
24
24
  */
25
- export type AgentTools = "all" | "none" | string[];
25
+ export type AgentTools = "builtins" | "all" | "none" | string[];
26
26
 
27
27
  export const BUILTIN_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"] as const;
28
28
 
@@ -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;
@@ -40,11 +42,12 @@ const BUILTIN_TOOLS = new Set<string>(BUILTIN_TOOL_NAMES);
40
42
 
41
43
  export function agentNeedsExtensions(tools: AgentTools): boolean {
42
44
  if (tools === "all") return true;
43
- if (tools === "none") return false;
45
+ if (tools === "builtins" || tools === "none") return false;
44
46
  return tools.some((t) => !BUILTIN_TOOLS.has(t));
45
47
  }
46
48
 
47
- // Default: everything. Agents list specific tools for lean / restricted mode.
49
+ // Default: all tools, matching pi-subagents behavior. Agents opt into lean mode
50
+ // with `tools: builtins` or explicit built-in allowlists.
48
51
  function parseToolsField(raw: unknown): AgentTools {
49
52
  if (raw === undefined || raw === null) return "all";
50
53
  const str = String(raw).trim();
@@ -52,10 +55,18 @@ function parseToolsField(raw: unknown): AgentTools {
52
55
  const lower = str.toLowerCase();
53
56
  if (lower === "all") return "all";
54
57
  if (lower === "none") return "none";
58
+ if (lower === "builtins" || lower === "builtin") return "builtins";
55
59
  const list = str.split(",").map((t) => t.trim()).filter(Boolean);
56
60
  return list.length ? list : "all";
57
61
  }
58
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
+
59
70
  function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
60
71
  if (!fs.existsSync(dir)) return [];
61
72
  let entries: fs.Dirent[];
@@ -75,11 +86,15 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
75
86
  const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
76
87
  if (!frontmatter?.name || !frontmatter?.description) continue;
77
88
  const tools = parseToolsField(frontmatter.tools);
89
+ const maxDepth = parseMaxDepthField(
90
+ frontmatter.maxDepth ?? frontmatter.max_depth ?? frontmatter.depth ?? frontmatter.subagentDepth,
91
+ );
78
92
  agents.push({
79
93
  name: frontmatter.name,
80
94
  description: frontmatter.description,
81
95
  model: frontmatter.model,
82
96
  tools,
97
+ maxDepth,
83
98
  systemPrompt: body.trim(),
84
99
  source,
85
100
  filePath,
package/index.ts CHANGED
@@ -14,6 +14,7 @@ import type {
14
14
  AgentToolUpdateCallback,
15
15
  ExtensionAPI,
16
16
  ExtensionContext,
17
+ ResourceLoader,
17
18
  ToolRenderResultOptions,
18
19
  } from "@mariozechner/pi-coding-agent";
19
20
  import { BackgroundJobManager } from "./background-job-manager.js";
@@ -36,6 +37,7 @@ import { type AgentConfig, agentNeedsExtensions, discoverAgents } from "./agents
36
37
 
37
38
  function formatTools(tools: AgentConfig["tools"]): string {
38
39
  if (tools === "all") return "all";
40
+ if (tools === "builtins") return "builtins (default)";
39
41
  if (tools === "none") return "none";
40
42
  return tools.join(", ");
41
43
  }
@@ -125,6 +127,125 @@ function refreshBgStatus(): void {
125
127
  _setBgStatus?.(running.length > 0 ? `⧗ ${running.length} bg agent${running.length > 1 ? "s" : ""}` : undefined);
126
128
  }
127
129
 
130
+ // ─── Resource loader pool ─────────────────────────────────────────────────────
131
+
132
+ interface LoaderPoolEntry {
133
+ idle: DefaultResourceLoader[];
134
+ active: Set<DefaultResourceLoader>;
135
+ warming: Set<Promise<void>>;
136
+ }
137
+
138
+ interface LoaderLease {
139
+ loader: ResourceLoader;
140
+ release: () => void;
141
+ }
142
+
143
+ const _loaderPool = new Map<string, LoaderPoolEntry>();
144
+
145
+ function loaderPoolKey(cwd: string, agentDir: string, noExtensions: boolean): string {
146
+ return `${cwd}\0${agentDir}\0${noExtensions ? "noext" : "ext"}`;
147
+ }
148
+
149
+ function getLoaderPoolEntry(cwd: string, agentDir: string, noExtensions: boolean): LoaderPoolEntry {
150
+ const key = loaderPoolKey(cwd, agentDir, noExtensions);
151
+ let entry = _loaderPool.get(key);
152
+ if (!entry) {
153
+ entry = { idle: [], active: new Set(), warming: new Set() };
154
+ _loaderPool.set(key, entry);
155
+ }
156
+ return entry;
157
+ }
158
+
159
+ function makeLoaderOptions(cwd: string, agentDir: string, noExtensions: boolean): DefaultResourceLoaderOptions {
160
+ return {
161
+ cwd,
162
+ agentDir,
163
+ noExtensions,
164
+ noContextFiles: true,
165
+ noSkills: true,
166
+ };
167
+ }
168
+
169
+ class AgentPromptResourceLoader implements ResourceLoader {
170
+ constructor(
171
+ private readonly base: ResourceLoader,
172
+ private readonly systemPromptOverride: string | undefined,
173
+ ) {}
174
+
175
+ getExtensions() { return this.base.getExtensions(); }
176
+ getSkills() { return this.base.getSkills(); }
177
+ getPrompts() { return this.base.getPrompts(); }
178
+ getThemes() { return this.base.getThemes(); }
179
+ getAgentsFiles() { return this.base.getAgentsFiles(); }
180
+ getSystemPrompt() { return this.systemPromptOverride ?? this.base.getSystemPrompt(); }
181
+ getAppendSystemPrompt() { return this.base.getAppendSystemPrompt(); }
182
+ extendResources(paths: Parameters<ResourceLoader["extendResources"]>[0]): void { this.base.extendResources(paths); }
183
+ reload(): Promise<void> { return this.base.reload(); }
184
+ }
185
+
186
+ function isLoaderWarm(cwd: string, agentDir: string, noExtensions: boolean): boolean {
187
+ const entry = _loaderPool.get(loaderPoolKey(cwd, agentDir, noExtensions));
188
+ return !!entry && entry.idle.length > 0;
189
+ }
190
+
191
+ async function allowUiPaint(coldLoader: boolean): Promise<void> {
192
+ await new Promise<void>((resolve) => setImmediate(resolve));
193
+ if (!coldLoader) return;
194
+ // Give pi's TUI render timer a real timers-phase turn before CPU-heavy extension loading.
195
+ await new Promise<void>((resolve) => setTimeout(resolve, 50));
196
+ await new Promise<void>((resolve) => setImmediate(resolve));
197
+ }
198
+
199
+ async function acquireResourceLoader(
200
+ cwd: string,
201
+ agentDir: string,
202
+ noExtensions: boolean,
203
+ systemPromptOverride: string | undefined,
204
+ ): Promise<LoaderLease> {
205
+ const entry = getLoaderPoolEntry(cwd, agentDir, noExtensions);
206
+
207
+ while (true) {
208
+ const cached = entry.idle.pop();
209
+ if (cached) {
210
+ entry.active.add(cached);
211
+ let released = false;
212
+ return {
213
+ loader: new AgentPromptResourceLoader(cached, systemPromptOverride),
214
+ release: () => {
215
+ if (released) return;
216
+ released = true;
217
+ entry.active.delete(cached);
218
+ entry.idle.push(cached);
219
+ },
220
+ };
221
+ }
222
+
223
+ const warming = entry.warming.values().next().value as Promise<void> | undefined;
224
+ if (warming) {
225
+ await warming;
226
+ continue;
227
+ }
228
+
229
+ const loader = new DefaultResourceLoader(makeLoaderOptions(cwd, agentDir, noExtensions));
230
+ const warmPromise = loader.reload()
231
+ .then(() => { entry.idle.push(loader); })
232
+ .finally(() => { entry.warming.delete(warmPromise); });
233
+ entry.warming.add(warmPromise);
234
+ await warmPromise;
235
+ }
236
+ }
237
+
238
+ function warmResourceLoader(cwd: string, agentDir: string, noExtensions: boolean): void {
239
+ const entry = getLoaderPoolEntry(cwd, agentDir, noExtensions);
240
+ if (entry.idle.length > 0 || entry.active.size > 0 || entry.warming.size > 0) return;
241
+ const loader = new DefaultResourceLoader(makeLoaderOptions(cwd, agentDir, noExtensions));
242
+ const warmPromise = loader.reload()
243
+ .then(() => { entry.idle.push(loader); })
244
+ .catch(() => { /* ignore warm failures; foreground call reports real error */ })
245
+ .finally(() => { entry.warming.delete(warmPromise); });
246
+ entry.warming.add(warmPromise);
247
+ }
248
+
128
249
  // ─── Foreground detach registry ───────────────────────────────────────────────
129
250
 
130
251
  interface ForegroundDetachEntry {
@@ -136,8 +257,9 @@ const _fgJobs = new Map<string, ForegroundDetachEntry>();
136
257
 
137
258
  // ─── In-process runner ───────────────────────────────────────────────────────
138
259
 
139
- const MAX_DEPTH = 2;
260
+ const DEFAULT_MAX_DEPTH = 0;
140
261
  const DEPTH_ENV = "PI_FAST_SUBAGENT_DEPTH";
262
+ const MAX_DEPTH_ENV = "PI_FAST_SUBAGENT_MAX_DEPTH";
141
263
 
142
264
  interface ToolCallEntry {
143
265
  id: string;
@@ -168,6 +290,7 @@ interface AgentRowStatus {
168
290
 
169
291
  interface SubagentDetails {
170
292
  mode?: "single" | "parallel";
293
+ agentName?: string;
171
294
  task?: string;
172
295
  // parallel
173
296
  parallelAgents?: AgentRowStatus[];
@@ -208,8 +331,9 @@ function formatBgJobDetails(job: BackgroundSubagentJob, now = Date.now()): strin
208
331
  return lines.join("\n");
209
332
  }
210
333
 
211
- // Module-level depth counter avoids process.env race conditions in parallel mode
334
+ // Module-level depth counters for nested in-process subagent calls.
212
335
  let _currentDepth = 0;
336
+ let _currentMaxDepth = DEFAULT_MAX_DEPTH;
213
337
 
214
338
  async function runAgent(
215
339
  agent: AgentConfig,
@@ -221,46 +345,70 @@ async function runAgent(
221
345
  parentDepth?: number,
222
346
  ): Promise<RunResult> {
223
347
  const depth = parentDepth ?? _currentDepth;
224
- if (depth >= MAX_DEPTH) {
348
+ const isNestedCall = depth > 0;
349
+ if (isNestedCall && depth > _currentMaxDepth) {
225
350
  return {
226
351
  output: "",
227
352
  exitCode: 1,
228
- 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.`,
229
354
  toolCalls: [],
230
355
  usage: { input: 0, output: 0, cost: 0, turns: 0 },
231
356
  };
232
357
  }
233
358
 
359
+ const bootStartedAt = Date.now();
234
360
  const { authStorage, modelRegistry } = getAuth();
235
361
  const agentDir = getAgentDir();
362
+ const noExtensions = !agentNeedsExtensions(agent.tools);
363
+ const coldLoader = !isLoaderWarm(cwd, agentDir, noExtensions);
364
+
365
+ // Fire an immediate "running" emit so the UI draws the agent header + prompt
366
+ // before the (potentially slow) extension/session load. Without this, pi looks
367
+ // frozen while `loader.reload()` and `createAgentSession()` are in flight.
368
+ onUpdate?.({
369
+ content: [{ type: "text", text: "" }],
370
+ details: {
371
+ agentName: agent.name,
372
+ task,
373
+ usage: { input: 0, output: 0, cost: 0, turns: 0 },
374
+ running: true,
375
+ elapsedMs: 0,
376
+ model: modelOverride ?? agent.model,
377
+ toolCalls: [],
378
+ } satisfies SubagentDetails,
379
+ });
380
+ // Yield through timers when loader is cold so pi's render loop paints before
381
+ // CPU-heavy extension loading runs.
382
+ await allowUiPaint(coldLoader);
236
383
 
237
- // Build resource loader — no extensions/context files to keep subagent lean.
238
- // Agents can opt in to extensions via `extensions: true` in frontmatter, which
239
- // makes tools like web_search / fetch_content / mcp / etc. available to the
240
- // subagent (subject to the optional `tools:` allowlist below).
241
- const loaderOptions: DefaultResourceLoaderOptions = {
384
+ const loaderLease = await acquireResourceLoader(
242
385
  cwd,
243
386
  agentDir,
244
- noExtensions: !agentNeedsExtensions(agent.tools),
245
- noContextFiles: true,
246
- noSkills: true,
247
- };
248
- if (agent.systemPrompt) {
249
- // Replace pi's base system prompt with the agent's own prompt
250
- loaderOptions.systemPromptOverride = () => agent.systemPrompt;
251
- }
252
-
253
- const loader = new DefaultResourceLoader(loaderOptions);
254
- await loader.reload();
387
+ noExtensions,
388
+ agent.systemPrompt || undefined,
389
+ );
255
390
 
256
- const { session } = await createAgentSession({
257
- cwd,
258
- agentDir,
259
- sessionManager: SessionManager.inMemory(cwd),
260
- authStorage,
261
- modelRegistry,
262
- resourceLoader: loader,
263
- });
391
+ let session: Awaited<ReturnType<typeof createAgentSession>>["session"];
392
+ try {
393
+ const created = await createAgentSession({
394
+ cwd,
395
+ agentDir,
396
+ sessionManager: SessionManager.inMemory(cwd),
397
+ authStorage,
398
+ modelRegistry,
399
+ resourceLoader: loaderLease.loader,
400
+ });
401
+ session = created.session;
402
+ } catch (e) {
403
+ loaderLease.release();
404
+ return {
405
+ output: "",
406
+ exitCode: 1,
407
+ error: e instanceof Error ? e.message : String(e),
408
+ toolCalls: [],
409
+ usage: { input: 0, output: 0, cost: 0, turns: 0 },
410
+ };
411
+ }
264
412
 
265
413
  // Resolve and apply model
266
414
  const modelStr = modelOverride ?? agent.model;
@@ -288,7 +436,7 @@ async function runAgent(
288
436
  let lastOutput = "";
289
437
  let currentDelta = "";
290
438
  let detectedModel: string | undefined;
291
- const startedAt = Date.now();
439
+ const startedAt = bootStartedAt;
292
440
  const configuredModel = modelOverride ?? agent.model;
293
441
  const toolCalls: ToolCallEntry[] = [];
294
442
  const toolStartTimes = new Map<string, number>();
@@ -300,6 +448,7 @@ async function runAgent(
300
448
  onUpdate?.({
301
449
  content: [{ type: "text", text: currentDelta || lastOutput || "" }],
302
450
  details: {
451
+ agentName: agent.name,
303
452
  task,
304
453
  usage,
305
454
  running: true,
@@ -397,10 +546,17 @@ async function runAgent(
397
546
  });
398
547
  });
399
548
 
400
- // 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.
401
551
  const prevEnvDepth = process.env[DEPTH_ENV];
402
- 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);
403
556
  _currentDepth = depth + 1;
557
+ _currentMaxDepth = depth + maxDepth;
558
+ process.env[DEPTH_ENV] = String(_currentDepth);
559
+ process.env[MAX_DEPTH_ENV] = String(_currentMaxDepth);
404
560
 
405
561
  let exitCode = 0;
406
562
  let error: string | undefined;
@@ -423,9 +579,13 @@ async function runAgent(
423
579
  clearInterval(heartbeat);
424
580
  unsubscribe();
425
581
  session.dispose();
582
+ loaderLease.release();
426
583
  if (prevEnvDepth === undefined) delete process.env[DEPTH_ENV];
427
584
  else process.env[DEPTH_ENV] = prevEnvDepth;
428
- _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;
429
589
  }
430
590
 
431
591
  return { output: lastOutput, exitCode, error, model: detectedModel, toolCalls, usage };
@@ -558,12 +718,21 @@ export default function (pi: ExtensionAPI) {
558
718
 
559
719
  pi.on("session_start", async (_event, ctx) => {
560
720
  _setBgStatus = (text) => ctx.ui.setStatus(BG_STATUS_KEY, text);
721
+
722
+ // Warm one extension-capable loader after startup. First `tools: all` subagent
723
+ // call can then reuse loaded extensions instead of blocking before first stream.
724
+ if (process.env.PI_FAST_SUBAGENT_WARM !== "0") {
725
+ const warmCwd = ctx.cwd;
726
+ const warmAgentDir = getAgentDir();
727
+ setTimeout(() => warmResourceLoader(warmCwd, warmAgentDir, false), 1000);
728
+ }
561
729
  });
562
730
 
563
731
  pi.on("session_shutdown", async () => {
564
732
  getBgManager().shutdown();
565
733
  _bgManager = null;
566
734
  _setBgStatus = null;
735
+ _loaderPool.clear();
567
736
  });
568
737
 
569
738
  // ─── Ctrl+Shift+B — move foreground subagent to background ─────────────────────────
@@ -613,6 +782,7 @@ export default function (pi: ExtensionAPI) {
613
782
  `Description: ${agent.description}`,
614
783
  agent.model ? `Model: ${agent.model}` : "",
615
784
  `Tools: ${formatTools(agent.tools)}`,
785
+ `Max subagent depth: ${agent.maxDepth}`,
616
786
  agent.systemPrompt ? `\nSystem prompt:\n${agent.systemPrompt}` : "",
617
787
  ].filter(Boolean).join("\n");
618
788
  ctx.ui.notify(lines, "info");
@@ -625,7 +795,7 @@ export default function (pi: ExtensionAPI) {
625
795
  "Add .md files to:\n" +
626
796
  " ~/.pi/agent/agents/ (user-level)\n" +
627
797
  " .pi/agents/ (project-level)\n" +
628
- "\nFrontmatter required: name, description. Optional: model, tools.",
798
+ "\nFrontmatter required: name, description. Optional: model, tools, maxDepth.",
629
799
  "info"
630
800
  );
631
801
  return;
@@ -866,14 +1036,15 @@ export default function (pi: ExtensionAPI) {
866
1036
 
867
1037
  function statusLine(): string {
868
1038
  if (details.backgroundJobId) return `moved to background · ${details.backgroundJobId}`;
1039
+ const prefix = details.agentName ? `${theme.fg("toolTitle", details.agentName)} · ` : "";
869
1040
  if (details.running) {
870
1041
  const parts: string[] = ["running"];
871
1042
  if (details.usage?.turns) parts.push(`${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}`);
872
1043
  if (details.elapsedMs != null) parts.push(formatDuration(details.elapsedMs));
873
1044
  if (details.model) parts.push(details.model);
874
- return parts.join(" · ");
1045
+ return prefix + parts.join(" · ");
875
1046
  }
876
- return formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
1047
+ return prefix + formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
877
1048
  }
878
1049
 
879
1050
  // Name(arg) ✓ 0.3s or Name(arg) (dim, still running)
@@ -904,6 +1075,8 @@ export default function (pi: ExtensionAPI) {
904
1075
  render(width: number): string[] {
905
1076
  const out: string[] = [];
906
1077
  const indent = " ";
1078
+ const ellipsisLine = (count: number) =>
1079
+ theme.fg("muted", `${indent}… (${count} more line${count === 1 ? "" : "s"})`);
907
1080
 
908
1081
  // ── Prompt ────────────────────────────────────────────────────
909
1082
  if (details.task) {
@@ -913,14 +1086,22 @@ export default function (pi: ExtensionAPI) {
913
1086
  for (const w of wrapLine(indent + line, width)) out.push(w);
914
1087
  }
915
1088
  } else {
916
- // Up to 8 visual lines in collapsed mode
1089
+ // Up to 8 visual lines from the HEAD of the prompt (keep opening, not tail).
917
1090
  const PROMPT_PREVIEW_LINES = 8;
918
1091
  if (cache.width !== width || cache.promptLines === undefined) {
919
- const preview = truncateToVisualLines(details.task, PROMPT_PREVIEW_LINES, width - indent.length);
920
- cache.promptLines = preview.visualLines.map((l) => truncateToWidth(indent + l, width, "..."));
921
- cache.promptSkipped = preview.skippedCount;
1092
+ const innerWidth = Math.max(1, width - indent.length);
1093
+ const allVisual: string[] = [];
1094
+ for (const raw of details.task.split("\n")) {
1095
+ for (const w of wrapLine(raw, innerWidth)) allVisual.push(w);
1096
+ }
1097
+ const head = allVisual.slice(0, PROMPT_PREVIEW_LINES);
1098
+ cache.promptLines = head.map((l) => truncateToWidth(indent + l, width, "..."));
1099
+ cache.promptSkipped = Math.max(0, allVisual.length - head.length);
922
1100
  }
923
1101
  out.push(...cache.promptLines);
1102
+ if ((cache.promptSkipped ?? 0) > 0) {
1103
+ out.push(truncateToWidth(ellipsisLine(cache.promptSkipped!), width, "..."));
1104
+ }
924
1105
  }
925
1106
  }
926
1107
 
@@ -950,6 +1131,10 @@ export default function (pi: ExtensionAPI) {
950
1131
  cache.skipped = preview.skippedCount;
951
1132
  cache.width = width;
952
1133
  }
1134
+ // truncateToVisualLines keeps the tail — show ellipsis BEFORE the visible lines.
1135
+ if ((cache.skipped ?? 0) > 0) {
1136
+ out.push(truncateToWidth(ellipsisLine(cache.skipped!), width, "..."));
1137
+ }
953
1138
  out.push(...(cache.responseLines ?? []));
954
1139
  }
955
1140
  }
@@ -1010,6 +1195,7 @@ export default function (pi: ExtensionAPI) {
1010
1195
  `**Description:** ${agent.description}`,
1011
1196
  agent.model ? `**Model:** ${agent.model}` : null,
1012
1197
  `**Tools:** ${formatTools(agent.tools)}`,
1198
+ `**Max subagent depth:** ${agent.maxDepth}`,
1013
1199
  agent.systemPrompt ? `\n**System prompt:**\n${agent.systemPrompt}` : null,
1014
1200
  ].filter(Boolean).join("\n");
1015
1201
  return { content: [{ type: "text", text: info }] };
@@ -1129,6 +1315,7 @@ export default function (pi: ExtensionAPI) {
1129
1315
  return {
1130
1316
  content: [{ type: "text", text: `Moved to background: ${bgJobId}. Completion will be announced automatically.` }],
1131
1317
  details: {
1318
+ agentName: params.agent,
1132
1319
  task: params.task,
1133
1320
  usage: { input: 0, output: 0, cost: 0, turns: 0 },
1134
1321
  running: false,
@@ -1142,6 +1329,7 @@ export default function (pi: ExtensionAPI) {
1142
1329
  return {
1143
1330
  content: [{ type: "text", text: getFinalText(result) }],
1144
1331
  details: {
1332
+ agentName: params.agent,
1145
1333
  task: params.task,
1146
1334
  usage: result.usage,
1147
1335
  running: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fast-subagent",
3
- "version": "0.6.1",
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": [