pi-fast-subagent 0.1.1 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +1 -13
  2. package/index.ts +386 -205
  3. package/package.json +13 -2
package/README.md CHANGED
@@ -8,8 +8,7 @@ Runs subagents with `createAgentSession()` in same process instead of spawning `
8
8
 
9
9
  - Single mode: `{ agent, task }`
10
10
  - Parallel mode: `{ tasks: [...] }`
11
- - Chain mode: `{ chain: [...] }`
12
- - Per-call or per-step model override
11
+ - Per-call model override
13
12
  - User + project agent discovery
14
13
  - Project agents override user agents
15
14
  - Max nesting depth guard
@@ -108,17 +107,6 @@ subagent({
108
107
  })
109
108
  ```
110
109
 
111
- ### Chain
112
-
113
- ```js
114
- subagent({
115
- chain: [
116
- { agent: "scout", task: "Explore app structure" },
117
- { agent: "scout", task: "Based on this: {previous}\n\nExtract only auth flow." }
118
- ]
119
- })
120
- ```
121
-
122
110
  ## Notes
123
111
 
124
112
  - Async/background isolation not supported in-process
package/index.ts CHANGED
@@ -4,13 +4,20 @@
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
- * Drop-in replacement for pi-subagents subprocess mode.
8
- * Supports: single, parallel, chain.
7
+ * Supports: single, parallel.
9
8
  * Agent .md files are compatible with pi-subagents frontmatter format.
10
9
  */
11
10
 
