pi-fast-subagent 0.7.0 → 0.9.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/index.ts CHANGED
@@ -4,117 +4,39 @@
4
4
  * Uses createAgentSession() to run subagents in the same process as pi —
5
5
  * no subprocess spawn, no cold-start overhead.
6
6
  *
7
- * Supports: single, parallel.
7
+ * Supports: single, parallel, background.
8
8
  * Agent .md files are compatible with pi-subagents frontmatter format.
9
9
  */
10
10
 
11
11
  import { randomUUID } from "node:crypto";
12
- import type {
13
- AgentToolResult,
14
- AgentToolUpdateCallback,
15
- ExtensionAPI,
16
- ExtensionContext,
17
- ResourceLoader,
18
- ToolRenderResultOptions,
19
- } from "@mariozechner/pi-coding-agent";
12
+ import { getAgentDir } from "@mariozechner/pi-coding-agent";
13
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolRenderResultOptions } from "@mariozechner/pi-coding-agent";
14
+ import { Theme } from "@mariozechner/pi-coding-agent";
15
+ import { Key } from "@mariozechner/pi-tui";
16
+
17
+ import { type AgentConfig, discoverAgents } from "./agents.js";
20
18
  import { BackgroundJobManager } from "./background-job-manager.js";
21
19
  import type { BackgroundHandleLike, BackgroundJobResult, BackgroundSubagentJob } from "./background-types.js";
22
- import { Theme } from "@mariozechner/pi-coding-agent";
23
- import { Key, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
24
- import { truncateToVisualLines, keyHint } from "@mariozechner/pi-coding-agent";
25
20
  import {
26
- AuthStorage,
27
- createAgentSession,
28
- DefaultResourceLoader,
29
- getAgentDir,
30
- ModelRegistry,
31
- SessionManager,
32
- } from "@mariozechner/pi-coding-agent";
33
-
34
- type DefaultResourceLoaderOptions = ConstructorParameters<typeof DefaultResourceLoader>[0];
35
- import { Type } from "@sinclair/typebox";
36
- import { type AgentConfig, agentNeedsExtensions, discoverAgents } from "./agents.js";
37
-
38
- function formatTools(tools: AgentConfig["tools"]): string {
39
- if (tools === "all") return "all";
40
- if (tools === "builtins") return "builtins (default)";
41
- if (tools === "none") return "none";
42
- return tools.join(", ");
43
- }
21
+ formatBgJobDetails,
22
+ formatBgJobSummary,
23
+ formatDuration,
24
+ formatTools,
25
+ getFinalText,
26
+ summarizeTask,
27
+ } from "./format.js";
28
+ import { defaultLoaderPool } from "./loader-pool.js";
29
+ import { renderSubagentResult } from "./render.js";
30
+ import { getCurrentDepth, mapConcurrent, runAgent } from "./runner.js";
31
+ import { SubagentParams } from "./schemas.js";
32
+ import type { AgentRowStatus, OnUpdate, RunResult, SubagentDetails, ToolCallEntry } from "./types.js";
33
+
34
+ // ─── Module-level state ─────────────────────────────────────────────────────
44
35
 
