pi-fast-subagent 0.8.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,575 +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 DEFAULT_MAX_DEPTH = 0;
261
- const DEPTH_ENV = "PI_FAST_SUBAGENT_DEPTH";
262
- const MAX_DEPTH_ENV = "PI_FAST_SUBAGENT_MAX_DEPTH";
263
-
264
- interface ToolCallEntry {
265
- id: string;
266
- name: string;
267
- argSummary: string;
268
- result?: string;
269
- isError?: boolean;
270
- durMs?: number;
271
- }
272
-
273
- interface RunResult {
274
- output: string;
275
- exitCode: number;
276
- error?: string;
277
- model?: string;
278
- toolCalls: ToolCallEntry[];
279
- usage: { input: number; output: number; cost: number; turns: number };
280
- }
281
-
282
- interface AgentRowStatus {
283
- name: string;
284
- taskSummary: string;
285
- status: "pending" | "running" | "done" | "error";
286
- durMs?: number;
287
- toolCalls?: ToolCallEntry[];
288
- responseText?: string;
289
- }
290
-
291
- interface SubagentDetails {
292
- mode?: "single" | "parallel";
293
- agentName?: string;
294
- task?: string;
295
- // parallel
296
- parallelAgents?: AgentRowStatus[];
297
- usage: RunResult["usage"];
298
- running: boolean;
299
- elapsedMs?: number;
300
- model?: string;
301
- backgroundJobId?: string;
302
- toolCalls: ToolCallEntry[];
303
- }
304
-
305
- type OnUpdate = (partial: { content: [{ type: "text"; text: string }]; details: unknown }) => void;
306
-
307
- function formatDuration(ms: number): string {
308
- const s = Math.max(0, Math.floor(ms / 1000));
309
- const m = Math.floor(s / 60);
310
- const rem = s % 60;
311
- return m > 0 ? `${m}m ${rem}s` : `${rem}s`;
312
- }
313
-
314
- function summarizeTask(task: string, max = 60): string {
315
- return task.length > max ? task.slice(0, max - 3) + "..." : task;
316
- }
317
-
318
- function formatBgJobSummary(job: BackgroundSubagentJob, now = Date.now()): string {
319
- const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(now - job.startedAt);
320
- return `${job.id} [${job.status}] ${job.agentName} · ${dur} · ${summarizeTask(job.task)}`;
321
- }
322
-
323
- function formatBgJobDetails(job: BackgroundSubagentJob, now = Date.now()): string {
324
- const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(now - job.startedAt);
325
- const lines = [`${job.id} [${job.status}] ${job.agentName} · ${dur}`, `Task: ${job.task}`];
326
- if (job.model) lines.push(`Model: ${job.model}`);
327
- if (job.status === "completed") lines.push(`\nResult:\n${job.resultSummary ?? "(no output)"}`);
328
- if (job.status === "failed") lines.push(`\nError: ${job.error ?? "(unknown)"}`);
329
- if (job.status === "cancelled") lines.push("\nCancelled.");
330
- if (job.status === "running") lines.push("\nStill running.");
331
- return lines.join("\n");
332
- }
333
-
334
- // Module-level depth counters for nested in-process subagent calls.
335
- let _currentDepth = 0;
336
- let _currentMaxDepth = DEFAULT_MAX_DEPTH;
337
-
338
- async function runAgent(
339
- agent: AgentConfig,
340
- task: string,
341
- cwd: string,
342
- modelOverride: string | undefined,
343
- signal: AbortSignal | undefined,
344
- onUpdate: OnUpdate | undefined,
345
- parentDepth?: number,
346
- ): Promise<RunResult> {
347
- const depth = parentDepth ?? _currentDepth;
348
- const isNestedCall = depth > 0;
349
- if (isNestedCall && depth > _currentMaxDepth) {
350
- return {
351
- output: "",
352
- exitCode: 1,
353
- error: `Nested subagents are disabled by default. Set maxDepth: ${depth} (or higher) in the parent agent frontmatter to allow this call.`,
354
- toolCalls: [],
355
- usage: { input: 0, output: 0, cost: 0, turns: 0 },
356
- };
357
- }
358
-
359
- const bootStartedAt = Date.now();
360
- const { authStorage, modelRegistry } = getAuth();
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);
383
-
384
- const loaderLease = await acquireResourceLoader(
385
- cwd,
386
- agentDir,
387
- noExtensions,
388
- agent.systemPrompt || undefined,
389
- );
390
-
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
- }
412
-
413
- // Resolve and apply model
414
- const modelStr = modelOverride ?? agent.model;
415
- if (modelStr) {
416
- const [provider, ...rest] = modelStr.split("/");
417
- const modelId = rest.join("/");
418
- if (provider && modelId) {
419
- const model = modelRegistry.find(provider, modelId);
420
- if (model) await session.setModel(model);
421
- }
422
- }
423
-
424
- // Apply tools allowlist.
425
- // "all" → no restriction (everything registered stays active)
426
- // "none" → disable every tool
427
- // string[] → explicit allowlist
428
- if (agent.tools === "none") {
429
- session.setActiveToolsByName([]);
430
- } else if (Array.isArray(agent.tools) && agent.tools.length > 0) {
431
- session.setActiveToolsByName(agent.tools);
432
- }
433
-
434
- // Track output and usage
435
- const usage = { input: 0, output: 0, cost: 0, turns: 0 };
436
- let lastOutput = "";
437
- let currentDelta = "";
438
- let detectedModel: string | undefined;
439
- const startedAt = bootStartedAt;
440
- const configuredModel = modelOverride ?? agent.model;
441
- const toolCalls: ToolCallEntry[] = [];
442
- const toolStartTimes = new Map<string, number>();
443
-
444
- let done = false;
445
-
446
- function emitUpdate(): void {
447
- if (done) return;
448
- onUpdate?.({
449
- content: [{ type: "text", text: currentDelta || lastOutput || "" }],
450
- details: {
451
- agentName: agent.name,
452
- task,
453
- usage,
454
- running: true,
455
- elapsedMs: Date.now() - startedAt,
456
- model: detectedModel ?? configuredModel,
457
- toolCalls: [...toolCalls],
458
- } satisfies SubagentDetails,
459
- });
460
- }
461
-
462
- emitUpdate();
463
-
464
- const heartbeat = setInterval(emitUpdate, 1000);
465
-
466
- const unsubscribe = session.subscribe((event: any) => {
467
- // Stream tool execution events
468
- if (event.type === "tool_execution_start") {
469
- toolStartTimes.set(event.toolCallId, Date.now());
470
- toolCalls.push({
471
- id: event.toolCallId,
472
- name: event.toolName,
473
- argSummary: summarizeToolArgs(event.toolName, event.args),
474
- });
475
- emitUpdate();
476
- return;
477
- }
478
-
479
- if (event.type === "tool_execution_end") {
480
- const startedAtTool = toolStartTimes.get(event.toolCallId);
481
- toolStartTimes.delete(event.toolCallId);
482
- const resultText: string = (event.result?.content ?? [])
483
- .filter((p: any) => p.type === "text")
484
- .map((p: any) => p.text as string)
485
- .join("\n");
486
- let entry: ToolCallEntry | undefined;
487
- for (let i = toolCalls.length - 1; i >= 0; i--) {
488
- if (toolCalls[i]!.id === event.toolCallId) { entry = toolCalls[i]; break; }
489
- }
490
- if (!entry) {
491
- for (let i = toolCalls.length - 1; i >= 0; i--) {
492
- if (toolCalls[i]!.name === event.toolName && toolCalls[i]!.result === undefined) { entry = toolCalls[i]; break; }
493
- }
494
- }
495
- if (entry) {
496
- entry.result = resultText;
497
- entry.isError = event.isError;
498
- entry.durMs = startedAtTool != null ? Date.now() - startedAtTool : undefined;
499
- }
500
- emitUpdate();
501
- return;
502
- }
503
-
504
- // Stream text deltas live to the UI
505
- if (event.type === "message_update") {
506
- const e = event.assistantMessageEvent;
507
- if (e?.type === "text_delta" && e.delta) {
508
- currentDelta += e.delta;
509
- emitUpdate();
510
- }
511
- return;
512
- }
513
-
514
- if (event.type !== "message_end" || !event.message) return;
515
- const msg = event.message;
516
- if (msg.role !== "assistant") return; // usage/model only tracked for assistant turns
517
-
518
- usage.turns++;
519
- const u = msg.usage;
520
- if (u) {
521
- usage.input += u.input ?? 0;
522
- usage.output += u.output ?? 0;
523
- usage.cost += u.cost?.total ?? 0;
524
- }
525
- if (msg.model) detectedModel = msg.model;
526
-
527
- // Extract last text content
528
- for (const part of msg.content ?? []) {
529
- if (part.type === "text") {
530
- lastOutput = part.text;
531
- break;
532
- }
533
- }
534
- // Reset delta accumulator for next turn
535
- currentDelta = "";
536
-
537
- onUpdate?.({
538
- content: [{ type: "text", text: lastOutput || "(running...)" }],
539
- details: {
540
- agent: agent.name,
541
- usage,
542
- running: true,
543
- elapsedMs: Date.now() - startedAt,
544
- model: detectedModel ?? configuredModel,
545
- },
546
- });
547
- });
548
-
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.
551
- const prevEnvDepth = process.env[DEPTH_ENV];
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);
556
- _currentDepth = depth + 1;
557
- _currentMaxDepth = depth + maxDepth;
558
- process.env[DEPTH_ENV] = String(_currentDepth);
559
- process.env[MAX_DEPTH_ENV] = String(_currentMaxDepth);
560
-
561
- let exitCode = 0;
562
- let error: string | undefined;
563
-
564
- try {
565
- if (signal?.aborted) throw new Error("Aborted");
566
-
567
- const onAbort = () => void session.abort();
568
- signal?.addEventListener("abort", onAbort, { once: true });
569
- try {
570
- await session.prompt(task);
571
- } finally {
572
- signal?.removeEventListener("abort", onAbort);
573
- }
574
- } catch (e) {
575
- exitCode = 1;
576
- error = signal?.aborted ? "Aborted" : e instanceof Error ? e.message : String(e);
577
- } finally {
578
- done = true;
579
- clearInterval(heartbeat);
580
- unsubscribe();
581
- session.dispose();
582
- loaderLease.release();
583
- if (prevEnvDepth === undefined) delete process.env[DEPTH_ENV];
584
- else process.env[DEPTH_ENV] = prevEnvDepth;
585
- if (prevEnvMaxDepth === undefined) delete process.env[MAX_DEPTH_ENV];
586
- else process.env[MAX_DEPTH_ENV] = prevEnvMaxDepth;
587
- _currentDepth = prevDepth;
588
- _currentMaxDepth = prevMaxDepth;
589
- }
590
-
591
- return { output: lastOutput, exitCode, error, model: detectedModel, toolCalls, usage };
592
- }
593
-
594
- // ─── Helpers ─────────────────────────────────────────────────────────────────
595
-
596
- async function mapConcurrent<TIn, TOut>(
597
- items: TIn[],
598
- concurrency: number,
599
- fn: (item: TIn, i: number) => Promise<TOut>,
600
- ): Promise<TOut[]> {
601
- if (!items.length) return [];
602
- const limit = Math.max(1, Math.min(concurrency, items.length));
603
- const results: TOut[] = new Array(items.length);
604
- let next = 0;
605
- await Promise.all(
606
- Array.from({ length: limit }, async () => {
607
- while (true) {
608
- const i = next++;
609
- if (i >= items.length) return;
610
- results[i] = await fn(items[i], i);
611
- }
612
- }),
613
- );
614
- return results;
615
- }
616
-
617
- function formatTokens(n: number): string {
618
- if (n < 1000) return String(n);
619
- if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
620
- return `${Math.round(n / 1000)}k`;
621
- }
622
-
623
- function formatUsage(usage: RunResult["usage"], model?: string): string {
624
- const parts: string[] = [];
625
- if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
626
- if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
627
- if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
628
- if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
629
- if (model) parts.push(model);
630
- return parts.join(" ");
631
- }
632
-
633
- function getFinalText(r: RunResult): string {
634
- if (r.exitCode !== 0) return `Error: ${r.error ?? r.output ?? "(no output)"}`;
635
- return r.output || "(no output)";
636
- }
637
-
638
- // ─── Tool schemas ─────────────────────────────────────────────────────────────
639
-
640
- const TaskItem = Type.Object({
641
- agent: Type.String({ description: "Agent name" }),
642
- task: Type.String({ description: "Task to delegate" }),
643
- model: Type.Optional(Type.String({ description: "Model override (provider/model)" })),
644
- cwd: Type.Optional(Type.String({ description: "Working directory" })),
645
- count: Type.Optional(Type.Number({ description: "Repeat this task N times" })),
646
- });
647
-
648
- const SubagentParams = Type.Object({
649
- // Single mode
650
- agent: Type.Optional(Type.String({ description: "Agent name (single mode)" })),
651
- task: Type.Optional(Type.String({ description: "Task (single mode)" })),
652
- model: Type.Optional(Type.String({ description: "Model override (single mode)" })),
653
- cwd: Type.Optional(Type.String({ description: "Working directory" })),
654
-
655
- // Parallel mode
656
- tasks: Type.Optional(
657
- Type.Array(TaskItem, {
658
- description: "Array of {agent, task} for parallel execution. Use count to repeat one task.",
659
- }),
660
- ),
661
- concurrency: Type.Optional(
662
- Type.Number({ description: "Max parallel concurrency (default: 4)", default: 4 }),
663
- ),
664
-
665
- // Background
666
- background: Type.Optional(Type.Boolean({ description: "Run in background, returns job ID immediately" })),
667
- jobId: Type.Optional(Type.String({ description: "Job ID for poll/cancel" })),
668
-
669
- // Management
670
- action: Type.Optional(
671
- Type.Union(
672
- [
673
- Type.Literal("list"),
674
- Type.Literal("get"),
675
- Type.Literal("status"),
676
- Type.Literal("poll"),
677
- Type.Literal("cancel"),
678
- Type.Literal("detach"),
679
- ],
680
- { description: "'list'/'get' for agents, 'status' for bg jobs, 'poll'/'cancel' for a specific job, 'detach' to move a foreground job to background" },
681
- ),
682
- ),
683
- agentScope: Type.Optional(
684
- Type.Union(
685
- [Type.Literal("user"), Type.Literal("project"), Type.Literal("both")],
686
- { description: "Agent scope filter", default: "both" },
687
- ),
688
- ),
689
- });
690
-
691
- // ─── Extension entry point ────────────────────────────────────────────────────
61
+ // ─── Extension entry point ──────────────────────────────────────────────────
692
62
 