12
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
13
- import { Text } from "@mariozechner/pi-tui";
11
+ import type {
12
+ AgentToolResult,
13
+ AgentToolUpdateCallback,
14
+ ExtensionAPI,
15
+ ExtensionContext,
16
+ ToolRenderResultOptions,
17
+ } from "@mariozechner/pi-coding-agent";
18
+ import { Theme } from "@mariozechner/pi-coding-agent";
19
+ import { truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
20
+ import { truncateToVisualLines, keyHint } from "@mariozechner/pi-coding-agent";
14
21
  import {
15
22
  AuthStorage,
16
23
  createAgentSession,
@@ -19,9 +26,70 @@ import {
19
26
  ModelRegistry,
20
27
  SessionManager,
21
28
  } from "@mariozechner/pi-coding-agent";
29
+
30
+ type DefaultResourceLoaderOptions = ConstructorParameters<typeof DefaultResourceLoader>[0];
22
31
  import { Type } from "@sinclair/typebox";
23
32
  import { type AgentConfig, discoverAgents } from "./agents.js";
24
33
 
34
+ // ─── Tool arg summarizer (compact one-liner per tool call) ─────────────────────
35
+
36
+ function shortPath(p: unknown): string {
37
+ if (typeof p !== "string") return "";
38
+ const cwd = process.cwd();
39
+ if (p.startsWith(cwd + "/")) return p.slice(cwd.length + 1);
40
+ return p.replace(/^\/Users\/[^/]+\/[^/]+\//, "");
41
+ }
42
+
43
+ function summarizeToolArgs(toolName: unknown, toolInput: unknown): string {
44
+ const name = String(toolName ?? "");
45
+ const input =
46
+ toolInput && typeof toolInput === "object" ? (toolInput as Record<string, unknown>) : {};
47
+ const filePath = (): string => shortPath(input.path ?? input.file_path) || "";
48
+ switch (name) {
49
+ case "Read":
50
+ case "read":
51
+ case "Write":
52
+ case "write":
53
+ case "Edit":
54
+ case "edit":
55
+ return filePath();
56
+ case "Bash":
57
+ case "bash": {
58
+ const cmd = String(input.command ?? "");
59
+ return cmd.length > 80 ? cmd.slice(0, 77) + "..." : cmd;
60
+ }
61
+ case "Glob":
62
+ case "glob":
63
+ return String(input.pattern ?? "");
64
+ case "find": {
65
+ const pat = String(input.pattern ?? "");
66
+ const p = shortPath(input.path);
67
+ return p ? `${pat} in ${p}` : pat;
68
+ }
69
+ case "Grep":
70
+ case "grep": {
71
+ const pat = String(input.pattern ?? "");
72
+ const g = input.glob ? ` ${input.glob}` : "";
73
+ return `${pat}${g}`;
74
+ }
75
+ case "ls":
76
+ return shortPath(input.path) || "";
77
+ case "subagent": {
78
+ const agent = String(input.agent ?? "");
79
+ const t = String(input.task ?? "");
80
+ const summary = t.length > 50 ? t.slice(0, 47) + "..." : t;
81
+ return agent ? `${agent}: ${summary}` : summary;
82
+ }
83
+ default: {
84
+ for (const v of Object.values(input)) {
85
+ if (typeof v === "string" && v.length > 0)
86
+ return v.length > 60 ? v.slice(0, 57) + "..." : v;
87
+ }
88
+ return "";
89
+ }
90
+ }
91
+ }
92
+
25
93
  // ─── Shared auth (created once, reused across calls) ─────────────────────────
26
94
 
27
95
  let _authStorage: ReturnType<typeof AuthStorage.create> | null = null;
@@ -38,14 +106,45 @@ function getAuth() {
38
106
  const MAX_DEPTH = 2;
39
107
  const DEPTH_ENV = "PI_FAST_SUBAGENT_DEPTH";
40
108
 
109
+ interface ToolCallEntry {
110
+ id: string;
111
+ name: string;
112
+ argSummary: string;
113
+ result?: string;
114
+ isError?: boolean;
115
+ durMs?: number;
116
+ }
117
+
41
118
  interface RunResult {
42
119
  output: string;
43
120
  exitCode: number;
44
121
  error?: string;
45
122
  model?: string;
123
+ toolCalls: ToolCallEntry[];
46
124
  usage: { input: number; output: number; cost: number; turns: number };
47
125
  }
48
126
 
127
+ interface AgentRowStatus {
128
+ name: string;
129
+ taskSummary: string;
130
+ status: "pending" | "running" | "done" | "error";
131
+ durMs?: number;
132
+ toolCalls?: ToolCallEntry[];
133
+ responseText?: string;
134
+ }
135
+
136
+ interface SubagentDetails {
137
+ mode?: "single" | "parallel";
138
+ task?: string;
139
+ // parallel
140
+ parallelAgents?: AgentRowStatus[];
141
+ usage: RunResult["usage"];
142
+ running: boolean;
143
+ elapsedMs?: number;
144
+ model?: string;
145
+ toolCalls: ToolCallEntry[];
146
+ }
147
+
49
148
  type OnUpdate = (partial: { content: [{ type: "text"; text: string }]; details: unknown }) => void;
50
149
 
51
150
  function formatDuration(ms: number): string {
@@ -55,6 +154,9 @@ function formatDuration(ms: number): string {
55
154
  return m > 0 ? `${m}m ${rem}s` : `${rem}s`;
56
155
  }
57
156
 
157
+ // Module-level depth counter — avoids process.env race conditions in parallel mode
158
+ let _currentDepth = 0;
159
+
58
160
  async function runAgent(
59
161
  agent: AgentConfig,
60
162
  task: string,
@@ -62,13 +164,15 @@ async function runAgent(
62
164
  modelOverride: string | undefined,
63
165
  signal: AbortSignal | undefined,
64
166
  onUpdate: OnUpdate | undefined,
167
+ parentDepth?: number,
65
168
  ): Promise<RunResult> {
66
- const depth = parseInt(process.env[DEPTH_ENV] ?? "0", 10);
169
+ const depth = parentDepth ?? _currentDepth;
67
170
  if (depth >= MAX_DEPTH) {
68
171
  return {
69
172
  output: "",
70
173
  exitCode: 1,
71
174
  error: `Max subagent depth (${MAX_DEPTH}) exceeded. Increase PI_FAST_SUBAGENT_DEPTH env to allow deeper nesting.`,
175
+ toolCalls: [],
72
176
  usage: { input: 0, output: 0, cost: 0, turns: 0 },
73
177
  };
74
178
  }
@@ -77,7 +181,7 @@ async function runAgent(
77
181
  const agentDir = getAgentDir();
78
182
 
79
183
  // Build resource loader — no extensions/context files to keep subagent lean
80
- const loaderOptions: ConstructorParameters<typeof DefaultResourceLoader>[0] = {
184
+ const loaderOptions: DefaultResourceLoaderOptions = {
81
185
  cwd,
82
186
  agentDir,
83
187
  noExtensions: true,
@@ -124,54 +228,78 @@ async function runAgent(
124
228
  let detectedModel: string | undefined;
125
229
  const startedAt = Date.now();
126
230
  const configuredModel = modelOverride ?? agent.model;
231
+ const toolCalls: ToolCallEntry[] = [];
232
+ const toolStartTimes = new Map<string, number>();
127
233
 
128
- onUpdate?.({
129
- content: [{ type: "text", text: "Starting subagent..." }],
130
- details: {
131
- agent: agent.name,
132
- usage,
133
- running: true,
134
- elapsedMs: 0,
135
- model: configuredModel,
136
- },
137
- });
138
-
139
- const heartbeat = setInterval(() => {
234
+ function emitUpdate(): void {
140
235
  onUpdate?.({
141
- content: [{ type: "text", text: currentDelta || lastOutput || "Running..." }],
236
+ content: [{ type: "text", text: currentDelta || lastOutput || "" }],
142
237
  details: {
143
- agent: agent.name,
238
+ task,
144
239
  usage,
145
240
  running: true,
146
241
  elapsedMs: Date.now() - startedAt,
147
242
  model: detectedModel ?? configuredModel,
148
- },
243
+ toolCalls: [...toolCalls],
244
+ } satisfies SubagentDetails,
149
245
  });
150
- }, 1000);
246
+ }
247
+
248
+ emitUpdate();
249
+
250
+ const heartbeat = setInterval(emitUpdate, 1000);
151
251
 
152
252
  const unsubscribe = session.subscribe((event: any) => {
253
+ // Stream tool execution events
254
+ if (event.type === "tool_execution_start") {
255
+ toolStartTimes.set(event.toolCallId, Date.now());
256
+ toolCalls.push({
257
+ id: event.toolCallId,
258
+ name: event.toolName,
259
+ argSummary: summarizeToolArgs(event.toolName, event.args),
260
+ });
261
+ emitUpdate();
262
+ return;
263
+ }
264
+
265
+ if (event.type === "tool_execution_end") {
266
+ const startedAtTool = toolStartTimes.get(event.toolCallId);
267
+ toolStartTimes.delete(event.toolCallId);
268
+ const resultText: string = (event.result?.content ?? [])
269
+ .filter((p: any) => p.type === "text")
270
+ .map((p: any) => p.text as string)
271
+ .join("\n");
272
+ let entry: ToolCallEntry | undefined;
273
+ for (let i = toolCalls.length - 1; i >= 0; i--) {
274
+ if (toolCalls[i]!.id === event.toolCallId) { entry = toolCalls[i]; break; }
275
+ }
276
+ if (!entry) {
277
+ for (let i = toolCalls.length - 1; i >= 0; i--) {
278
+ if (toolCalls[i]!.name === event.toolName && toolCalls[i]!.result === undefined) { entry = toolCalls[i]; break; }
279
+ }
280
+ }
281
+ if (entry) {
282
+ entry.result = resultText;
283
+ entry.isError = event.isError;
284
+ entry.durMs = startedAtTool != null ? Date.now() - startedAtTool : undefined;
285
+ }
286
+ emitUpdate();
287
+ return;
288
+ }
289
+
153
290
  // Stream text deltas live to the UI
154
291
  if (event.type === "message_update") {
155
292
  const e = event.assistantMessageEvent;
156
293
  if (e?.type === "text_delta" && e.delta) {
157
294
  currentDelta += e.delta;
158
- onUpdate?.({
159
- content: [{ type: "text", text: currentDelta }],
160
- details: {
161
- agent: agent.name,
162
- usage,
163
- running: true,
164
- elapsedMs: Date.now() - startedAt,
165
- model: detectedModel ?? configuredModel,
166
- },
167
- });
295
+ emitUpdate();
168
296
  }
169
297
  return;
170
298
  }
171
299
 
172
300
  if (event.type !== "message_end" || !event.message) return;
173
301
  const msg = event.message;
174
- if (msg.role !== "assistant") return;
302
+ if (msg.role !== "assistant") return; // usage/model only tracked for assistant turns
175
303
 
176
304
  usage.turns++;
177
305
  const u = msg.usage;
@@ -204,9 +332,10 @@ async function runAgent(
204
332
  });
205
333
  });
206
334
 
207
- // Propagate depth to any nested fast-subagent calls
208
- const prevDepth = process.env[DEPTH_ENV];
335
+ // Propagate depth to nested calls — use module counter (safe for parallel) + env for subprocess compat
336
+ const prevEnvDepth = process.env[DEPTH_ENV];
209
337
  process.env[DEPTH_ENV] = String(depth + 1);
338
+ _currentDepth = depth + 1;
210
339
 
211
340
  let exitCode = 0;
212
341
  let error: string | undefined;
@@ -228,11 +357,12 @@ async function runAgent(
228
357
  clearInterval(heartbeat);
229
358
  unsubscribe();
230
359
  session.dispose();
231
- if (prevDepth === undefined) delete process.env[DEPTH_ENV];
232
- else process.env[DEPTH_ENV] = prevDepth;
360
+ if (prevEnvDepth === undefined) delete process.env[DEPTH_ENV];
361
+ else process.env[DEPTH_ENV] = prevEnvDepth;
362
+ _currentDepth = depth;
233
363
  }
234
364
 
235
- return { output: lastOutput, exitCode, error, model: detectedModel, usage };
365
+ return { output: lastOutput, exitCode, error, model: detectedModel, toolCalls, usage };
236
366
  }
237
367
 
238
368
  // ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -289,19 +419,6 @@ const TaskItem = Type.Object({
289
419
  count: Type.Optional(Type.Number({ description: "Repeat this task N times" })),
290
420
  });
291
421
 
292
- const ChainItem = Type.Object({
293
- agent: Type.String({ description: "Agent name" }),
294
- task: Type.Optional(
295
- Type.String({
296
- description:
297
- "Task template. Supports {previous} (output from prior step) and {task} (first step task). " +
298
- "Defaults to {previous} for steps 2+.",
299
- }),
300
- ),
301
- model: Type.Optional(Type.String({ description: "Model override (provider/model)" })),
302
- cwd: Type.Optional(Type.String({ description: "Working directory" })),
303
- });
304
-
305
422
  const SubagentParams = Type.Object({
306
423
  // Single mode
307
424
  agent: Type.Optional(Type.String({ description: "Agent name (single mode)" })),
@@ -319,13 +436,6 @@ const SubagentParams = Type.Object({
319
436
  Type.Number({ description: "Max parallel concurrency (default: 4)", default: 4 }),
320
437
  ),
321
438
 
322
- // Chain mode
323
- chain: Type.Optional(
324
- Type.Array(ChainItem, {
325
- description: "Sequential chain. Use {previous} in task to receive prior step output.",
326
- }),
327
- ),
328
-
329
439
  // Management
330
440
  action: Type.Optional(
331
441
  Type.Union(
@@ -352,41 +462,186 @@ export default function (pi: ExtensionAPI) {
352
462
  label: "Subagent",
353
463
  description: [
354
464
  "Delegate tasks to specialized subagents. Runs IN-PROCESS — no subprocess cold-start overhead.",
355
- "Modes: single ({ agent, task }), parallel ({ tasks: [...] }), chain ({ chain: [...] }).",
356
- "Chain supports {task} (first step task) and {previous} (prior step output) template vars.",
465
+ "Modes: single ({ agent, task }), parallel ({ tasks: [...] }).",
357
466
  "Agents defined as .md files in ~/.pi/agent/agents/ (user) or .pi/agents/ (project).",
358
467
  "Use { action: 'list' } to discover available agents.",
359
468
  ].join(" "),
360
469
  parameters: SubagentParams,
361
470
 
362
- renderResult(result, { isPartial }, theme) {
363
- const text = result.content?.[0]?.type === "text" ? result.content[0].text : "";
364
- const details = (result.details ?? {}) as {
365
- usage?: RunResult["usage"];
366
- running?: boolean;
367
- elapsedMs?: number;
368
- model?: string;
369
- };
471
+ renderResult(result: AgentToolResult<unknown>, { isPartial, expanded }: ToolRenderResultOptions, theme: Theme) {
472
+ const agentText = result.content?.[0]?.type === "text" ? (result.content[0] as any).text as string : "";
473
+ const details = (result.details ?? {}) as SubagentDetails;
474
+ const toolCalls = details.toolCalls ?? [];
475
+
476
+ // ── Parallel / Chain mode renders ────────────────────────────────
477
+ if (details.mode === "parallel" && details.parallelAgents) {
478
+ const agents = details.parallelAgents;
479
+ const doneCount = agents.filter((a) => a.status === "done" || a.status === "error").length;
480
+
481
+ function agentToolRow(t: ToolCallEntry): string {
482
+ const arg = t.argSummary || "";
483
+ const call = `${t.name}(${arg})`;
484
+ if (t.result === undefined) return theme.fg("dim", call);
485
+ const dur = t.durMs != null ? (t.durMs < 1000 ? ` ${t.durMs}ms` : ` ${(t.durMs / 1000).toFixed(1)}s`) : "";
486
+ return `${call}${t.isError ? " ✗" : ` ✓${dur}`}`;
487
+ }
488
+
489
+ function wrapL(text: string, w: number): string[] {
490
+ try { return wrapTextWithAnsi(text, w); } catch { return [truncateToWidth(text, w, "...")]; }
491
+ }
370
492
 
371
- let status = "";
372
- if (details.running) {
373
- const statusParts: string[] = ["running"];
374
- if (details.usage?.turns) statusParts.push(`${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}`);
375
- if (details.elapsedMs !== undefined) statusParts.push(formatDuration(details.elapsedMs));
376
- if (details.model) statusParts.push(details.model);
377
- status = `\n${statusParts.join(" · ")}`;
378
- } else if (details.usage) {
379
- const usageStr = formatUsage(details.usage, details.model);
380
- if (usageStr) status = `\n${usageStr}`;
493
+ const cache: { width?: number } = {};
494
+ return {
495
+ invalidate() { cache.width = undefined; },
496
+ render(width: number): string[] {
497
+ const out: string[] = [];
498
+ const header = details.running
499
+ ? `Parallel (${doneCount}/${agents.length} done)`
500
+ : `Parallel: ${agents.filter((a) => a.status === "done").length}/${agents.length} succeeded`;
501
+ out.push(truncateToWidth(header, width, "..."));
502
+
503
+ for (const a of agents) {
504
+ const dur = a.durMs != null ? (a.durMs < 1000 ? ` ${a.durMs}ms` : ` ${(a.durMs / 1000).toFixed(1)}s`) : "";
505
+ const mark = a.status === "pending" ? theme.fg("dim", "⋅") : a.status === "running" ? theme.fg("dim", "→") : a.status === "done" ? `✓${dur}` : `✗${dur}`;
506
+
507
+ if (expanded) {
508
+ // Full solo-style block per agent
509
+ out.push("");
510
+ out.push(truncateToWidth(`[${a.name}] ${mark}`, width, "..."));
511
+ out.push(truncateToWidth(`Prompt:`, width, "..."));
512
+ out.push(truncateToWidth(` ${a.taskSummary}`, width, "..."));
513
+ for (const t of a.toolCalls ?? []) {
514
+ out.push(truncateToWidth(agentToolRow(t), width, "..."));
515
+ }
516
+ if (a.responseText) {
517
+ out.push("Response:");
518
+ const preview = truncateToVisualLines(a.responseText, 6, width - 2);
519
+ for (const l of preview.visualLines) out.push(truncateToWidth(" " + l, width, "..."));
520
+ if (preview.skippedCount > 0) out.push(truncateToWidth(theme.fg("dim", ` … ${preview.skippedCount} more lines`), width, "..."));
521
+ } else if (a.status === "running") {
522
+ out.push(theme.fg("dim", " running..."));
523
+ }
524
+ } else {
525
+ // Collapsed: compact one-liner
526
+ const row = ` [${a.name}] ${mark} ${a.taskSummary}`;
527
+ out.push(truncateToWidth(row, width, "..."));
528
+ // Show tool call rows compactly
529
+ for (const t of a.toolCalls ?? []) {
530
+ out.push(truncateToWidth(` ${agentToolRow(t)}`, width, "..."));
531
+ }
532
+ if (a.responseText && (a.status === "done" || a.status === "error")) {
533
+ const preview = truncateToVisualLines(a.responseText, 2, width - 4);
534
+ for (const l of preview.visualLines) out.push(truncateToWidth(" " + l, width, "..."));
535
+ }
536
+ }
537
+ }
538
+
539
+ out.push("");
540
+ const status = details.running
541
+ ? ["running", details.usage?.turns ? `${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}` : ""].filter(Boolean).join(" · ")
542
+ : formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
543
+ const expandHint = !expanded ? keyHint("app.tools.expand", "expand for full output") : "";
544
+ out.push(truncateToWidth([status, expandHint].filter(Boolean).join(" "), width, "..."));
545
+ return out;
546
+ },
547
+ };
381
548
  }
382
549
 
383
- if (isPartial) {
384
- return new Text(theme.fg("dim", (text || "Running...") + status), 0, 0);
550
+ function statusLine(): string {
551
+ if (details.running) {
552
+ const parts: string[] = ["running"];
553
+ if (details.usage?.turns) parts.push(`${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}`);
554
+ if (details.elapsedMs != null) parts.push(formatDuration(details.elapsedMs));
555
+ if (details.model) parts.push(details.model);
556
+ return parts.join(" · ");
557
+ }
558
+ return formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
385
559
  }
386
- return new Text(text + status, 0, 0);
560
+
561
+ // Name(arg) ✓ 0.3s or Name(arg) (dim, still running)
562
+ function toolRow(t: ToolCallEntry): string {
563
+ const arg = t.argSummary ? t.argSummary : "";
564
+ const call = `${t.name}(${arg})`;
565
+ if (t.result === undefined) return theme.fg("dim", call);
566
+ const dur = t.durMs != null
567
+ ? t.durMs < 1000 ? ` ${t.durMs}ms` : ` ${(t.durMs / 1000).toFixed(1)}s`
568
+ : "";
569
+ return `${call}${t.isError ? " ✗" : ` ✓${dur}`}`;
570
+ }
571
+
572
+ function wrapLine(text: string, w: number): string[] {
573
+ try { return wrapTextWithAnsi(text, w); } catch { return [truncateToWidth(text, w, "...")]; }
574
+ }
575
+
576
+ const cache: { width?: number; responseLines?: string[]; skipped?: number } = {};
577
+
578
+ return {
579
+ invalidate() { cache.width = undefined; },
580
+ render(width: number): string[] {
581
+ const out: string[] = [];
582
+ const indent = " ";
583
+
584
+ // ── Prompt ────────────────────────────────────────────────────
585
+ if (details.task) {
586
+ out.push("Prompt:");
587
+ const taskLines = details.task.split("\n");
588
+ if (expanded) {
589
+ for (const line of taskLines) {
590
+ for (const w of wrapLine(indent + line, width)) out.push(w);
591
+ }
592
+ } else {
593
+ // Single truncated line in collapsed
594
+ const oneLiner = taskLines[0] ?? "";
595
+ out.push(truncateToWidth(indent + oneLiner, width, "..."));
596
+ }
597
+ }
598
+
599
+ // ── Tool calls ─────────────────────────────────────────────
600
+ for (const t of toolCalls) {
601
+ out.push(truncateToWidth(toolRow(t), width, "..."));
602
+ if (expanded && t.result !== undefined) {
603
+ for (const line of t.result.split("\n")) {
604
+ for (const w of wrapLine(theme.fg("dim", indent + line), width)) out.push(w);
605
+ }
606
+ }
607
+ }
608
+
609
+ // ── Response ────────────────────────────────────────────
610
+ const responseText = agentText || (isPartial ? "" : "");
611
+ if (responseText || isPartial) {
612
+ out.push("Response:");
613
+ if (expanded) {
614
+ for (const line of responseText.split("\n")) {
615
+ for (const w of wrapLine(indent + line, width)) out.push(w);
616
+ }
617
+ } else {
618
+ const PREVIEW_LINES = 6;
619
+ if (cache.width !== width) {
620
+ const preview = truncateToVisualLines(responseText, PREVIEW_LINES, width - indent.length);
621
+ cache.responseLines = preview.visualLines.map((l) => truncateToWidth(indent + l, width, "..."));
622
+ cache.skipped = preview.skippedCount;
623
+ cache.width = width;
624
+ }
625
+ out.push(...(cache.responseLines ?? []));
626
+ }
627
+ }
628
+
629
+ // ── Status ───────────────────────────────────────────────
630
+ const status = statusLine();
631
+ const expandHint = !expanded && (cache.skipped ?? 0) > 0
632
+ ? keyHint("app.tools.expand", `expand · ${cache.skipped} lines hidden`)
633
+ : !expanded && toolCalls.some((t) => t.result !== undefined)
634
+ ? keyHint("app.tools.expand", "expand for tool outputs")
635
+ : "";
636
+ const statusWithHint = [status, expandHint].filter(Boolean).join(" ");
637
+ if (statusWithHint) out.push(truncateToWidth(statusWithHint, width, "..."));
638
+
639
+ return out;
640
+ },
641
+ };
387
642
  },
388
643
 
389
- async execute(_id, params, signal, onUpdate, ctx) {
644
+ async execute(_id: string, params: Record<string, any>, signal: AbortSignal | undefined, onUpdate: AgentToolUpdateCallback<unknown> | undefined, ctx: ExtensionContext): Promise<any> {
390
645
  const cwd = params.cwd ?? ctx.cwd;
391
646
  const agents = discoverAgents(cwd);
392
647
 
@@ -400,7 +655,7 @@ export default function (pi: ExtensionAPI) {
400
655
  };
401
656
 
402
657
  // ── Management: list ──────────────────────────────────────────────────────
403
- if (params.action === "list" || (!params.agent && !params.tasks && !params.chain)) {
658
+ if (params.action === "list" || (!params.agent && !params.tasks)) {
404
659
  if (agents.length === 0) {
405
660
  return {
406
661
  content: [{
@@ -446,18 +701,19 @@ export default function (pi: ExtensionAPI) {
446
701
  return {
447
702
  content: [{ type: "text", text: getFinalText(result) }],
448
703
  details: {
704
+ task: params.task,
449
705
  usage: result.usage,
450
706
  running: false,
451
707
  elapsedMs: undefined,
452
708
  model: result.model,
453
- },
709
+ toolCalls: result.toolCalls,
710
+ } satisfies SubagentDetails,
454
711
  isError: result.exitCode !== 0,
455
712
  };
456
713
  }
457
714
 
458
- // ── Parallel mode ─────────────────────────────────────────────────────────
715
+ // ── Parallel mode ─────────────────────────────────────────────
459
716
  if (params.tasks && params.tasks.length > 0) {
460
- // Expand count shorthand
461
717
  const expanded: Array<{ agent: string; task: string; model?: string; cwd?: string }> = [];
462
718
  for (const t of params.tasks) {
463
719
  const n = t.count ?? 1;
@@ -465,138 +721,63 @@ export default function (pi: ExtensionAPI) {
465
721
  }
466
722
 
467
723
  const concurrency = params.concurrency ?? 4;
468
- let doneCount = 0;
469
-
470
- const allResults = await mapConcurrent(
471
- expanded,
472
- concurrency,
473
- async (t, _i) => {
474
- const { agent, error } = findAgent(t.agent);
475
- if (error || !agent) {
476
- return { agentName: t.agent, output: "", exitCode: 1, error, model: undefined, usage: { input: 0, output: 0, cost: 0, turns: 0 } };
477
- }
478
- const result = await runAgent(agent, t.task, t.cwd ?? cwd, t.model, signal, undefined);
479
- doneCount++;
480
- onUpdate?.({
481
- content: [{ type: "text", text: `Parallel: ${doneCount}/${expanded.length} done...` }],
482
- details: {},
483
- });
484
- return { ...result, agentName: t.agent };
485
- },
486
- );
487
-
488
- const successCount = allResults.filter((r) => r.exitCode === 0).length;
489
- const summaries = allResults.map((r) => {
490
- const out = getFinalText(r);
491
- const preview = out.length > 300 ? `${out.slice(0, 300)}...` : out;
492
- return `**[${r.agentName}]** ${r.exitCode === 0 ? "✓" : "✗"}\n${preview}`;
724
+ const emptyUsage = { input: 0, output: 0, cost: 0, turns: 0 };
725
+ const parallelAgents: AgentRowStatus[] = expanded.map((t) => ({
726
+ name: t.agent,
727
+ taskSummary: t.task.length > 60 ? t.task.slice(0, 57) + "..." : t.task,
728
+ status: "pending" as const,
729
+ }));
730
+ let runningUsage = { ...emptyUsage };
731
+
732
+ const emitParallel = (running: boolean) => onUpdate?.({
733
+ content: [{ type: "text", text: "" }],
734
+ details: { mode: "parallel", parallelAgents: [...parallelAgents], usage: { ...runningUsage }, running, toolCalls: [] } satisfies SubagentDetails,
493
735
  });
494
- const totalUsage = allResults.reduce(
495
- (acc, r) => ({
496
- input: acc.input + r.usage.input,
497
- output: acc.output + r.usage.output,
498
- cost: acc.cost + r.usage.cost,
499
- turns: acc.turns + r.usage.turns,
500
- }),
501
- { input: 0, output: 0, cost: 0, turns: 0 },
502
- );
503
-
504
- return {
505
- content: [{
506
- type: "text",
507
- text: [
508
- `Parallel: ${successCount}/${allResults.length} succeeded`,
509
- "",
510
- summaries.join("\n\n"),
511
- "",
512
- formatUsage(totalUsage),
513
- ].join("\n"),
514
- }],
515
- };
516
- }
517
-
518
- // ── Chain mode ────────────────────────────────────────────────────────────
519
- if (params.chain && params.chain.length > 0) {
520
- const firstTask = params.chain[0]?.task ?? "";
521
- let previousOutput = "";
522
736
 
523
- const stepResults: Array<RunResult & { agentName: string; step: number }> = [];
737
+ emitParallel(true);
524
738
 
525
- for (let i = 0; i < params.chain.length; i++) {
526
- const step = params.chain[i];
527
- const { agent, error } = findAgent(step.agent);
739
+ const parentDepth = _currentDepth;
740
+ const allResults = await mapConcurrent(expanded, concurrency, async (t, i) => {
741
+ parallelAgents[i]!.status = "running";
742
+ emitParallel(true);
743
+ const { agent, error } = findAgent(t.agent);
528
744
  if (error || !agent) {
529
- return {
530
- content: [{ type: "text", text: `Chain stopped at step ${i + 1}: ${error ?? "Not found"}` }],
531
- isError: true,
532
- };
533
- }
534
-
535
- // Resolve task template
536
- let task = step.task ?? (i === 0 ? firstTask : "{previous}");
537
- task = task
538
- .replace(/\{previous\}/g, previousOutput)
539
- .replace(/\{task\}/g, firstTask);
540
-
541
- if (onUpdate) {
542
- onUpdate({
543
- content: [{
544
- type: "text",
545
- text: `Chain step ${i + 1}/${params.chain.length}: ${step.agent}...`,
546
- }],
547
- details: {},
548
- });
549
- }
550
-
551
- const result = await runAgent(
552
- agent,
553
- task,
554
- step.cwd ?? cwd,
555
- step.model,
556
- signal,
557
- onUpdate,
558
- );
559
-
560
- stepResults.push({ ...result, agentName: step.agent, step: i + 1 });
561
-
562
- if (result.exitCode !== 0) {
563
- return {
564
- content: [{
565
- type: "text",
566
- text: `Chain failed at step ${i + 1} (${step.agent}): ${result.error ?? "(no output)"}`,
567
- }],
568
- isError: true,
569
- };
745
+ parallelAgents[i]!.status = "error";
746
+ emitParallel(true);
747
+ return { agentName: t.agent, output: "", exitCode: 1, error, model: undefined, toolCalls: [] as ToolCallEntry[], usage: emptyUsage };
570
748
  }
749
+ const agentStart = Date.now();
750
+ const agentOnUpdate: OnUpdate = (partial) => {
751
+ const d = partial.details as SubagentDetails | undefined;
752
+ parallelAgents[i]!.toolCalls = d?.toolCalls ? [...d.toolCalls] : parallelAgents[i]!.toolCalls;
753
+ parallelAgents[i]!.responseText = (partial.content?.[0] as any)?.text || parallelAgents[i]!.responseText;
754
+ emitParallel(true);
755
+ };
756
+ const result = await runAgent(agent, t.task, t.cwd ?? cwd, t.model, signal, agentOnUpdate, parentDepth);
757
+ parallelAgents[i]!.status = result.exitCode === 0 ? "done" : "error";
758
+ parallelAgents[i]!.durMs = Date.now() - agentStart;
759
+ parallelAgents[i]!.toolCalls = result.toolCalls;
760
+ parallelAgents[i]!.responseText = result.output;
761
+ runningUsage = { input: runningUsage.input + result.usage.input, output: runningUsage.output + result.usage.output, cost: runningUsage.cost + result.usage.cost, turns: runningUsage.turns + result.usage.turns };
762
+ emitParallel(true);
763
+ return { ...result, agentName: t.agent, toolCalls: result.toolCalls ?? [] };
764
+ });
571
765
 
572
- previousOutput = result.output;
573
- }
574
-
575
- const last = stepResults[stepResults.length - 1];
576
- const totalUsage = stepResults.reduce(
577
- (acc, r) => ({
578
- input: acc.input + r.usage.input,
579
- output: acc.output + r.usage.output,
580
- cost: acc.cost + r.usage.cost,
581
- turns: acc.turns + r.usage.turns,
582
- }),
583
- { input: 0, output: 0, cost: 0, turns: 0 },
766
+ const totalUsage = allResults.reduce(
767
+ (acc, r) => ({ input: acc.input + r.usage.input, output: acc.output + r.usage.output, cost: acc.cost + r.usage.cost, turns: acc.turns + r.usage.turns }),
768
+ emptyUsage,
584
769
  );
770
+ const outputs = allResults.map((r) => `[${r.agentName}] ${r.exitCode === 0 ? "✓" : "✗"}\n${getFinalText(r)}`).join("\n\n");
585
771
 
586
772
  return {
587
- content: [{
588
- type: "text",
589
- text: [
590
- last.output,
591
- "",
592
- `Chain: ${stepResults.length} steps · ${formatUsage(totalUsage)}`,
593
- ].join("\n"),
594
- }],
773
+ content: [{ type: "text", text: outputs }],
774
+ details: { mode: "parallel", parallelAgents, usage: totalUsage, running: false, toolCalls: [] } satisfies SubagentDetails,
595
775
  };
596
776
  }
597
777
 
778
+ // ── Chain mode ────────────────────────────────────────────
598
779
  // Shouldn't reach here
599
- return { content: [{ type: "text", text: "Provide agent+task, tasks array, or chain array." }] };
780
+ return { content: [{ type: "text", text: "Provide agent+task or tasks array." }] };
600
781
  },
601
782
  });
602
783
  }
package/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "pi-fast-subagent",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "In-process subagent delegation for pi with single, parallel, and chain modes",
5
5
  "type": "module",
6
- "keywords": ["pi-package", "pi", "subagent", "agents", "extension"],
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi",
9
+ "subagent",
10
+ "agents",
11
+ "extension"
12
+ ],
7
13
  "homepage": "https://github.com/tuansondinh/pi-fast-subagent#readme",
8
14
  "repository": {
9
15
  "type": "git",
@@ -28,5 +34,10 @@
28
34
  "@mariozechner/pi-tui": "*",
29
35
  "@sinclair/typebox": "*"
30
36
  },
37
+ "devDependencies": {
38
+ "@mariozechner/pi-coding-agent": "^0.68.0",
39
+ "@mariozechner/pi-tui": "^0.68.0",
40
+ "@sinclair/typebox": "^0.34.41"
41
+ },
31
42
  "license": "MIT"
32
43
  }