45
- // ─── Tool arg summarizer (compact one-liner per tool call) ─────────────────────
46
-
47
- function shortPath(p: unknown): string {
48
- if (typeof p !== "string") return "";
49
- const cwd = process.cwd();
50
- if (p.startsWith(cwd + "/")) return p.slice(cwd.length + 1);
51
- return p.replace(/^\/Users\/[^/]+\/[^/]+\//, "");
52
- }
53
-
54
- function summarizeToolArgs(toolName: unknown, toolInput: unknown): string {
55
- const name = String(toolName ?? "");
56
- const input =
57
- toolInput && typeof toolInput === "object" ? (toolInput as Record<string, unknown>) : {};
58
- const filePath = (): string => shortPath(input.path ?? input.file_path) || "";
59
- switch (name) {
60
- case "Read":
61
- case "read":
62
- case "Write":
63
- case "write":
64
- case "Edit":
65
- case "edit":
66
- return filePath();
67
- case "Bash":
68
- case "bash": {
69
- const cmd = String(input.command ?? "");
70
- return cmd.length > 80 ? cmd.slice(0, 77) + "..." : cmd;
71
- }
72
- case "Glob":
73
- case "glob":
74
- return String(input.pattern ?? "");
75
- case "find": {
76
- const pat = String(input.pattern ?? "");
77
- const p = shortPath(input.path);
78
- return p ? `${pat} in ${p}` : pat;
79
- }
80
- case "Grep":
81
- case "grep": {
82
- const pat = String(input.pattern ?? "");
83
- const g = input.glob ? ` ${input.glob}` : "";
84
- return `${pat}${g}`;
85
- }
86
- case "ls":
87
- return shortPath(input.path) || "";
88
- case "subagent": {
89
- const agent = String(input.agent ?? "");
90
- const t = String(input.task ?? "");
91
- const summary = t.length > 50 ? t.slice(0, 47) + "..." : t;
92
- return agent ? `${agent}: ${summary}` : summary;
93
- }
94
- default: {
95
- for (const v of Object.values(input)) {
96
- if (typeof v === "string" && v.length > 0)
97
- return v.length > 60 ? v.slice(0, 57) + "..." : v;
98
- }
99
- return "";
100
- }
101
- }
102
- }
103
-
104
- // ─── Shared auth (created once, reused across calls) ─────────────────────────
105
-
106
- let _authStorage: ReturnType<typeof AuthStorage.create> | null = null;
107
- let _modelRegistry: ReturnType<typeof ModelRegistry.create> | null = null;
108
36
  let _bgManager: BackgroundJobManager | null = null;
109
37
  let _onBgJobComplete: ((job: BackgroundSubagentJob) => void) | null = null;
110
38
  let _setBgStatus: ((text: string | undefined) => void) | null = null;
111
39
 
112
- function getAuth() {
113
- if (!_authStorage) _authStorage = AuthStorage.create();
114
- if (!_modelRegistry) _modelRegistry = ModelRegistry.create(_authStorage);
115
- return { authStorage: _authStorage, modelRegistry: _modelRegistry };
116
- }
117
-
118
40
  function getBgManager(): BackgroundJobManager {
119
41
  if (!_bgManager) _bgManager = new BackgroundJobManager({
120
42
  onJobComplete: (job) => _onBgJobComplete?.(job),
@@ -127,562 +49,21 @@ function refreshBgStatus(): void {
127
49
  _setBgStatus?.(running.length > 0 ? `⧗ ${running.length} bg agent${running.length > 1 ? "s" : ""}` : undefined);
128
50
  }
129
51
 
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
-
249
- // ─── Foreground detach registry ───────────────────────────────────────────────
52
+ // ─── Foreground detach registry ─────────────────────────────────────────────
250
53
 
251
54
  interface ForegroundDetachEntry {
252
55
  agentName: string;
253
56
  task: string;
254
- detach: () => string; // returns bg job id
57
+ detach: () => string;
255
58
  }
256
59
  const _fgJobs = new Map<string, ForegroundDetachEntry>();
257
60
 
258
- // ─── In-process runner ───────────────────────────────────────────────────────
259
-
260
- const MAX_DEPTH = 2;
261
- const DEPTH_ENV = "PI_FAST_SUBAGENT_DEPTH";
262
-
263
- interface ToolCallEntry {
264
- id: string;
265
- name: string;
266
- argSummary: string;
267
- result?: string;
268
- isError?: boolean;
269
- durMs?: number;
270
- }
271
-
272
- interface RunResult {
273
- output: string;
274
- exitCode: number;
275
- error?: string;
276
- model?: string;
277
- toolCalls: ToolCallEntry[];
278
- usage: { input: number; output: number; cost: number; turns: number };
279
- }
280
-
281
- interface AgentRowStatus {
282
- name: string;
283
- taskSummary: string;
284
- status: "pending" | "running" | "done" | "error";
285
- durMs?: number;
286
- toolCalls?: ToolCallEntry[];
287
- responseText?: string;
288
- }
289
-
290
- interface SubagentDetails {
291
- mode?: "single" | "parallel";
292
- agentName?: string;
293
- task?: string;
294
- // parallel
295
- parallelAgents?: AgentRowStatus[];
296
- usage: RunResult["usage"];
297
- running: boolean;
298
- elapsedMs?: number;
299
- model?: string;
300
- backgroundJobId?: string;
301
- toolCalls: ToolCallEntry[];
302
- }
303
-
304
- type OnUpdate = (partial: { content: [{ type: "text"; text: string }]; details: unknown }) => void;
305
-
306
- function formatDuration(ms: number): string {
307
- const s = Math.max(0, Math.floor(ms / 1000));
308
- const m = Math.floor(s / 60);
309
- const rem = s % 60;
310
- return m > 0 ? `${m}m ${rem}s` : `${rem}s`;
311
- }
312
-
313
- function summarizeTask(task: string, max = 60): string {
314
- return task.length > max ? task.slice(0, max - 3) + "..." : task;
315
- }
316
-
317
- function formatBgJobSummary(job: BackgroundSubagentJob, now = Date.now()): string {
318
- const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(now - job.startedAt);
319
- return `${job.id} [${job.status}] ${job.agentName} · ${dur} · ${summarizeTask(job.task)}`;
320
- }
321
-
322
- function formatBgJobDetails(job: BackgroundSubagentJob, now = Date.now()): string {
323
- const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(now - job.startedAt);
324
- const lines = [`${job.id} [${job.status}] ${job.agentName} · ${dur}`, `Task: ${job.task}`];
325
- if (job.model) lines.push(`Model: ${job.model}`);
326
- if (job.status === "completed") lines.push(`\nResult:\n${job.resultSummary ?? "(no output)"}`);
327
- if (job.status === "failed") lines.push(`\nError: ${job.error ?? "(unknown)"}`);
328
- if (job.status === "cancelled") lines.push("\nCancelled.");
329
- if (job.status === "running") lines.push("\nStill running.");
330
- return lines.join("\n");
331
- }
332
-
333
- // Module-level depth counter — avoids process.env race conditions in parallel mode
334
- let _currentDepth = 0;
335
-
336
- async function runAgent(
337
- agent: AgentConfig,
338
- task: string,
339
- cwd: string,
340
- modelOverride: string | undefined,
341
- signal: AbortSignal | undefined,
342
- onUpdate: OnUpdate | undefined,
343
- parentDepth?: number,
344
- ): Promise<RunResult> {
345
- const depth = parentDepth ?? _currentDepth;
346
- if (depth >= MAX_DEPTH) {
347
- return {
348
- output: "",
349
- exitCode: 1,
350
- error: `Max subagent depth (${MAX_DEPTH}) exceeded. Increase PI_FAST_SUBAGENT_DEPTH env to allow deeper nesting.`,
351
- toolCalls: [],
352
- usage: { input: 0, output: 0, cost: 0, turns: 0 },
353
- };
354
- }
355
-
356
- const bootStartedAt = Date.now();
357
- const { authStorage, modelRegistry } = getAuth();
358
- const agentDir = getAgentDir();
359
- const noExtensions = !agentNeedsExtensions(agent.tools);
360
- const coldLoader = !isLoaderWarm(cwd, agentDir, noExtensions);
361
-
362
- // Fire an immediate "running" emit so the UI draws the agent header + prompt
363
- // before the (potentially slow) extension/session load. Without this, pi looks
364
- // frozen while `loader.reload()` and `createAgentSession()` are in flight.
365
- onUpdate?.({
366
- content: [{ type: "text", text: "" }],
367
- details: {
368
- agentName: agent.name,
369
- task,
370
- usage: { input: 0, output: 0, cost: 0, turns: 0 },
371
- running: true,
372
- elapsedMs: 0,
373
- model: modelOverride ?? agent.model,
374
- toolCalls: [],
375
- } satisfies SubagentDetails,
376
- });
377
- // Yield through timers when loader is cold so pi's render loop paints before
378
- // CPU-heavy extension loading runs.
379
- await allowUiPaint(coldLoader);
380
-
381
- const loaderLease = await acquireResourceLoader(
382
- cwd,
383
- agentDir,
384
- noExtensions,
385
- agent.systemPrompt || undefined,
386
- );
387
-
388
- let session: Awaited<ReturnType<typeof createAgentSession>>["session"];
389
- try {
390
- const created = await createAgentSession({
391
- cwd,
392
- agentDir,
393
- sessionManager: SessionManager.inMemory(cwd),
394
- authStorage,
395
- modelRegistry,
396
- resourceLoader: loaderLease.loader,
397
- });
398
- session = created.session;
399
- } catch (e) {
400
- loaderLease.release();
401
- return {
402
- output: "",
403
- exitCode: 1,
404
- error: e instanceof Error ? e.message : String(e),
405
- toolCalls: [],
406
- usage: { input: 0, output: 0, cost: 0, turns: 0 },
407
- };
408
- }
409
-
410
- // Resolve and apply model
411
- const modelStr = modelOverride ?? agent.model;
412
- if (modelStr) {
413
- const [provider, ...rest] = modelStr.split("/");
414
- const modelId = rest.join("/");
415
- if (provider && modelId) {
416
- const model = modelRegistry.find(provider, modelId);
417
- if (model) await session.setModel(model);
418
- }
419
- }
420
-
421
- // Apply tools allowlist.
422
- // "all" → no restriction (everything registered stays active)
423
- // "none" → disable every tool
424
- // string[] → explicit allowlist
425
- if (agent.tools === "none") {
426
- session.setActiveToolsByName([]);
427
- } else if (Array.isArray(agent.tools) && agent.tools.length > 0) {
428
- session.setActiveToolsByName(agent.tools);
429
- }
430
-
431
- // Track output and usage
432
- const usage = { input: 0, output: 0, cost: 0, turns: 0 };
433
- let lastOutput = "";
434
- let currentDelta = "";
435
- let detectedModel: string | undefined;
436
- const startedAt = bootStartedAt;
437
- const configuredModel = modelOverride ?? agent.model;
438
- const toolCalls: ToolCallEntry[] = [];
439
- const toolStartTimes = new Map<string, number>();
440
-
441
- let done = false;
442
-
443
- function emitUpdate(): void {
444
- if (done) return;
445
- onUpdate?.({
446
- content: [{ type: "text", text: currentDelta || lastOutput || "" }],
447
- details: {
448
- agentName: agent.name,
449
- task,
450
- usage,
451
- running: true,
452
- elapsedMs: Date.now() - startedAt,
453
- model: detectedModel ?? configuredModel,
454
- toolCalls: [...toolCalls],
455
- } satisfies SubagentDetails,
456
- });
457
- }
458
-
459
- emitUpdate();
460
-
461
- const heartbeat = setInterval(emitUpdate, 1000);
462
-
463
- const unsubscribe = session.subscribe((event: any) => {
464
- // Stream tool execution events
465
- if (event.type === "tool_execution_start") {
466
- toolStartTimes.set(event.toolCallId, Date.now());
467
- toolCalls.push({
468
- id: event.toolCallId,
469
- name: event.toolName,
470
- argSummary: summarizeToolArgs(event.toolName, event.args),
471
- });
472
- emitUpdate();
473
- return;
474
- }
475
-
476
- if (event.type === "tool_execution_end") {
477
- const startedAtTool = toolStartTimes.get(event.toolCallId);
478
- toolStartTimes.delete(event.toolCallId);
479
- const resultText: string = (event.result?.content ?? [])
480
- .filter((p: any) => p.type === "text")
481
- .map((p: any) => p.text as string)
482
- .join("\n");
483
- let entry: ToolCallEntry | undefined;
484
- for (let i = toolCalls.length - 1; i >= 0; i--) {
485
- if (toolCalls[i]!.id === event.toolCallId) { entry = toolCalls[i]; break; }
486
- }
487
- if (!entry) {
488
- for (let i = toolCalls.length - 1; i >= 0; i--) {
489
- if (toolCalls[i]!.name === event.toolName && toolCalls[i]!.result === undefined) { entry = toolCalls[i]; break; }
490
- }
491
- }
492
- if (entry) {
493
- entry.result = resultText;
494
- entry.isError = event.isError;
495
- entry.durMs = startedAtTool != null ? Date.now() - startedAtTool : undefined;
496
- }
497
- emitUpdate();
498
- return;
499
- }
500
-
501
- // Stream text deltas live to the UI
502
- if (event.type === "message_update") {
503
- const e = event.assistantMessageEvent;
504
- if (e?.type === "text_delta" && e.delta) {
505
- currentDelta += e.delta;
506
- emitUpdate();
507
- }
508
- return;
509
- }
510
-
511
- if (event.type !== "message_end" || !event.message) return;
512
- const msg = event.message;
513
- if (msg.role !== "assistant") return; // usage/model only tracked for assistant turns
514
-
515
- usage.turns++;
516
- const u = msg.usage;
517
- if (u) {
518
- usage.input += u.input ?? 0;
519
- usage.output += u.output ?? 0;
520
- usage.cost += u.cost?.total ?? 0;
521
- }
522
- if (msg.model) detectedModel = msg.model;
523
-
524
- // Extract last text content
525
- for (const part of msg.content ?? []) {
526
- if (part.type === "text") {
527
- lastOutput = part.text;
528
- break;
529
- }
530
- }
531
- // Reset delta accumulator for next turn
532
- currentDelta = "";
533
-
534
- onUpdate?.({
535
- content: [{ type: "text", text: lastOutput || "(running...)" }],
536
- details: {
537
- agent: agent.name,
538
- usage,
539
- running: true,
540
- elapsedMs: Date.now() - startedAt,
541
- model: detectedModel ?? configuredModel,
542
- },
543
- });
544
- });
545
-
546
- // Propagate depth to nested calls — use module counter (safe for parallel) + env for subprocess compat
547
- const prevEnvDepth = process.env[DEPTH_ENV];
548
- process.env[DEPTH_ENV] = String(depth + 1);
549
- _currentDepth = depth + 1;
550
-
551
- let exitCode = 0;
552
- let error: string | undefined;
553
-
554
- try {
555
- if (signal?.aborted) throw new Error("Aborted");
556
-
557
- const onAbort = () => void session.abort();
558
- signal?.addEventListener("abort", onAbort, { once: true });
559
- try {
560
- await session.prompt(task);
561
- } finally {
562
- signal?.removeEventListener("abort", onAbort);
563
- }
564
- } catch (e) {
565
- exitCode = 1;
566
- error = signal?.aborted ? "Aborted" : e instanceof Error ? e.message : String(e);
567
- } finally {
568
- done = true;
569
- clearInterval(heartbeat);
570
- unsubscribe();
571
- session.dispose();
572
- loaderLease.release();
573
- if (prevEnvDepth === undefined) delete process.env[DEPTH_ENV];
574
- else process.env[DEPTH_ENV] = prevEnvDepth;
575
- _currentDepth = depth;
576
- }
577
-
578
- return { output: lastOutput, exitCode, error, model: detectedModel, toolCalls, usage };
579
- }
580
-
581
- // ─── Helpers ─────────────────────────────────────────────────────────────────
582
-
583
- async function mapConcurrent<TIn, TOut>(
584
- items: TIn[],
585
- concurrency: number,
586
- fn: (item: TIn, i: number) => Promise<TOut>,
587
- ): Promise<TOut[]> {
588
- if (!items.length) return [];
589
- const limit = Math.max(1, Math.min(concurrency, items.length));
590
- const results: TOut[] = new Array(items.length);
591
- let next = 0;
592
- await Promise.all(
593
- Array.from({ length: limit }, async () => {
594
- while (true) {
595
- const i = next++;
596
- if (i >= items.length) return;
597
- results[i] = await fn(items[i], i);
598
- }
599
- }),
600
- );
601
- return results;
602
- }
603
-
604
- function formatTokens(n: number): string {
605
- if (n < 1000) return String(n);
606
- if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
607
- return `${Math.round(n / 1000)}k`;
608
- }
609
-
610
- function formatUsage(usage: RunResult["usage"], model?: string): string {
611
- const parts: string[] = [];
612
- if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
613
- if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
614
- if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
615
- if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
616
- if (model) parts.push(model);
617
- return parts.join(" ");
618
- }
619
-
620
- function getFinalText(r: RunResult): string {
621
- if (r.exitCode !== 0) return `Error: ${r.error ?? r.output ?? "(no output)"}`;
622
- return r.output || "(no output)";
623
- }
624
-
625
- // ─── Tool schemas ─────────────────────────────────────────────────────────────
626
-
627
- const TaskItem = Type.Object({
628
- agent: Type.String({ description: "Agent name" }),
629
- task: Type.String({ description: "Task to delegate" }),
630
- model: Type.Optional(Type.String({ description: "Model override (provider/model)" })),
631
- cwd: Type.Optional(Type.String({ description: "Working directory" })),
632
- count: Type.Optional(Type.Number({ description: "Repeat this task N times" })),
633
- });
634
-
635
- const SubagentParams = Type.Object({
636
- // Single mode
637
- agent: Type.Optional(Type.String({ description: "Agent name (single mode)" })),
638
- task: Type.Optional(Type.String({ description: "Task (single mode)" })),
639
- model: Type.Optional(Type.String({ description: "Model override (single mode)" })),
640
- cwd: Type.Optional(Type.String({ description: "Working directory" })),
641
-
642
- // Parallel mode
643
- tasks: Type.Optional(
644
- Type.Array(TaskItem, {
645
- description: "Array of {agent, task} for parallel execution. Use count to repeat one task.",
646
- }),
647
- ),
648
- concurrency: Type.Optional(
649
- Type.Number({ description: "Max parallel concurrency (default: 4)", default: 4 }),
650
- ),
651
-
652
- // Background
653
- background: Type.Optional(Type.Boolean({ description: "Run in background, returns job ID immediately" })),
654
- jobId: Type.Optional(Type.String({ description: "Job ID for poll/cancel" })),
655
-
656
- // Management
657
- action: Type.Optional(
658
- Type.Union(
659
- [
660
- Type.Literal("list"),
661
- Type.Literal("get"),
662
- Type.Literal("status"),
663
- Type.Literal("poll"),
664
- Type.Literal("cancel"),
665
- Type.Literal("detach"),
666
- ],
667
- { description: "'list'/'get' for agents, 'status' for bg jobs, 'poll'/'cancel' for a specific job, 'detach' to move a foreground job to background" },
668
- ),
669
- ),
670
- agentScope: Type.Optional(
671
- Type.Union(
672
- [Type.Literal("user"), Type.Literal("project"), Type.Literal("both")],
673
- { description: "Agent scope filter", default: "both" },
674
- ),
675
- ),
676
- });
677
-
678
- // ─── Extension entry point ────────────────────────────────────────────────────
61
+ // ─── Extension entry point ──────────────────────────────────────────────────
679
62
 
680
63
  export default function (pi: ExtensionAPI) {
681
- // ─── Status keys ────────────────────────────────────────────────────────────────────
682
64
  const BG_STATUS_KEY = "fast-subagent-bg";
683
65
  const FG_STATUS_KEY = "fast-subagent-fg";
684
66
 
685
- // ─── Background job lifecycle ─────────────────────────────────────────────────────
686
67
  _onBgJobComplete = (job) => {
687
68
  refreshBgStatus();
688
69
  const elapsed = job.completedAt ? ((job.completedAt - job.startedAt) / 1000).toFixed(1) : "?";
@@ -706,12 +87,12 @@ export default function (pi: ExtensionAPI) {
706
87
  pi.on("session_start", async (_event, ctx) => {
707
88
  _setBgStatus = (text) => ctx.ui.setStatus(BG_STATUS_KEY, text);
708
89
 
709
- // Warm one extension-capable loader after startup. First `tools: all` subagent
710
- // call can then reuse loaded extensions instead of blocking before first stream.
90
+ // Warm one extension-capable loader after startup so first `tools: all`
91
+ // subagent call reuses loaded extensions instead of blocking.
711
92
  if (process.env.PI_FAST_SUBAGENT_WARM !== "0") {
712
93
  const warmCwd = ctx.cwd;
713
94
  const warmAgentDir = getAgentDir();
714
- setTimeout(() => warmResourceLoader(warmCwd, warmAgentDir, false), 1000);
95
+ setTimeout(() => defaultLoaderPool.warm(warmCwd, warmAgentDir, false), 1000);
715
96
  }
716
97
  });
717
98
 
@@ -719,10 +100,10 @@ export default function (pi: ExtensionAPI) {
719
100
  getBgManager().shutdown();
720
101
  _bgManager = null;
721
102
  _setBgStatus = null;
722
- _loaderPool.clear();
103
+ defaultLoaderPool.clear();
723
104
  });
724
105
 
725
- // ─── Ctrl+Shift+B — move foreground subagent to background ─────────────────────────
106
+ // ─── Ctrl+Shift+B — detach foreground subagent ────────────────────────────
726
107
  pi.registerShortcut(Key.ctrlShift("b"), {
727
108
  description: "Move foreground subagent to background",
728
109
  handler: async (ctx) => {
@@ -743,7 +124,7 @@ export default function (pi: ExtensionAPI) {
743
124
  },
744
125
  });
745
126
 
746
- // ─── /agent slash command ─────────────────────────────────────────────────
127
+ // ─── /fast-subagent:agent ─────────────────────────────────────────────────
747
128
  pi.registerCommand("fast-subagent:agent", {
748
129
  description: "List available subagents. Usage: /fast-subagent:agent [name] — show details for a specific agent.",
749
130
  getArgumentCompletions(prefix: string) {
@@ -769,6 +150,7 @@ export default function (pi: ExtensionAPI) {
769
150
  `Description: ${agent.description}`,
770
151
  agent.model ? `Model: ${agent.model}` : "",
771
152
  `Tools: ${formatTools(agent.tools)}`,
153
+ `Max subagent depth: ${agent.maxDepth}`,
772
154
  agent.systemPrompt ? `\nSystem prompt:\n${agent.systemPrompt}` : "",
773
155
  ].filter(Boolean).join("\n");
774
156
  ctx.ui.notify(lines, "info");
@@ -781,8 +163,8 @@ export default function (pi: ExtensionAPI) {
781
163
  "Add .md files to:\n" +
782
164
  " ~/.pi/agent/agents/ (user-level)\n" +
783
165
  " .pi/agents/ (project-level)\n" +
784
- "\nFrontmatter required: name, description. Optional: model, tools.",
785
- "info"
166
+ "\nFrontmatter required: name, description. Optional: model, tools, maxDepth.",
167
+ "info",
786
168
  );
787
169
  return;
788
170
  }
@@ -793,15 +175,11 @@ export default function (pi: ExtensionAPI) {
793
175
  const lines: string[] = [`Agents (${agents.length}):`];
794
176
  if (projectAgents.length) {
795
177
  lines.push("\nProject (.pi/agents/):");
796
- for (const a of projectAgents) {
797
- lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
798
- }
178
+ for (const a of projectAgents) lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
799
179
  }
800
180
  if (userAgents.length) {
801
181
  lines.push("\nUser (~/.pi/agent/agents/):");
802
- for (const a of userAgents) {
803
- lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
804
- }
182
+ for (const a of userAgents) lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
805
183
  }
806
184
  lines.push("");
807
185
  lines.push("Tip: /fast-subagent:agent <name> for details · Add .md files to .pi/agents/ to create new agents");
@@ -809,7 +187,7 @@ export default function (pi: ExtensionAPI) {
809
187
  },
810
188
  });
811
189
 
812
- // ─── /bg slash command ────────────────────────────────────────────────────
190
+ // ─── /fast-subagent:bg ────────────────────────────────────────────────────
813
191
  pi.registerCommand("fast-subagent:bg", {
814
192
  description: "Move a running foreground subagent to background. Shortcut: Ctrl+Shift+B. Usage: /fast-subagent:bg [fg-job-id] — omit ID to list active foreground jobs.",
815
193
  getArgumentCompletions(_prefix: string) {
@@ -835,14 +213,11 @@ export default function (pi: ExtensionAPI) {
835
213
  return;
836
214
  }
837
215
  const bgJobId = entry.detach();
838
- ctx.ui.notify(
839
- `Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.`,
840
- "info",
841
- );
216
+ ctx.ui.notify(`Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.`, "info");
842
217
  },
843
218
  });
844
219
 
845
- // ─── /bg-status slash command ─────────────────────────────────────────────
220
+ // ─── /fast-subagent:bg-status ─────────────────────────────────────────────
846
221
  pi.registerCommand("fast-subagent:bg-status", {
847
222
  description: "Show active background subagents. Usage: /fast-subagent:bg-status [sa-job-id] — omit ID to open selector.",
848
223
  getArgumentCompletions(prefix: string) {
@@ -882,7 +257,7 @@ export default function (pi: ExtensionAPI) {
882
257
  },
883
258
  });
884
259
 
885
- // ─── /bg-cancel slash command ─────────────────────────────────────────────
260
+ // ─── /fast-subagent:bg-cancel ─────────────────────────────────────────────
886
261
  pi.registerCommand("fast-subagent:bg-cancel", {
887
262
  description: "Cancel running background subagent. Usage: /fast-subagent:bg-cancel [sa-job-id] — omit ID to choose with arrow keys.",
888
263
  getArgumentCompletions(prefix: string) {
@@ -930,6 +305,7 @@ export default function (pi: ExtensionAPI) {
930
305
  },
931
306
  });
932
307
 
308
+ // ─── `subagent` tool ──────────────────────────────────────────────────────
933
309
  pi.registerTool({
934
310
  name: "subagent",
935
311
  label: "Subagent",
@@ -941,209 +317,11 @@ export default function (pi: ExtensionAPI) {
941
317
  ].join(" "),
942
318
  parameters: SubagentParams,
943
319
 
944
- renderResult(result: AgentToolResult<unknown>, { isPartial, expanded }: ToolRenderResultOptions, theme: Theme) {
945
- const agentText = result.content?.[0]?.type === "text" ? (result.content[0] as any).text as string : "";
946
- const details = (result.details ?? {}) as SubagentDetails;
947
- const toolCalls = details.toolCalls ?? [];
948
-
949
- // ── Parallel / Chain mode renders ────────────────────────────────
950
- if (details.mode === "parallel" && details.parallelAgents) {
951
- const agents = details.parallelAgents;
952
- const doneCount = agents.filter((a) => a.status === "done" || a.status === "error").length;
953
-
954
- function agentToolRow(t: ToolCallEntry): string {
955
- const arg = t.argSummary || "";
956
- const call = `${t.name}(${arg})`;
957
- if (t.result === undefined) return theme.fg("dim", call);
958
- const dur = t.durMs != null ? (t.durMs < 1000 ? ` ${t.durMs}ms` : ` ${(t.durMs / 1000).toFixed(1)}s`) : "";
959
- return `${call}${t.isError ? " ✗" : ` ✓${dur}`}`;
960
- }
961
-
962
- function wrapL(text: string, w: number): string[] {
963
- try { return wrapTextWithAnsi(text, w); } catch { return [truncateToWidth(text, w, "...")]; }
964
- }
965
-
966
- const cache: { width?: number } = {};
967
- return {
968
- invalidate() { cache.width = undefined; },
969
- render(width: number): string[] {
970
- const out: string[] = [];
971
- const header = details.running
972
- ? `Parallel (${doneCount}/${agents.length} done)`
973
- : `Parallel: ${agents.filter((a) => a.status === "done").length}/${agents.length} succeeded`;
974
- out.push(truncateToWidth(header, width, "..."));
975
-
976
- for (const a of agents) {
977
- const dur = a.durMs != null ? (a.durMs < 1000 ? ` ${a.durMs}ms` : ` ${(a.durMs / 1000).toFixed(1)}s`) : "";
978
- const mark = a.status === "pending" ? theme.fg("dim", "⋅") : a.status === "running" ? theme.fg("dim", "→") : a.status === "done" ? `✓${dur}` : `✗${dur}`;
979
-
980
- if (expanded) {
981
- // Full solo-style block per agent
982
- out.push("");
983
- out.push(truncateToWidth(`[${a.name}] ${mark}`, width, "..."));
984
- out.push(truncateToWidth(`Prompt:`, width, "..."));
985
- out.push(truncateToWidth(` ${a.taskSummary}`, width, "..."));
986
- for (const t of a.toolCalls ?? []) {
987
- out.push(truncateToWidth(agentToolRow(t), width, "..."));
988
- }
989
- if (a.responseText) {
990
- out.push("Response:");
991
- const preview = truncateToVisualLines(a.responseText, 6, width - 2);
992
- for (const l of preview.visualLines) out.push(truncateToWidth(" " + l, width, "..."));
993
- if (preview.skippedCount > 0) out.push(truncateToWidth(theme.fg("dim", ` … ${preview.skippedCount} more lines`), width, "..."));
994
- } else if (a.status === "running") {
995
- out.push(theme.fg("dim", " running..."));
996
- }
997
- } else {
998
- // Collapsed: compact one-liner
999
- const row = ` [${a.name}] ${mark} ${a.taskSummary}`;
1000
- out.push(truncateToWidth(row, width, "..."));
1001
- // Show tool call rows compactly
1002
- for (const t of a.toolCalls ?? []) {
1003
- out.push(truncateToWidth(` ${agentToolRow(t)}`, width, "..."));
1004
- }
1005
- if (a.responseText && (a.status === "done" || a.status === "error")) {
1006
- const preview = truncateToVisualLines(a.responseText, 2, width - 4);
1007
- for (const l of preview.visualLines) out.push(truncateToWidth(" " + l, width, "..."));
1008
- }
1009
- }
1010
- }
1011
-
1012
- out.push("");
1013
- const status = details.running
1014
- ? ["running", details.usage?.turns ? `${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}` : ""].filter(Boolean).join(" · ")
1015
- : formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
1016
- const expandHint = !expanded ? keyHint("app.tools.expand", "expand for full output") : "";
1017
- out.push(truncateToWidth([status, expandHint].filter(Boolean).join(" "), width, "..."));
1018
- return out;
1019
- },
1020
- };
1021
- }
1022
-
1023
- function statusLine(): string {
1024
- if (details.backgroundJobId) return `moved to background · ${details.backgroundJobId}`;
1025
- const prefix = details.agentName ? `${theme.fg("toolTitle", details.agentName)} · ` : "";
1026
- if (details.running) {
1027
- const parts: string[] = ["running"];
1028
- if (details.usage?.turns) parts.push(`${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}`);
1029
- if (details.elapsedMs != null) parts.push(formatDuration(details.elapsedMs));
1030
- if (details.model) parts.push(details.model);
1031
- return prefix + parts.join(" · ");
1032
- }
1033
- return prefix + formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
1034
- }
1035
-
1036
- // Name(arg) ✓ 0.3s or Name(arg) (dim, still running)
1037
- function toolRow(t: ToolCallEntry): string {
1038
- const arg = t.argSummary ? t.argSummary : "";
1039
- const call = `${t.name}(${arg})`;
1040
- if (t.result === undefined) return theme.fg("dim", call);
1041
- const dur = t.durMs != null
1042
- ? t.durMs < 1000 ? ` ${t.durMs}ms` : ` ${(t.durMs / 1000).toFixed(1)}s`
1043
- : "";
1044
- return `${call}${t.isError ? " ✗" : ` ✓${dur}`}`;
1045
- }
1046
-
1047
- function wrapLine(text: string, w: number): string[] {
1048
- try { return wrapTextWithAnsi(text, w); } catch { return [truncateToWidth(text, w, "...")]; }
1049
- }
1050
-
1051
- const cache: {
1052
- width?: number;
1053
- promptLines?: string[];
1054
- promptSkipped?: number;
1055
- responseLines?: string[];
1056
- skipped?: number;
1057
- } = {};
1058
-
1059
- return {
1060
- invalidate() { cache.width = undefined; },
1061
- render(width: number): string[] {
1062
- const out: string[] = [];
1063
- const indent = " ";
1064
- const ellipsisLine = (count: number) =>
1065
- theme.fg("muted", `${indent}… (${count} more line${count === 1 ? "" : "s"})`);
1066
-
1067
- // ── Prompt ────────────────────────────────────────────────────
1068
- if (details.task) {
1069
- out.push("Prompt:");
1070
- if (expanded) {
1071
- for (const line of details.task.split("\n")) {
1072
- for (const w of wrapLine(indent + line, width)) out.push(w);
1073
- }
1074
- } else {
1075
- // Up to 8 visual lines from the HEAD of the prompt (keep opening, not tail).
1076
- const PROMPT_PREVIEW_LINES = 8;
1077
- if (cache.width !== width || cache.promptLines === undefined) {
1078
- const innerWidth = Math.max(1, width - indent.length);
1079
- const allVisual: string[] = [];
1080
- for (const raw of details.task.split("\n")) {
1081
- for (const w of wrapLine(raw, innerWidth)) allVisual.push(w);
1082
- }
1083
- const head = allVisual.slice(0, PROMPT_PREVIEW_LINES);
1084
- cache.promptLines = head.map((l) => truncateToWidth(indent + l, width, "..."));
1085
- cache.promptSkipped = Math.max(0, allVisual.length - head.length);
1086
- }
1087
- out.push(...cache.promptLines);
1088
- if ((cache.promptSkipped ?? 0) > 0) {
1089
- out.push(truncateToWidth(ellipsisLine(cache.promptSkipped!), width, "..."));
1090
- }
1091
- }
1092
- }
1093
-
1094
- // ── Tool calls ─────────────────────────────────────────────
1095
- for (const t of toolCalls) {
1096
- out.push(truncateToWidth(toolRow(t), width, "..."));
1097
- if (expanded && t.result !== undefined) {
1098
- for (const line of t.result.split("\n")) {
1099
- for (const w of wrapLine(theme.fg("dim", indent + line), width)) out.push(w);
1100
- }
1101
- }
1102
- }
1103
-
1104
- // ── Response ────────────────────────────────────────────
1105
- const responseText = agentText || (isPartial ? "" : "");
1106
- if (responseText || isPartial) {
1107
- out.push("Response:");
1108
- if (expanded) {
1109
- for (const line of responseText.split("\n")) {
1110
- for (const w of wrapLine(indent + line, width)) out.push(w);
1111
- }
1112
- } else {
1113
- const PREVIEW_LINES = 6;
1114
- if (cache.width !== width) {
1115
- const preview = truncateToVisualLines(responseText, PREVIEW_LINES, width - indent.length);
1116
- cache.responseLines = preview.visualLines.map((l) => truncateToWidth(indent + l, width, "..."));
1117
- cache.skipped = preview.skippedCount;
1118
- cache.width = width;
1119
- }
1120
- // truncateToVisualLines keeps the tail — show ellipsis BEFORE the visible lines.
1121
- if ((cache.skipped ?? 0) > 0) {
1122
- out.push(truncateToWidth(ellipsisLine(cache.skipped!), width, "..."));
1123
- }
1124
- out.push(...(cache.responseLines ?? []));
1125
- }
1126
- }
1127
-
1128
- // ── Status ───────────────────────────────────────────────
1129
- const status = statusLine();
1130
- const totalSkipped = (cache.skipped ?? 0) + (cache.promptSkipped ?? 0);
1131
- const expandHint = !expanded && totalSkipped > 0
1132
- ? keyHint("app.tools.expand", `expand · ${totalSkipped} lines hidden`)
1133
- : !expanded && toolCalls.some((t) => t.result !== undefined)
1134
- ? keyHint("app.tools.expand", "expand for tool outputs")
1135
- : "";
1136
- const statusWithHint = [status, expandHint].filter(Boolean).join(" ");
1137
- if (statusWithHint) out.push(truncateToWidth(statusWithHint, width, "..."));
1138
- if (details.running && !details.backgroundJobId)
1139
- out.push(truncateToWidth(theme.fg("dim", "Ctrl+Shift+B: move to background"), width, "..."));
1140
-
1141
- return out;
1142
- },
1143
- };
320
+ renderResult(result: AgentToolResult<unknown>, opts: ToolRenderResultOptions, theme: Theme) {
321
+ return renderSubagentResult(result, opts, theme);
1144
322
  },
1145
323
 
1146
- async execute(_id: string, params: Record<string, any>, signal: AbortSignal | undefined, onUpdate: AgentToolUpdateCallback<unknown> | undefined, ctx: ExtensionContext): Promise<any> {
324
+ async execute(_id: string, params: Record<string, any>, signal: AbortSignal | undefined, onUpdate, ctx: ExtensionContext): Promise<any> {
1147
325
  const cwd = params.cwd ?? ctx.cwd;
1148
326
  const agents = discoverAgents(cwd);
1149
327
 
@@ -1156,7 +334,7 @@ export default function (pi: ExtensionAPI) {
1156
334
  return { agent: found };
1157
335
  };
1158
336
 
1159
- // ── Management: list ──────────────────────────────────────────────────────
337
+ // ── Management: list ────────────────────────────────────────────────
1160
338
  if (params.action === "list" || (!params.action && !params.agent && !params.tasks)) {
1161
339
  if (agents.length === 0) {
1162
340
  return {
@@ -1172,7 +350,7 @@ export default function (pi: ExtensionAPI) {
1172
350
  return { content: [{ type: "text", text: `Agents (${agents.length}):\n${lines.join("\n")}` }] };
1173
351
  }
1174
352
 
1175
- // ── Management: get ───────────────────────────────────────────────────────
353
+ // ── Management: get ─────────────────────────────────────────────────
1176
354
  if (params.action === "get" && params.agent) {
1177
355
  const { agent, error } = findAgent(params.agent);
1178
356
  if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
@@ -1181,12 +359,13 @@ export default function (pi: ExtensionAPI) {
1181
359
  `**Description:** ${agent.description}`,
1182
360
  agent.model ? `**Model:** ${agent.model}` : null,
1183
361
  `**Tools:** ${formatTools(agent.tools)}`,
362
+ `**Max subagent depth:** ${agent.maxDepth}`,
1184
363
  agent.systemPrompt ? `\n**System prompt:**\n${agent.systemPrompt}` : null,
1185
364
  ].filter(Boolean).join("\n");
1186
365
  return { content: [{ type: "text", text: info }] };
1187
366
  }
1188
367
 
1189
- // ── Background status ───────────────────────────────────────────────────
368
+ // ── Background status ───────────────────────────────────────────────
1190
369
  if (params.action === "status") {
1191
370
  const jobs = getBgManager().getAllJobs();
1192
371
  if (jobs.length === 0) return { content: [{ type: "text", text: "No background jobs." }] };
@@ -1197,7 +376,7 @@ export default function (pi: ExtensionAPI) {
1197
376
  return { content: [{ type: "text", text: lines.join("\n") }] };
1198
377
  }
1199
378
 
1200
- // ── Background poll ────────────────────────────────────────────────────────
379
+ // ── Background poll ─────────────────────────────────────────────────
1201
380
  if (params.action === "poll") {
1202
381
  if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId to poll." }] };
1203
382
  const job = getBgManager().getJob(params.jobId);
@@ -1210,7 +389,7 @@ export default function (pi: ExtensionAPI) {
1210
389
  return { content: [{ type: "text", text: parts.join("\n") }] };
1211
390
  }
1212
391
 
1213
- // ── Background cancel ──────────────────────────────────────────────────────
392
+ // ── Background cancel ───────────────────────────────────────────────
1214
393
  if (params.action === "cancel") {
1215
394
  if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId to cancel." }] };
1216
395
  const result = getBgManager().cancel(params.jobId);
@@ -1220,7 +399,7 @@ export default function (pi: ExtensionAPI) {
1220
399
  return { content: [{ type: "text", text: msg }] };
1221
400
  }
1222
401
 
1223
- // ── Foreground → background detach ────────────────────────────────────────
402
+ // ── Foreground → background detach ──────────────────────────────────
1224
403
  if (params.action === "detach") {
1225
404
  if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId (fg_xxxxx) to detach." }] };
1226
405
  const fgEntry = _fgJobs.get(params.jobId);
@@ -1229,23 +408,21 @@ export default function (pi: ExtensionAPI) {
1229
408
  return { content: [{ type: "text", text: `Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.` }] };
1230
409
  }
1231
410
 
1232
- // ── Single mode ───────────────────────────────────────────────────────────
411
+ // ── Single mode ─────────────────────────────────────────────────────
1233
412
  if (params.agent && params.task) {
1234
413
  const { agent, error } = findAgent(params.agent);
1235
414
  if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
1236
415
 
1237
- // Background dispatch — fire and forget
1238
416
  if (params.background) {
1239
417
  const bgAbort = new AbortController();
1240
418
  const handle: BackgroundHandleLike = { abort: () => bgAbort.abort() };
1241
419
  const resultPromise: Promise<BackgroundJobResult> = runAgent(
1242
- agent, params.task, cwd, params.model, bgAbort.signal, undefined
420
+ agent, params.task, cwd, params.model, bgAbort.signal, undefined,
1243
421
  ).then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
1244
422
  const jobId = getBgManager().adoptHandle(agent.name, params.task, cwd, handle, resultPromise);
1245
423
  return { content: [{ type: "text", text: `Background job started: ${jobId}\nTo check status, ask me to poll job ${jobId}.` }] };
1246
424
  }
1247
425
 
1248
- // Foreground run with detach support
1249
426
  const fgId = `fg_${randomUUID().slice(0, 8)}`;
1250
427
  const agentAbort = new AbortController();
1251
428
  const forwardAbort = () => agentAbort.abort();
@@ -1254,18 +431,15 @@ export default function (pi: ExtensionAPI) {
1254
431
  let detachResolveFn: ((bgJobId: string) => void) | null = null;
1255
432
  const detachPromise = new Promise<string>((resolve) => { detachResolveFn = resolve; });
1256
433
 
1257
- // Wrap onUpdate so detach can stop forwarding updates to the parent
1258
- // agent's listener (which becomes invalid once execute() returns).
1259
434
  let forwardUpdates = true;
1260
435
  const wrappedOnUpdate: OnUpdate | undefined = onUpdate
1261
- ? (partial) => { if (forwardUpdates) onUpdate(partial); }
436
+ ? (partial) => { if (forwardUpdates) (onUpdate as unknown as OnUpdate)(partial); }
1262
437
  : undefined;
1263
438
 
1264
439
  const agentRunPromise: Promise<RunResult> = runAgent(
1265
440
  agent, params.task, cwd, params.model, agentAbort.signal, wrappedOnUpdate,
1266
441
  );
1267
442
 
1268
- // Derived promise for the bg manager (used only if we detach)
1269
443
  const bgResultPromise: Promise<BackgroundJobResult> = agentRunPromise
1270
444
  .then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
1271
445
 
@@ -1296,7 +470,7 @@ export default function (pi: ExtensionAPI) {
1296
470
  });
1297
471
 
1298
472
  if (outcome === "detached") {
1299
- const bgJobId = await detachPromise; // already resolved — instant
473
+ const bgJobId = await detachPromise;
1300
474
  return {
1301
475
  content: [{ type: "text", text: `Moved to background: ${bgJobId}. Completion will be announced automatically.` }],
1302
476
  details: {
@@ -1321,12 +495,13 @@ export default function (pi: ExtensionAPI) {
1321
495
  elapsedMs: undefined,
1322
496
  model: result.model,
1323
497
  toolCalls: result.toolCalls,
498
+ executionEvents: result.executionEvents,
1324
499
  } satisfies SubagentDetails,
1325
500
  isError: result.exitCode !== 0,
1326
501
  };
1327
502
  }
1328
503
 
1329
- // ── Parallel mode ─────────────────────────────────────────────
504
+ // ── Parallel mode ───────────────────────────────────────────────────
1330
505
  if (params.tasks && params.tasks.length > 0) {
1331
506
  const expanded: Array<{ agent: string; task: string; model?: string; cwd?: string }> = [];
1332
507
  for (const t of params.tasks) {
@@ -1343,14 +518,14 @@ export default function (pi: ExtensionAPI) {
1343
518
  }));
1344
519
  let runningUsage = { ...emptyUsage };
1345
520
 
1346
- const emitParallel = (running: boolean) => onUpdate?.({
521
+ const emitParallel = (running: boolean) => (onUpdate as unknown as OnUpdate | undefined)?.({
1347
522
  content: [{ type: "text", text: "" }],
1348
523
  details: { mode: "parallel", parallelAgents: [...parallelAgents], usage: { ...runningUsage }, running, toolCalls: [] } satisfies SubagentDetails,
1349
524
  });
1350
525
 
1351
526
  emitParallel(true);
1352
527
 
1353
- const parentDepth = _currentDepth;
528
+ const parentDepth = getCurrentDepth();
1354
529
  const allResults = await mapConcurrent(expanded, concurrency, async (t, i) => {
1355
530
  parallelAgents[i]!.status = "running";
1356
531
  emitParallel(true);
@@ -1389,8 +564,6 @@ export default function (pi: ExtensionAPI) {
1389
564
  };
1390
565
  }
1391
566
 
1392
- // ── Chain mode ────────────────────────────────────────────
1393
- // Shouldn't reach here
1394
567
  return { content: [{ type: "text", text: "Provide agent+task or tasks array." }] };
1395
568
  },
1396
569
  });