693
63
  export default function (pi: ExtensionAPI) {
694
- // ─── Status keys ────────────────────────────────────────────────────────────────────
695
64
  const BG_STATUS_KEY = "fast-subagent-bg";
696
65
  const FG_STATUS_KEY = "fast-subagent-fg";
697
66
 
698
- // ─── Background job lifecycle ─────────────────────────────────────────────────────
699
67
  _onBgJobComplete = (job) => {
700
68
  refreshBgStatus();
701
69
  const elapsed = job.completedAt ? ((job.completedAt - job.startedAt) / 1000).toFixed(1) : "?";
@@ -719,12 +87,12 @@ export default function (pi: ExtensionAPI) {
719
87
  pi.on("session_start", async (_event, ctx) => {
720
88
  _setBgStatus = (text) => ctx.ui.setStatus(BG_STATUS_KEY, text);
721
89
 
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.
90
+ // Warm one extension-capable loader after startup so first `tools: all`
91
+ // subagent call reuses loaded extensions instead of blocking.
724
92
  if (process.env.PI_FAST_SUBAGENT_WARM !== "0") {
725
93
  const warmCwd = ctx.cwd;
726
94
  const warmAgentDir = getAgentDir();
727
- setTimeout(() => warmResourceLoader(warmCwd, warmAgentDir, false), 1000);
95
+ setTimeout(() => defaultLoaderPool.warm(warmCwd, warmAgentDir, false), 1000);
728
96
  }
729
97
  });
730
98
 
@@ -732,10 +100,10 @@ export default function (pi: ExtensionAPI) {
732
100
  getBgManager().shutdown();
733
101
  _bgManager = null;
734
102
  _setBgStatus = null;
735
- _loaderPool.clear();
103
+ defaultLoaderPool.clear();
736
104
  });
737
105
 
738
- // ─── Ctrl+Shift+B — move foreground subagent to background ─────────────────────────
106
+ // ─── Ctrl+Shift+B — detach foreground subagent ────────────────────────────
739
107
  pi.registerShortcut(Key.ctrlShift("b"), {
740
108
  description: "Move foreground subagent to background",
741
109
  handler: async (ctx) => {
@@ -756,7 +124,7 @@ export default function (pi: ExtensionAPI) {
756
124
  },
757
125
  });
758
126
 
759
- // ─── /agent slash command ─────────────────────────────────────────────────
127
+ // ─── /fast-subagent:agent ─────────────────────────────────────────────────
760
128
  pi.registerCommand("fast-subagent:agent", {
761
129
  description: "List available subagents. Usage: /fast-subagent:agent [name] — show details for a specific agent.",
762
130
  getArgumentCompletions(prefix: string) {
@@ -796,7 +164,7 @@ export default function (pi: ExtensionAPI) {
796
164
  " ~/.pi/agent/agents/ (user-level)\n" +
797
165
  " .pi/agents/ (project-level)\n" +
798
166
  "\nFrontmatter required: name, description. Optional: model, tools, maxDepth.",
799
- "info"
167
+ "info",
800
168
  );
801
169
  return;
802
170
  }
@@ -807,15 +175,11 @@ export default function (pi: ExtensionAPI) {
807
175
  const lines: string[] = [`Agents (${agents.length}):`];
808
176
  if (projectAgents.length) {
809
177
  lines.push("\nProject (.pi/agents/):");
810
- for (const a of projectAgents) {
811
- lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
812
- }
178
+ for (const a of projectAgents) lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
813
179
  }
814
180
  if (userAgents.length) {
815
181
  lines.push("\nUser (~/.pi/agent/agents/):");
816
- for (const a of userAgents) {
817
- lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
818
- }
182
+ for (const a of userAgents) lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
819
183
  }
820
184
  lines.push("");
821
185
  lines.push("Tip: /fast-subagent:agent <name> for details · Add .md files to .pi/agents/ to create new agents");
@@ -823,7 +187,7 @@ export default function (pi: ExtensionAPI) {
823
187
  },
824
188
  });
825
189
 
826
- // ─── /bg slash command ────────────────────────────────────────────────────
190
+ // ─── /fast-subagent:bg ────────────────────────────────────────────────────
827
191
  pi.registerCommand("fast-subagent:bg", {
828
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.",
829
193
  getArgumentCompletions(_prefix: string) {
@@ -849,14 +213,11 @@ export default function (pi: ExtensionAPI) {
849
213
  return;
850
214
  }
851
215
  const bgJobId = entry.detach();
852
- ctx.ui.notify(
853
- `Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.`,
854
- "info",
855
- );
216
+ ctx.ui.notify(`Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.`, "info");
856
217
  },
857
218
  });
858
219
 
859
- // ─── /bg-status slash command ─────────────────────────────────────────────
220
+ // ─── /fast-subagent:bg-status ─────────────────────────────────────────────
860
221
  pi.registerCommand("fast-subagent:bg-status", {
861
222
  description: "Show active background subagents. Usage: /fast-subagent:bg-status [sa-job-id] — omit ID to open selector.",
862
223
  getArgumentCompletions(prefix: string) {
@@ -896,7 +257,7 @@ export default function (pi: ExtensionAPI) {
896
257
  },
897
258
  });
898
259
 
899
- // ─── /bg-cancel slash command ─────────────────────────────────────────────
260
+ // ─── /fast-subagent:bg-cancel ─────────────────────────────────────────────
900
261
  pi.registerCommand("fast-subagent:bg-cancel", {
901
262
  description: "Cancel running background subagent. Usage: /fast-subagent:bg-cancel [sa-job-id] — omit ID to choose with arrow keys.",
902
263
  getArgumentCompletions(prefix: string) {
@@ -944,6 +305,7 @@ export default function (pi: ExtensionAPI) {
944
305
  },
945
306
  });
946
307
 
308
+ // ─── `subagent` tool ──────────────────────────────────────────────────────
947
309
  pi.registerTool({
948
310
  name: "subagent",
949
311
  label: "Subagent",
@@ -955,209 +317,11 @@ export default function (pi: ExtensionAPI) {
955
317
  ].join(" "),
956
318
  parameters: SubagentParams,
957
319
 
958
- renderResult(result: AgentToolResult<unknown>, { isPartial, expanded }: ToolRenderResultOptions, theme: Theme) {
959
- const agentText = result.content?.[0]?.type === "text" ? (result.content[0] as any).text as string : "";
960
- const details = (result.details ?? {}) as SubagentDetails;
961
- const toolCalls = details.toolCalls ?? [];
962
-
963
- // ── Parallel / Chain mode renders ────────────────────────────────
964
- if (details.mode === "parallel" && details.parallelAgents) {
965
- const agents = details.parallelAgents;
966
- const doneCount = agents.filter((a) => a.status === "done" || a.status === "error").length;
967
-
968
- function agentToolRow(t: ToolCallEntry): string {
969
- const arg = t.argSummary || "";
970
- const call = `${t.name}(${arg})`;
971
- if (t.result === undefined) return theme.fg("dim", call);
972
- const dur = t.durMs != null ? (t.durMs < 1000 ? ` ${t.durMs}ms` : ` ${(t.durMs / 1000).toFixed(1)}s`) : "";
973
- return `${call}${t.isError ? " ✗" : ` ✓${dur}`}`;
974
- }
975
-
976
- function wrapL(text: string, w: number): string[] {
977
- try { return wrapTextWithAnsi(text, w); } catch { return [truncateToWidth(text, w, "...")]; }
978
- }
979
-
980
- const cache: { width?: number } = {};
981
- return {
982
- invalidate() { cache.width = undefined; },
983
- render(width: number): string[] {
984
- const out: string[] = [];
985
- const header = details.running
986
- ? `Parallel (${doneCount}/${agents.length} done)`
987
- : `Parallel: ${agents.filter((a) => a.status === "done").length}/${agents.length} succeeded`;
988
- out.push(truncateToWidth(header, width, "..."));
989
-
990
- for (const a of agents) {
991
- const dur = a.durMs != null ? (a.durMs < 1000 ? ` ${a.durMs}ms` : ` ${(a.durMs / 1000).toFixed(1)}s`) : "";
992
- const mark = a.status === "pending" ? theme.fg("dim", "⋅") : a.status === "running" ? theme.fg("dim", "→") : a.status === "done" ? `✓${dur}` : `✗${dur}`;
993
-
994
- if (expanded) {
995
- // Full solo-style block per agent
996
- out.push("");
997
- out.push(truncateToWidth(`[${a.name}] ${mark}`, width, "..."));
998
- out.push(truncateToWidth(`Prompt:`, width, "..."));
999
- out.push(truncateToWidth(` ${a.taskSummary}`, width, "..."));
1000
- for (const t of a.toolCalls ?? []) {
1001
- out.push(truncateToWidth(agentToolRow(t), width, "..."));
1002
- }
1003
- if (a.responseText) {
1004
- out.push("Response:");
1005
- const preview = truncateToVisualLines(a.responseText, 6, width - 2);
1006
- for (const l of preview.visualLines) out.push(truncateToWidth(" " + l, width, "..."));
1007
- if (preview.skippedCount > 0) out.push(truncateToWidth(theme.fg("dim", ` … ${preview.skippedCount} more lines`), width, "..."));
1008
- } else if (a.status === "running") {
1009
- out.push(theme.fg("dim", " running..."));
1010
- }
1011
- } else {
1012
- // Collapsed: compact one-liner
1013
- const row = ` [${a.name}] ${mark} ${a.taskSummary}`;
1014
- out.push(truncateToWidth(row, width, "..."));
1015
- // Show tool call rows compactly
1016
- for (const t of a.toolCalls ?? []) {
1017
- out.push(truncateToWidth(` ${agentToolRow(t)}`, width, "..."));
1018
- }
1019
- if (a.responseText && (a.status === "done" || a.status === "error")) {
1020
- const preview = truncateToVisualLines(a.responseText, 2, width - 4);
1021
- for (const l of preview.visualLines) out.push(truncateToWidth(" " + l, width, "..."));
1022
- }
1023
- }
1024
- }
1025
-
1026
- out.push("");
1027
- const status = details.running
1028
- ? ["running", details.usage?.turns ? `${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}` : ""].filter(Boolean).join(" · ")
1029
- : formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
1030
- const expandHint = !expanded ? keyHint("app.tools.expand", "expand for full output") : "";
1031
- out.push(truncateToWidth([status, expandHint].filter(Boolean).join(" "), width, "..."));
1032
- return out;
1033
- },
1034
- };
1035
- }
1036
-
1037
- function statusLine(): string {
1038
- if (details.backgroundJobId) return `moved to background · ${details.backgroundJobId}`;
1039
- const prefix = details.agentName ? `${theme.fg("toolTitle", details.agentName)} · ` : "";
1040
- if (details.running) {
1041
- const parts: string[] = ["running"];
1042
- if (details.usage?.turns) parts.push(`${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}`);
1043
- if (details.elapsedMs != null) parts.push(formatDuration(details.elapsedMs));
1044
- if (details.model) parts.push(details.model);
1045
- return prefix + parts.join(" · ");
1046
- }
1047
- return prefix + formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
1048
- }
1049
-
1050
- // Name(arg) ✓ 0.3s or Name(arg) (dim, still running)
1051
- function toolRow(t: ToolCallEntry): string {
1052
- const arg = t.argSummary ? t.argSummary : "";
1053
- const call = `${t.name}(${arg})`;
1054
- if (t.result === undefined) return theme.fg("dim", call);
1055
- const dur = t.durMs != null
1056
- ? t.durMs < 1000 ? ` ${t.durMs}ms` : ` ${(t.durMs / 1000).toFixed(1)}s`
1057
- : "";
1058
- return `${call}${t.isError ? " ✗" : ` ✓${dur}`}`;
1059
- }
1060
-
1061
- function wrapLine(text: string, w: number): string[] {
1062
- try { return wrapTextWithAnsi(text, w); } catch { return [truncateToWidth(text, w, "...")]; }
1063
- }
1064
-
1065
- const cache: {
1066
- width?: number;
1067
- promptLines?: string[];
1068
- promptSkipped?: number;
1069
- responseLines?: string[];
1070
- skipped?: number;
1071
- } = {};
1072
-
1073
- return {
1074
- invalidate() { cache.width = undefined; },
1075
- render(width: number): string[] {
1076
- const out: string[] = [];
1077
- const indent = " ";
1078
- const ellipsisLine = (count: number) =>
1079
- theme.fg("muted", `${indent}… (${count} more line${count === 1 ? "" : "s"})`);
1080
-
1081
- // ── Prompt ────────────────────────────────────────────────────
1082
- if (details.task) {
1083
- out.push("Prompt:");
1084
- if (expanded) {
1085
- for (const line of details.task.split("\n")) {
1086
- for (const w of wrapLine(indent + line, width)) out.push(w);
1087
- }
1088
- } else {
1089
- // Up to 8 visual lines from the HEAD of the prompt (keep opening, not tail).
1090
- const PROMPT_PREVIEW_LINES = 8;
1091
- if (cache.width !== width || cache.promptLines === undefined) {
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);
1100
- }
1101
- out.push(...cache.promptLines);
1102
- if ((cache.promptSkipped ?? 0) > 0) {
1103
- out.push(truncateToWidth(ellipsisLine(cache.promptSkipped!), width, "..."));
1104
- }
1105
- }
1106
- }
1107
-
1108
- // ── Tool calls ─────────────────────────────────────────────
1109
- for (const t of toolCalls) {
1110
- out.push(truncateToWidth(toolRow(t), width, "..."));
1111
- if (expanded && t.result !== undefined) {
1112
- for (const line of t.result.split("\n")) {
1113
- for (const w of wrapLine(theme.fg("dim", indent + line), width)) out.push(w);
1114
- }
1115
- }
1116
- }
1117
-
1118
- // ── Response ────────────────────────────────────────────
1119
- const responseText = agentText || (isPartial ? "" : "");
1120
- if (responseText || isPartial) {
1121
- out.push("Response:");
1122
- if (expanded) {
1123
- for (const line of responseText.split("\n")) {
1124
- for (const w of wrapLine(indent + line, width)) out.push(w);
1125
- }
1126
- } else {
1127
- const PREVIEW_LINES = 6;
1128
- if (cache.width !== width) {
1129
- const preview = truncateToVisualLines(responseText, PREVIEW_LINES, width - indent.length);
1130
- cache.responseLines = preview.visualLines.map((l) => truncateToWidth(indent + l, width, "..."));
1131
- cache.skipped = preview.skippedCount;
1132
- cache.width = width;
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
- }
1138
- out.push(...(cache.responseLines ?? []));
1139
- }
1140
- }
1141
-
1142
- // ── Status ───────────────────────────────────────────────
1143
- const status = statusLine();
1144
- const totalSkipped = (cache.skipped ?? 0) + (cache.promptSkipped ?? 0);
1145
- const expandHint = !expanded && totalSkipped > 0
1146
- ? keyHint("app.tools.expand", `expand · ${totalSkipped} lines hidden`)
1147
- : !expanded && toolCalls.some((t) => t.result !== undefined)
1148
- ? keyHint("app.tools.expand", "expand for tool outputs")
1149
- : "";
1150
- const statusWithHint = [status, expandHint].filter(Boolean).join(" ");
1151
- if (statusWithHint) out.push(truncateToWidth(statusWithHint, width, "..."));
1152
- if (details.running && !details.backgroundJobId)
1153
- out.push(truncateToWidth(theme.fg("dim", "Ctrl+Shift+B: move to background"), width, "..."));
1154
-
1155
- return out;
1156
- },
1157
- };
320
+ renderResult(result: AgentToolResult<unknown>, opts: ToolRenderResultOptions, theme: Theme) {
321
+ return renderSubagentResult(result, opts, theme);
1158
322
  },
1159
323
 
1160
- 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> {
1161
325
  const cwd = params.cwd ?? ctx.cwd;
1162
326
  const agents = discoverAgents(cwd);
1163
327
 
@@ -1170,7 +334,7 @@ export default function (pi: ExtensionAPI) {
1170
334
  return { agent: found };
1171
335
  };
1172
336
 
1173
- // ── Management: list ──────────────────────────────────────────────────────
337
+ // ── Management: list ────────────────────────────────────────────────
1174
338
  if (params.action === "list" || (!params.action && !params.agent && !params.tasks)) {
1175
339
  if (agents.length === 0) {
1176
340
  return {
@@ -1186,7 +350,7 @@ export default function (pi: ExtensionAPI) {
1186
350
  return { content: [{ type: "text", text: `Agents (${agents.length}):\n${lines.join("\n")}` }] };
1187
351
  }
1188
352
 
1189
- // ── Management: get ───────────────────────────────────────────────────────
353
+ // ── Management: get ─────────────────────────────────────────────────
1190
354
  if (params.action === "get" && params.agent) {
1191
355
  const { agent, error } = findAgent(params.agent);
1192
356
  if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
@@ -1201,7 +365,7 @@ export default function (pi: ExtensionAPI) {
1201
365
  return { content: [{ type: "text", text: info }] };
1202
366
  }
1203
367
 
1204
- // ── Background status ───────────────────────────────────────────────────
368
+ // ── Background status ───────────────────────────────────────────────
1205
369
  if (params.action === "status") {
1206
370
  const jobs = getBgManager().getAllJobs();
1207
371
  if (jobs.length === 0) return { content: [{ type: "text", text: "No background jobs." }] };
@@ -1212,7 +376,7 @@ export default function (pi: ExtensionAPI) {
1212
376
  return { content: [{ type: "text", text: lines.join("\n") }] };
1213
377
  }
1214
378
 
1215
- // ── Background poll ────────────────────────────────────────────────────────
379
+ // ── Background poll ─────────────────────────────────────────────────
1216
380
  if (params.action === "poll") {
1217
381
  if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId to poll." }] };
1218
382
  const job = getBgManager().getJob(params.jobId);
@@ -1225,7 +389,7 @@ export default function (pi: ExtensionAPI) {
1225
389
  return { content: [{ type: "text", text: parts.join("\n") }] };
1226
390
  }
1227
391
 
1228
- // ── Background cancel ──────────────────────────────────────────────────────
392
+ // ── Background cancel ───────────────────────────────────────────────
1229
393
  if (params.action === "cancel") {
1230
394
  if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId to cancel." }] };
1231
395
  const result = getBgManager().cancel(params.jobId);
@@ -1235,7 +399,7 @@ export default function (pi: ExtensionAPI) {
1235
399
  return { content: [{ type: "text", text: msg }] };
1236
400
  }
1237
401
 
1238
- // ── Foreground → background detach ────────────────────────────────────────
402
+ // ── Foreground → background detach ──────────────────────────────────
1239
403
  if (params.action === "detach") {
1240
404
  if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId (fg_xxxxx) to detach." }] };
1241
405
  const fgEntry = _fgJobs.get(params.jobId);
@@ -1244,23 +408,21 @@ export default function (pi: ExtensionAPI) {
1244
408
  return { content: [{ type: "text", text: `Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.` }] };
1245
409
  }
1246
410
 
1247
- // ── Single mode ───────────────────────────────────────────────────────────
411
+ // ── Single mode ─────────────────────────────────────────────────────
1248
412
  if (params.agent && params.task) {
1249
413
  const { agent, error } = findAgent(params.agent);
1250
414
  if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
1251
415
 
1252
- // Background dispatch — fire and forget
1253
416
  if (params.background) {
1254
417
  const bgAbort = new AbortController();
1255
418
  const handle: BackgroundHandleLike = { abort: () => bgAbort.abort() };
1256
419
  const resultPromise: Promise<BackgroundJobResult> = runAgent(
1257
- agent, params.task, cwd, params.model, bgAbort.signal, undefined
420
+ agent, params.task, cwd, params.model, bgAbort.signal, undefined,
1258
421
  ).then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
1259
422
  const jobId = getBgManager().adoptHandle(agent.name, params.task, cwd, handle, resultPromise);
1260
423
  return { content: [{ type: "text", text: `Background job started: ${jobId}\nTo check status, ask me to poll job ${jobId}.` }] };
1261
424
  }
1262
425
 
1263
- // Foreground run with detach support
1264
426
  const fgId = `fg_${randomUUID().slice(0, 8)}`;
1265
427
  const agentAbort = new AbortController();
1266
428
  const forwardAbort = () => agentAbort.abort();
@@ -1269,18 +431,15 @@ export default function (pi: ExtensionAPI) {
1269
431
  let detachResolveFn: ((bgJobId: string) => void) | null = null;
1270
432
  const detachPromise = new Promise<string>((resolve) => { detachResolveFn = resolve; });
1271
433
 
1272
- // Wrap onUpdate so detach can stop forwarding updates to the parent
1273
- // agent's listener (which becomes invalid once execute() returns).
1274
434
  let forwardUpdates = true;
1275
435
  const wrappedOnUpdate: OnUpdate | undefined = onUpdate
1276
- ? (partial) => { if (forwardUpdates) onUpdate(partial); }
436
+ ? (partial) => { if (forwardUpdates) (onUpdate as unknown as OnUpdate)(partial); }
1277
437
  : undefined;
1278
438
 
1279
439
  const agentRunPromise: Promise<RunResult> = runAgent(
1280
440
  agent, params.task, cwd, params.model, agentAbort.signal, wrappedOnUpdate,
1281
441
  );
1282
442
 
1283
- // Derived promise for the bg manager (used only if we detach)
1284
443
  const bgResultPromise: Promise<BackgroundJobResult> = agentRunPromise
1285
444
  .then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
1286
445
 
@@ -1311,7 +470,7 @@ export default function (pi: ExtensionAPI) {
1311
470
  });
1312
471
 
1313
472
  if (outcome === "detached") {
1314
- const bgJobId = await detachPromise; // already resolved — instant
473
+ const bgJobId = await detachPromise;
1315
474
  return {
1316
475
  content: [{ type: "text", text: `Moved to background: ${bgJobId}. Completion will be announced automatically.` }],
1317
476
  details: {
@@ -1336,12 +495,13 @@ export default function (pi: ExtensionAPI) {
1336
495
  elapsedMs: undefined,
1337
496
  model: result.model,
1338
497
  toolCalls: result.toolCalls,
498
+ executionEvents: result.executionEvents,
1339
499
  } satisfies SubagentDetails,
1340
500
  isError: result.exitCode !== 0,
1341
501
  };
1342
502
  }
1343
503
 
1344
- // ── Parallel mode ─────────────────────────────────────────────
504
+ // ── Parallel mode ───────────────────────────────────────────────────
1345
505
  if (params.tasks && params.tasks.length > 0) {
1346
506
  const expanded: Array<{ agent: string; task: string; model?: string; cwd?: string }> = [];
1347
507
  for (const t of params.tasks) {
@@ -1358,14 +518,14 @@ export default function (pi: ExtensionAPI) {
1358
518
  }));
1359
519
  let runningUsage = { ...emptyUsage };
1360
520
 
1361
- const emitParallel = (running: boolean) => onUpdate?.({
521
+ const emitParallel = (running: boolean) => (onUpdate as unknown as OnUpdate | undefined)?.({
1362
522
  content: [{ type: "text", text: "" }],
1363
523
  details: { mode: "parallel", parallelAgents: [...parallelAgents], usage: { ...runningUsage }, running, toolCalls: [] } satisfies SubagentDetails,
1364
524
  });
1365
525
 
1366
526
  emitParallel(true);
1367
527
 
1368
- const parentDepth = _currentDepth;
528
+ const parentDepth = getCurrentDepth();
1369
529
  const allResults = await mapConcurrent(expanded, concurrency, async (t, i) => {
1370
530
  parallelAgents[i]!.status = "running";
1371
531
  emitParallel(true);
@@ -1404,8 +564,6 @@ export default function (pi: ExtensionAPI) {
1404
564
  };
1405
565
  }
1406
566
 
1407
- // ── Chain mode ────────────────────────────────────────────
1408
- // Shouldn't reach here
1409
567
  return { content: [{ type: "text", text: "Provide agent+task or tasks array." }] };
1410
568
  },
1411
569
  });