pi-fast-subagent 0.8.0 → 0.9.1

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 { renderSubagentCall, 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,15 @@ 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, "..."));
320
+ renderCall(args, theme, context) {
321
+ return renderSubagentCall(args, theme, context);
322
+ },
1154
323
 
1155
- return out;
1156
- },
1157
- };
324
+ renderResult(result: AgentToolResult<unknown>, opts: ToolRenderResultOptions, theme: Theme) {
325
+ return renderSubagentResult(result, opts, theme);
1158
326
  },
1159
327
 
1160
- async execute(_id: string, params: Record<string, any>, signal: AbortSignal | undefined, onUpdate: AgentToolUpdateCallback<unknown> | undefined, ctx: ExtensionContext): Promise<any> {
328
+ async execute(_id: string, params: Record<string, any>, signal: AbortSignal | undefined, onUpdate, ctx: ExtensionContext): Promise<any> {
1161
329
  const cwd = params.cwd ?? ctx.cwd;
1162
330
  const agents = discoverAgents(cwd);
1163
331
 
@@ -1170,7 +338,7 @@ export default function (pi: ExtensionAPI) {
1170
338
  return { agent: found };
1171
339
  };
1172
340
 
1173
- // ── Management: list ──────────────────────────────────────────────────────
341
+ // ── Management: list ────────────────────────────────────────────────
1174
342
  if (params.action === "list" || (!params.action && !params.agent && !params.tasks)) {
1175
343
  if (agents.length === 0) {
1176
344
  return {
@@ -1186,7 +354,7 @@ export default function (pi: ExtensionAPI) {
1186
354
  return { content: [{ type: "text", text: `Agents (${agents.length}):\n${lines.join("\n")}` }] };
1187
355
  }
1188
356
 
1189
- // ── Management: get ───────────────────────────────────────────────────────
357
+ // ── Management: get ─────────────────────────────────────────────────
1190
358
  if (params.action === "get" && params.agent) {
1191
359
  const { agent, error } = findAgent(params.agent);
1192
360
  if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
@@ -1201,7 +369,7 @@ export default function (pi: ExtensionAPI) {
1201
369
  return { content: [{ type: "text", text: info }] };
1202
370
  }
1203
371
 
1204
- // ── Background status ───────────────────────────────────────────────────
372
+ // ── Background status ───────────────────────────────────────────────
1205
373
  if (params.action === "status") {
1206
374
  const jobs = getBgManager().getAllJobs();
1207
375
  if (jobs.length === 0) return { content: [{ type: "text", text: "No background jobs." }] };
@@ -1212,7 +380,7 @@ export default function (pi: ExtensionAPI) {
1212
380
  return { content: [{ type: "text", text: lines.join("\n") }] };
1213
381
  }
1214
382
 
1215
- // ── Background poll ────────────────────────────────────────────────────────
383
+ // ── Background poll ─────────────────────────────────────────────────
1216
384
  if (params.action === "poll") {
1217
385
  if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId to poll." }] };
1218
386
  const job = getBgManager().getJob(params.jobId);
@@ -1225,7 +393,7 @@ export default function (pi: ExtensionAPI) {
1225
393
  return { content: [{ type: "text", text: parts.join("\n") }] };
1226
394
  }
1227
395
 
1228
- // ── Background cancel ──────────────────────────────────────────────────────
396
+ // ── Background cancel ───────────────────────────────────────────────
1229
397
  if (params.action === "cancel") {
1230
398
  if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId to cancel." }] };
1231
399
  const result = getBgManager().cancel(params.jobId);
@@ -1235,7 +403,7 @@ export default function (pi: ExtensionAPI) {
1235
403
  return { content: [{ type: "text", text: msg }] };
1236
404
  }
1237
405
 
1238
- // ── Foreground → background detach ────────────────────────────────────────
406
+ // ── Foreground → background detach ──────────────────────────────────
1239
407
  if (params.action === "detach") {
1240
408
  if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId (fg_xxxxx) to detach." }] };
1241
409
  const fgEntry = _fgJobs.get(params.jobId);
@@ -1244,23 +412,21 @@ export default function (pi: ExtensionAPI) {
1244
412
  return { content: [{ type: "text", text: `Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.` }] };
1245
413
  }
1246
414
 
1247
- // ── Single mode ───────────────────────────────────────────────────────────
415
+ // ── Single mode ─────────────────────────────────────────────────────
1248
416
  if (params.agent && params.task) {
1249
417
  const { agent, error } = findAgent(params.agent);
1250
418
  if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
1251
419
 
1252
- // Background dispatch — fire and forget
1253
420
  if (params.background) {
1254
421
  const bgAbort = new AbortController();
1255
422
  const handle: BackgroundHandleLike = { abort: () => bgAbort.abort() };
1256
423
  const resultPromise: Promise<BackgroundJobResult> = runAgent(
1257
- agent, params.task, cwd, params.model, bgAbort.signal, undefined
424
+ agent, params.task, cwd, params.model, bgAbort.signal, undefined,
1258
425
  ).then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
1259
426
  const jobId = getBgManager().adoptHandle(agent.name, params.task, cwd, handle, resultPromise);
1260
427
  return { content: [{ type: "text", text: `Background job started: ${jobId}\nTo check status, ask me to poll job ${jobId}.` }] };
1261
428
  }
1262
429
 
1263
- // Foreground run with detach support
1264
430
  const fgId = `fg_${randomUUID().slice(0, 8)}`;
1265
431
  const agentAbort = new AbortController();
1266
432
  const forwardAbort = () => agentAbort.abort();
@@ -1269,18 +435,15 @@ export default function (pi: ExtensionAPI) {
1269
435
  let detachResolveFn: ((bgJobId: string) => void) | null = null;
1270
436
  const detachPromise = new Promise<string>((resolve) => { detachResolveFn = resolve; });
1271
437
 
1272
- // Wrap onUpdate so detach can stop forwarding updates to the parent
1273
- // agent's listener (which becomes invalid once execute() returns).
1274
438
  let forwardUpdates = true;
1275
439
  const wrappedOnUpdate: OnUpdate | undefined = onUpdate
1276
- ? (partial) => { if (forwardUpdates) onUpdate(partial); }
440
+ ? (partial) => { if (forwardUpdates) (onUpdate as unknown as OnUpdate)(partial); }
1277
441
  : undefined;
1278
442
 
1279
443
  const agentRunPromise: Promise<RunResult> = runAgent(
1280
444
  agent, params.task, cwd, params.model, agentAbort.signal, wrappedOnUpdate,
1281
445
  );
1282
446
 
1283
- // Derived promise for the bg manager (used only if we detach)
1284
447
  const bgResultPromise: Promise<BackgroundJobResult> = agentRunPromise
1285
448
  .then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
1286
449
 
@@ -1311,7 +474,7 @@ export default function (pi: ExtensionAPI) {
1311
474
  });
1312
475
 
1313
476
  if (outcome === "detached") {
1314
- const bgJobId = await detachPromise; // already resolved — instant
477
+ const bgJobId = await detachPromise;
1315
478
  return {
1316
479
  content: [{ type: "text", text: `Moved to background: ${bgJobId}. Completion will be announced automatically.` }],
1317
480
  details: {
@@ -1336,12 +499,13 @@ export default function (pi: ExtensionAPI) {
1336
499
  elapsedMs: undefined,
1337
500
  model: result.model,
1338
501
  toolCalls: result.toolCalls,
502
+ executionEvents: result.executionEvents,
1339
503
  } satisfies SubagentDetails,
1340
504
  isError: result.exitCode !== 0,
1341
505
  };
1342
506
  }
1343
507
 
1344
- // ── Parallel mode ─────────────────────────────────────────────
508
+ // ── Parallel mode ───────────────────────────────────────────────────
1345
509
  if (params.tasks && params.tasks.length > 0) {
1346
510
  const expanded: Array<{ agent: string; task: string; model?: string; cwd?: string }> = [];
1347
511
  for (const t of params.tasks) {
@@ -1358,14 +522,14 @@ export default function (pi: ExtensionAPI) {
1358
522
  }));
1359
523
  let runningUsage = { ...emptyUsage };
1360
524
 
1361
- const emitParallel = (running: boolean) => onUpdate?.({
525
+ const emitParallel = (running: boolean) => (onUpdate as unknown as OnUpdate | undefined)?.({
1362
526
  content: [{ type: "text", text: "" }],
1363
527
  details: { mode: "parallel", parallelAgents: [...parallelAgents], usage: { ...runningUsage }, running, toolCalls: [] } satisfies SubagentDetails,
1364
528
  });
1365
529
 
1366
530
  emitParallel(true);
1367
531
 
1368
- const parentDepth = _currentDepth;
532
+ const parentDepth = getCurrentDepth();
1369
533
  const allResults = await mapConcurrent(expanded, concurrency, async (t, i) => {
1370
534
  parallelAgents[i]!.status = "running";
1371
535
  emitParallel(true);
@@ -1404,8 +568,6 @@ export default function (pi: ExtensionAPI) {
1404
568
  };
1405
569
  }
1406
570
 
1407
- // ── Chain mode ────────────────────────────────────────────
1408
- // Shouldn't reach here
1409
571
  return { content: [{ type: "text", text: "Provide agent+task or tasks array." }] };
1410
572
  },
1411
573
  });