@oh-my-pi/subagents 0.1.0 → 0.4.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.
@@ -34,7 +34,11 @@ import * as readline from "node:readline";
34
34
  import { Type } from "@sinclair/typebox";
35
35
  import { StringEnum, type AgentToolUpdateCallback } from "@mariozechner/pi-ai";
36
36
  import { Text } from "@mariozechner/pi-tui";
37
- import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
37
+ import type {
38
+ CustomAgentTool,
39
+ CustomToolFactory,
40
+ ToolAPI,
41
+ } from "@mariozechner/pi-coding-agent";
38
42
 
39
43
  const MAX_OUTPUT_LINES = 5000;
40
44
  const MAX_OUTPUT_BYTES = 500_000;
@@ -45,612 +49,660 @@ const MAX_AGENTS_IN_DESCRIPTION = 10;
45
49
  type AgentScope = "user" | "project" | "both";
46
50
 
47
51
  interface AgentConfig {
48
- name: string;
49
- description: string;
50
- tools?: string[];
51
- model?: string;
52
- forkContext?: boolean;
53
- systemPrompt: string;
54
- source: "user" | "project";
55
- filePath: string;
52
+ name: string;
53
+ description: string;
54
+ tools?: string[];
55
+ model?: string;
56
+ forkContext?: boolean;
57
+ systemPrompt: string;
58
+ source: "user" | "project";
59
+ filePath: string;
56
60
  }
57
61
 
58
62
  interface AgentProgress {
59
- agent: string;
60
- agentSource: "user" | "project" | "unknown";
61
- status: "running" | "completed" | "failed";
62
- task: string;
63
- currentTool?: string;
64
- currentToolDescription?: string;
65
- toolCount: number;
66
- tokens: number;
67
- durationMs: number;
68
- step?: number;
69
- index: number;
70
- modelOverride?: string;
63
+ agent: string;
64
+ agentSource: "user" | "project" | "unknown";
65
+ status: "running" | "completed" | "failed";
66
+ task: string;
67
+ currentTool?: string;
68
+ currentToolDescription?: string;
69
+ toolCount: number;
70
+ tokens: number;
71
+ durationMs: number;
72
+ step?: number;
73
+ index: number;
74
+ modelOverride?: string;
71
75
  }
72
76
 
73
77
  interface SingleResult {
74
- agent: string;
75
- agentSource: "user" | "project" | "unknown";
76
- task: string;
77
- exitCode: number;
78
- stdout: string;
79
- stderr: string;
80
- truncated: boolean;
81
- durationMs: number;
82
- step?: number;
83
- modelOverride?: string;
78
+ agent: string;
79
+ agentSource: "user" | "project" | "unknown";
80
+ task: string;
81
+ exitCode: number;
82
+ stdout: string;
83
+ stderr: string;
84
+ truncated: boolean;
85
+ durationMs: number;
86
+ step?: number;
87
+ modelOverride?: string;
84
88
  }
85
89
 
86
90
  interface TaskDetails {
87
- agentScope: AgentScope;
88
- projectAgentsDir: string | null;
89
- results: SingleResult[];
90
- totalDurationMs: number;
91
- /** Output paths when write=true */
92
- outputPaths?: string[];
93
- /** For streaming progress updates */
94
- progress?: AgentProgress[];
91
+ agentScope: AgentScope;
92
+ projectAgentsDir: string | null;
93
+ results: SingleResult[];
94
+ totalDurationMs: number;
95
+ /** Output paths when write=true */
96
+ outputPaths?: string[];
97
+ /** For streaming progress updates */
98
+ progress?: AgentProgress[];
95
99
  }
96
100
 
97
- function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
98
- const frontmatter: Record<string, string> = {};
99
- const normalized = content.replace(/\r\n/g, "\n");
100
-
101
- if (!normalized.startsWith("---")) {
102
- return { frontmatter, body: normalized };
103
- }
104
-
105
- const endIndex = normalized.indexOf("\n---", 3);
106
- if (endIndex === -1) {
107
- return { frontmatter, body: normalized };
108
- }
109
-
110
- const frontmatterBlock = normalized.slice(4, endIndex);
111
- const body = normalized.slice(endIndex + 4).trim();
112
-
113
- for (const line of frontmatterBlock.split("\n")) {
114
- const match = line.match(/^([\w-]+):\s*(.*)$/);
115
- if (match) {
116
- let value = match[2].trim();
117
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
118
- value = value.slice(1, -1);
119
- }
120
- frontmatter[match[1]] = value;
101
+ function parseFrontmatter(content: string): {
102
+ frontmatter: Record<string, string>;
103
+ body: string;
104
+ } {
105
+ const frontmatter: Record<string, string> = {};
106
+ const normalized = content.replace(/\r\n/g, "\n");
107
+
108
+ if (!normalized.startsWith("---")) {
109
+ return { frontmatter, body: normalized };
110
+ }
111
+
112
+ const endIndex = normalized.indexOf("\n---", 3);
113
+ if (endIndex === -1) {
114
+ return { frontmatter, body: normalized };
115
+ }
116
+
117
+ const frontmatterBlock = normalized.slice(4, endIndex);
118
+ const body = normalized.slice(endIndex + 4).trim();
119
+
120
+ for (const line of frontmatterBlock.split("\n")) {
121
+ const match = line.match(/^([\w-]+):\s*(.*)$/);
122
+ if (match) {
123
+ let value = match[2].trim();
124
+ if (
125
+ (value.startsWith('"') && value.endsWith('"')) ||
126
+ (value.startsWith("'") && value.endsWith("'"))
127
+ ) {
128
+ value = value.slice(1, -1);
121
129
  }
122
- }
130
+ frontmatter[match[1]] = value;
131
+ }
132
+ }
123
133
 
124
- return { frontmatter, body };
134
+ return { frontmatter, body };
125
135
  }
126
136
 
127
- function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
128
- const agents: AgentConfig[] = [];
129
-
130
- if (!fs.existsSync(dir)) {
131
- return agents;
132
- }
133
-
134
- let entries: fs.Dirent[];
135
- try {
136
- entries = fs.readdirSync(dir, { withFileTypes: true });
137
- } catch {
138
- return agents;
139
- }
140
-
141
- for (const entry of entries) {
142
- if (!entry.name.endsWith(".md")) continue;
143
-
144
- const filePath = path.join(dir, entry.name);
145
-
146
- // Handle both regular files and symlinks (statSync follows symlinks)
147
- try {
148
- if (!fs.statSync(filePath).isFile()) continue;
149
- } catch {
150
- continue;
151
- }
152
- let content: string;
153
- try {
154
- content = fs.readFileSync(filePath, "utf-8");
155
- } catch {
156
- continue;
157
- }
158
-
159
- const { frontmatter, body } = parseFrontmatter(content);
160
-
161
- if (!frontmatter.name || !frontmatter.description) {
162
- continue;
163
- }
164
-
165
- const tools = frontmatter.tools
166
- ?.split(",")
167
- .map((t) => t.trim())
168
- .filter(Boolean);
169
-
170
- const forkContext = frontmatter.forkContext === undefined
171
- ? undefined
172
- : frontmatter.forkContext === "true" || frontmatter.forkContext === "1";
173
-
174
- agents.push({
175
- name: frontmatter.name,
176
- description: frontmatter.description,
177
- tools: tools && tools.length > 0 ? tools : undefined,
178
- model: frontmatter.model,
179
- forkContext,
180
- systemPrompt: body,
181
- source,
182
- filePath,
183
- });
184
- }
185
-
186
- return agents;
137
+ function loadAgentsFromDir(
138
+ dir: string,
139
+ source: "user" | "project",
140
+ ): AgentConfig[] {
141
+ const agents: AgentConfig[] = [];
142
+
143
+ if (!fs.existsSync(dir)) {
144
+ return agents;
145
+ }
146
+
147
+ let entries: fs.Dirent[];
148
+ try {
149
+ entries = fs.readdirSync(dir, { withFileTypes: true });
150
+ } catch {
151
+ return agents;
152
+ }
153
+
154
+ for (const entry of entries) {
155
+ if (!entry.name.endsWith(".md")) continue;
156
+
157
+ const filePath = path.join(dir, entry.name);
158
+
159
+ // Handle both regular files and symlinks (statSync follows symlinks)
160
+ try {
161
+ if (!fs.statSync(filePath).isFile()) continue;
162
+ } catch {
163
+ continue;
164
+ }
165
+ let content: string;
166
+ try {
167
+ content = fs.readFileSync(filePath, "utf-8");
168
+ } catch {
169
+ continue;
170
+ }
171
+
172
+ const { frontmatter, body } = parseFrontmatter(content);
173
+
174
+ if (!frontmatter.name || !frontmatter.description) {
175
+ continue;
176
+ }
177
+
178
+ const tools = frontmatter.tools
179
+ ?.split(",")
180
+ .map((t) => t.trim())
181
+ .filter(Boolean);
182
+
183
+ const forkContext =
184
+ frontmatter.forkContext === undefined
185
+ ? undefined
186
+ : frontmatter.forkContext === "true" || frontmatter.forkContext === "1";
187
+
188
+ agents.push({
189
+ name: frontmatter.name,
190
+ description: frontmatter.description,
191
+ tools: tools && tools.length > 0 ? tools : undefined,
192
+ model: frontmatter.model,
193
+ forkContext,
194
+ systemPrompt: body,
195
+ source,
196
+ filePath,
197
+ });
198
+ }
199
+
200
+ return agents;
187
201
  }
188
202
 
189
203
  function isDirectory(p: string): boolean {
190
- try {
191
- return fs.statSync(p).isDirectory();
192
- } catch {
193
- return false;
194
- }
204
+ try {
205
+ return fs.statSync(p).isDirectory();
206
+ } catch {
207
+ return false;
208
+ }
195
209
  }
196
210
 
197
211
  function findNearestProjectAgentsDir(cwd: string): string | null {
198
- let currentDir = cwd;
199
- while (true) {
200
- const candidate = path.join(currentDir, ".pi", "agents");
201
- if (isDirectory(candidate)) return candidate;
202
-
203
- const parentDir = path.dirname(currentDir);
204
- if (parentDir === currentDir) return null;
205
- currentDir = parentDir;
206
- }
212
+ let currentDir = cwd;
213
+ while (true) {
214
+ const candidate = path.join(currentDir, ".pi", "agents");
215
+ if (isDirectory(candidate)) return candidate;
216
+
217
+ const parentDir = path.dirname(currentDir);
218
+ if (parentDir === currentDir) return null;
219
+ currentDir = parentDir;
220
+ }
207
221
  }
208
222
 
209
- function discoverAgents(cwd: string, scope: AgentScope): { agents: AgentConfig[]; projectAgentsDir: string | null } {
210
- const userDir = path.join(os.homedir(), ".pi", "agent", "agents");
211
- const projectAgentsDir = findNearestProjectAgentsDir(cwd);
212
-
213
- const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
214
- const projectAgents =
215
- scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
216
-
217
- const agentMap = new Map<string, AgentConfig>();
218
-
219
- if (scope === "both") {
220
- // Explicit opt-in: project agents override user agents with the same name.
221
- for (const agent of userAgents) agentMap.set(agent.name, agent);
222
- for (const agent of projectAgents) agentMap.set(agent.name, agent);
223
- } else if (scope === "user") {
224
- for (const agent of userAgents) agentMap.set(agent.name, agent);
225
- } else {
226
- for (const agent of projectAgents) agentMap.set(agent.name, agent);
227
- }
228
-
229
- return { agents: Array.from(agentMap.values()), projectAgentsDir };
223
+ function discoverAgents(
224
+ cwd: string,
225
+ scope: AgentScope,
226
+ ): { agents: AgentConfig[]; projectAgentsDir: string | null } {
227
+ const userDir = path.join(os.homedir(), ".pi", "agent", "agents");
228
+ const projectAgentsDir = findNearestProjectAgentsDir(cwd);
229
+
230
+ const userAgents =
231
+ scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
232
+ const projectAgents =
233
+ scope === "user" || !projectAgentsDir
234
+ ? []
235
+ : loadAgentsFromDir(projectAgentsDir, "project");
236
+
237
+ const agentMap = new Map<string, AgentConfig>();
238
+
239
+ if (scope === "both") {
240
+ // Explicit opt-in: project agents override user agents with the same name.
241
+ for (const agent of userAgents) agentMap.set(agent.name, agent);
242
+ for (const agent of projectAgents) agentMap.set(agent.name, agent);
243
+ } else if (scope === "user") {
244
+ for (const agent of userAgents) agentMap.set(agent.name, agent);
245
+ } else {
246
+ for (const agent of projectAgents) agentMap.set(agent.name, agent);
247
+ }
248
+
249
+ return { agents: Array.from(agentMap.values()), projectAgentsDir };
230
250
  }
231
251
 
232
252
  function truncateOutput(output: string): { text: string; truncated: boolean } {
233
- let truncated = false;
234
- let byteBudget = MAX_OUTPUT_BYTES;
235
- let lineBudget = MAX_OUTPUT_LINES;
236
-
237
- let i = 0;
238
- let lastNewlineIndex = -1;
239
- while (i < output.length && byteBudget > 0) {
240
- const ch = output.charCodeAt(i);
241
- byteBudget--;
242
-
243
- if (ch === 10 /* \n */) {
244
- lineBudget--;
245
- lastNewlineIndex = i;
246
- if (lineBudget <= 0) {
247
- truncated = true;
248
- break;
249
- }
253
+ let truncated = false;
254
+ let byteBudget = MAX_OUTPUT_BYTES;
255
+ let lineBudget = MAX_OUTPUT_LINES;
256
+
257
+ let i = 0;
258
+ let lastNewlineIndex = -1;
259
+ while (i < output.length && byteBudget > 0) {
260
+ const ch = output.charCodeAt(i);
261
+ byteBudget--;
262
+
263
+ if (ch === 10 /* \n */) {
264
+ lineBudget--;
265
+ lastNewlineIndex = i;
266
+ if (lineBudget <= 0) {
267
+ truncated = true;
268
+ break;
250
269
  }
270
+ }
251
271
 
252
- i++;
253
- }
272
+ i++;
273
+ }
254
274
 
255
- if (i < output.length) {
256
- truncated = true;
257
- }
275
+ if (i < output.length) {
276
+ truncated = true;
277
+ }
258
278
 
259
- if (truncated && lineBudget <= 0 && lastNewlineIndex >= 0) {
260
- output = output.slice(0, lastNewlineIndex);
261
- } else {
262
- output = output.slice(0, i);
263
- }
279
+ if (truncated && lineBudget <= 0 && lastNewlineIndex >= 0) {
280
+ output = output.slice(0, lastNewlineIndex);
281
+ } else {
282
+ output = output.slice(0, i);
283
+ }
264
284
 
265
- return { text: output, truncated };
285
+ return { text: output, truncated };
266
286
  }
267
287
 
268
288
  function previewFirstLines(text: string, maxLines: number): string {
269
- if (maxLines <= 0) return "";
270
- let linesRemaining = maxLines;
271
- let i = 0;
272
- while (i < text.length) {
273
- const nextNewline = text.indexOf("\n", i);
274
- if (nextNewline === -1) return text;
275
- linesRemaining--;
276
- if (linesRemaining <= 0) return text.slice(0, nextNewline);
277
- i = nextNewline + 1;
278
- }
279
- return text;
289
+ if (maxLines <= 0) return "";
290
+ let linesRemaining = maxLines;
291
+ let i = 0;
292
+ while (i < text.length) {
293
+ const nextNewline = text.indexOf("\n", i);
294
+ if (nextNewline === -1) return text;
295
+ linesRemaining--;
296
+ if (linesRemaining <= 0) return text.slice(0, nextNewline);
297
+ i = nextNewline + 1;
298
+ }
299
+ return text;
280
300
  }
281
301
 
282
302
  function sanitizeAgentName(name: string): string {
283
- return name.replace(/[^\w.-]+/g, "_").slice(0, 50);
303
+ return name.replace(/[^\w.-]+/g, "_").slice(0, 50);
284
304
  }
285
305
 
286
- function formatToolArgs(toolName: string, args: Record<string, unknown>): string {
287
- const MAX_LEN = 60;
288
-
289
- // Extract the most relevant arg based on tool type
290
- let preview = "";
291
- if (args.command) {
292
- preview = String(args.command);
293
- } else if (args.file_path) {
294
- preview = String(args.file_path);
295
- } else if (args.path) {
296
- preview = String(args.path);
297
- } else if (args.pattern) {
298
- preview = String(args.pattern);
299
- } else if (args.query) {
300
- preview = String(args.query);
301
- } else if (args.url) {
302
- preview = String(args.url);
303
- } else if (args.task) {
304
- preview = String(args.task);
305
- } else {
306
- // Fallback: stringify first non-empty string arg
307
- for (const val of Object.values(args)) {
308
- if (typeof val === "string" && val.length > 0) {
309
- preview = val;
310
- break;
311
- }
306
+ function formatToolArgs(
307
+ toolName: string,
308
+ args: Record<string, unknown>,
309
+ ): string {
310
+ const MAX_LEN = 60;
311
+
312
+ // Extract the most relevant arg based on tool type
313
+ let preview = "";
314
+ if (args.command) {
315
+ preview = String(args.command);
316
+ } else if (args.file_path) {
317
+ preview = String(args.file_path);
318
+ } else if (args.path) {
319
+ preview = String(args.path);
320
+ } else if (args.pattern) {
321
+ preview = String(args.pattern);
322
+ } else if (args.query) {
323
+ preview = String(args.query);
324
+ } else if (args.url) {
325
+ preview = String(args.url);
326
+ } else if (args.task) {
327
+ preview = String(args.task);
328
+ } else {
329
+ // Fallback: stringify first non-empty string arg
330
+ for (const val of Object.values(args)) {
331
+ if (typeof val === "string" && val.length > 0) {
332
+ preview = val;
333
+ break;
312
334
  }
313
- }
335
+ }
336
+ }
314
337
 
315
- if (!preview) {
316
- return toolName;
317
- }
338
+ if (!preview) {
339
+ return toolName;
340
+ }
318
341
 
319
- // Truncate and clean up
320
- preview = preview.replace(/\n/g, " ").trim();
321
- if (preview.length > MAX_LEN) {
322
- preview = preview.slice(0, MAX_LEN) + "…";
323
- }
342
+ // Truncate and clean up
343
+ preview = preview.replace(/\n/g, " ").trim();
344
+ if (preview.length > MAX_LEN) {
345
+ preview = preview.slice(0, MAX_LEN) + "…";
346
+ }
324
347
 
325
- return `${toolName}: ${preview}`;
348
+ return `${toolName}: ${preview}`;
326
349
  }
327
350
 
328
351
  function formatDuration(ms: number): string {
329
- if (ms < 1000) return `${ms}ms`;
330
- if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
331
- const mins = Math.floor(ms / 60000);
332
- const secs = ((ms % 60000) / 1000).toFixed(0);
333
- return `${mins}m${secs}s`;
352
+ if (ms < 1000) return `${ms}ms`;
353
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
354
+ const mins = Math.floor(ms / 60000);
355
+ const secs = ((ms % 60000) / 1000).toFixed(0);
356
+ return `${mins}m${secs}s`;
334
357
  }
335
358
 
336
359
  function formatTokens(tokens: number): string {
337
- if (tokens < 1000) return `${tokens}`;
338
- if (tokens < 10000) return `${(tokens / 1000).toFixed(1)}k`;
339
- return `${Math.round(tokens / 1000)}k`;
360
+ if (tokens < 1000) return `${tokens}`;
361
+ if (tokens < 10000) return `${(tokens / 1000).toFixed(1)}k`;
362
+ return `${Math.round(tokens / 1000)}k`;
340
363
  }
341
364
 
342
365
  function pluralize(count: number, singular: string, plural?: string): string {
343
- return count === 1 ? singular : (plural ?? singular + "s");
366
+ return count === 1 ? singular : (plural ?? singular + "s");
344
367
  }
345
368
 
346
369
  async function mapWithConcurrencyLimit<TIn, TOut>(
347
- items: TIn[],
348
- concurrency: number,
349
- fn: (item: TIn, index: number) => Promise<TOut>
370
+ items: TIn[],
371
+ concurrency: number,
372
+ fn: (item: TIn, index: number) => Promise<TOut>,
350
373
  ): Promise<TOut[]> {
351
- if (items.length === 0) return [];
352
- const limit = Math.max(1, Math.min(concurrency, items.length));
353
- const results: TOut[] = new Array(items.length);
354
-
355
- let nextIndex = 0;
356
- const workers = new Array(limit).fill(null).map(async () => {
357
- while (true) {
358
- const current = nextIndex++;
359
- if (current >= items.length) return;
360
- results[current] = await fn(items[current], current);
361
- }
362
- });
363
-
364
- await Promise.all(workers);
365
- return results;
374
+ if (items.length === 0) return [];
375
+ const limit = Math.max(1, Math.min(concurrency, items.length));
376
+ const results: TOut[] = new Array(items.length);
377
+
378
+ let nextIndex = 0;
379
+ const workers = new Array(limit).fill(null).map(async () => {
380
+ while (true) {
381
+ const current = nextIndex++;
382
+ if (current >= items.length) return;
383
+ results[current] = await fn(items[current], current);
384
+ }
385
+ });
386
+
387
+ await Promise.all(workers);
388
+ return results;
366
389
  }
367
390
 
368
- function writePromptToTempFile(agentName: string, prompt: string): { dir: string; filePath: string } {
369
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-task-agent-"));
370
- const safeName = agentName.replace(/[^\w.-]+/g, "_");
371
- const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
372
- fs.writeFileSync(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
373
- return { dir: tmpDir, filePath };
391
+ function writePromptToTempFile(
392
+ agentName: string,
393
+ prompt: string,
394
+ ): { dir: string; filePath: string } {
395
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-task-agent-"));
396
+ const safeName = agentName.replace(/[^\w.-]+/g, "_");
397
+ const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
398
+ fs.writeFileSync(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
399
+ return { dir: tmpDir, filePath };
374
400
  }
375
401
 
376
402
  interface RunAgentOptions {
377
- onProgress?: (progress: AgentProgress) => void;
378
- index?: number;
379
- signal?: AbortSignal;
380
- model?: string;
403
+ onProgress?: (progress: AgentProgress) => void;
404
+ index?: number;
405
+ signal?: AbortSignal;
406
+ model?: string;
381
407
  }
382
408
 
383
409
  async function runSingleAgent(
384
- cwd: string,
385
- agents: AgentConfig[],
386
- agentName: string,
387
- task: string,
388
- step?: number,
389
- options?: RunAgentOptions
410
+ cwd: string,
411
+ agents: AgentConfig[],
412
+ agentName: string,
413
+ task: string,
414
+ step?: number,
415
+ options?: RunAgentOptions,
390
416
  ): Promise<SingleResult> {
391
- // Check if already aborted
392
- if (options?.signal?.aborted) {
393
- return {
394
- agent: agentName,
395
- agentSource: "unknown",
396
- task,
397
- exitCode: 1,
398
- stdout: "",
399
- stderr: "Aborted",
400
- truncated: false,
401
- durationMs: 0,
402
- step,
403
- };
404
- }
405
- const startTime = Date.now();
406
- const agent = agents.find((a) => a.name === agentName);
407
- const index = options?.index ?? 0;
417
+ // Check if already aborted
418
+ if (options?.signal?.aborted) {
419
+ return {
420
+ agent: agentName,
421
+ agentSource: "unknown",
422
+ task,
423
+ exitCode: 1,
424
+ stdout: "",
425
+ stderr: "Aborted",
426
+ truncated: false,
427
+ durationMs: 0,
428
+ step,
429
+ };
430
+ }
431
+ const startTime = Date.now();
432
+ const agent = agents.find((a) => a.name === agentName);
433
+ const index = options?.index ?? 0;
434
+
435
+ if (!agent) {
436
+ return {
437
+ agent: agentName,
438
+ agentSource: "unknown",
439
+ task,
440
+ exitCode: 1,
441
+ stdout: "",
442
+ stderr: `Unknown agent: ${agentName}. Available: ${agents.map((a) => a.name).join(", ") || "none"}`,
443
+ truncated: false,
444
+ durationMs: Date.now() - startTime,
445
+ step,
446
+ };
447
+ }
448
+
449
+ const args: string[] = ["-p", "--no-session", "--mode", "json"];
450
+
451
+ const modelToUse = options?.model ?? agent.model;
452
+ if (modelToUse) {
453
+ // Use --models for pattern matching (takes first match from available models)
454
+ args.push("--models", modelToUse);
455
+ }
456
+
457
+ if (agent.tools && agent.tools.length > 0) {
458
+ args.push("--tools", agent.tools.join(","));
459
+ }
460
+
461
+ let tmpPromptDir: string | null = null;
462
+ let tmpPromptPath: string | null = null;
463
+
464
+ try {
465
+ if (agent.systemPrompt.trim()) {
466
+ const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
467
+ tmpPromptDir = tmp.dir;
468
+ tmpPromptPath = tmp.filePath;
469
+ args.push("--append-system-prompt", tmpPromptPath);
470
+ }
471
+
472
+ args.push(`Task: ${task}`);
473
+
474
+ // Emit initial "Initializing" state
475
+ options?.onProgress?.({
476
+ agent: agentName,
477
+ agentSource: agent.source,
478
+ status: "running",
479
+ task,
480
+ currentTool: undefined,
481
+ currentToolDescription: "Initializing…",
482
+ toolCount: 0,
483
+ tokens: 0,
484
+ durationMs: 0,
485
+ step,
486
+ index,
487
+ modelOverride: options?.model,
488
+ });
489
+
490
+ return await new Promise<SingleResult>((resolve) => {
491
+ const proc = spawn("pi", args, {
492
+ cwd,
493
+ stdio: ["ignore", "pipe", "pipe"],
494
+ });
408
495
 
409
- if (!agent) {
410
- return {
411
- agent: agentName,
412
- agentSource: "unknown",
413
- task,
414
- exitCode: 1,
415
- stdout: "",
416
- stderr: `Unknown agent: ${agentName}. Available: ${agents.map((a) => a.name).join(", ") || "none"}`,
417
- truncated: false,
418
- durationMs: Date.now() - startTime,
419
- step,
496
+ let toolCount = 0;
497
+ let tokens = 0;
498
+ let currentTool: string | undefined;
499
+ let currentToolDescription: string | undefined;
500
+ let lastTextContent = "";
501
+ let stderrContent = "";
502
+ let aborted = false;
503
+ let resolved = false;
504
+
505
+ const doResolve = (result: SingleResult) => {
506
+ if (resolved) return;
507
+ resolved = true;
508
+ options?.signal?.removeEventListener("abort", onAbort);
509
+ resolve(result);
420
510
  };
421
- }
422
-
423
- const args: string[] = ["-p", "--no-session", "--mode", "json"];
424
-
425
- const modelToUse = options?.model ?? agent.model;
426
- if (modelToUse) {
427
- // Use --models for pattern matching (takes first match from available models)
428
- args.push("--models", modelToUse);
429
- }
430
-
431
- if (agent.tools && agent.tools.length > 0) {
432
- args.push("--tools", agent.tools.join(","));
433
- }
434
-
435
- let tmpPromptDir: string | null = null;
436
- let tmpPromptPath: string | null = null;
437
511
 
438
- try {
439
- if (agent.systemPrompt.trim()) {
440
- const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
441
- tmpPromptDir = tmp.dir;
442
- tmpPromptPath = tmp.filePath;
443
- args.push("--append-system-prompt", tmpPromptPath);
444
- }
445
-
446
- args.push(`Task: ${task}`);
447
-
448
- // Emit initial "Initializing" state
449
- options?.onProgress?.({
450
- agent: agentName,
451
- agentSource: agent.source,
452
- status: "running",
453
- task,
454
- currentTool: undefined,
455
- currentToolDescription: "Initializing…",
456
- toolCount: 0,
457
- tokens: 0,
458
- durationMs: 0,
459
- step,
460
- index,
461
- modelOverride: options?.model,
462
- });
463
-
464
- return await new Promise<SingleResult>((resolve) => {
465
- const proc = spawn("pi", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
466
-
467
- let toolCount = 0;
468
- let tokens = 0;
469
- let currentTool: string | undefined;
470
- let currentToolDescription: string | undefined;
471
- let lastTextContent = "";
472
- let stderrContent = "";
473
- let aborted = false;
474
- let resolved = false;
475
-
476
- const doResolve = (result: SingleResult) => {
477
- if (resolved) return;
478
- resolved = true;
479
- options?.signal?.removeEventListener("abort", onAbort);
480
- resolve(result);
481
- };
482
-
483
- // Handle abort signal (ESC key)
484
- const onAbort = () => {
485
- aborted = true;
486
- proc.kill("SIGTERM");
487
- };
488
- options?.signal?.addEventListener("abort", onAbort);
489
-
490
- const rl = readline.createInterface({ input: proc.stdout });
491
- let status: "running" | "completed" | "failed" = "running";
492
-
493
- const emitProgress = () => {
494
- options?.onProgress?.({
495
- agent: agentName,
496
- agentSource: agent.source,
497
- status,
498
- task,
499
- currentTool,
500
- currentToolDescription,
501
- toolCount,
502
- tokens,
503
- durationMs: Date.now() - startTime,
504
- step,
505
- index,
506
- modelOverride: options?.model,
507
- });
508
- };
512
+ // Handle abort signal (ESC key)
513
+ const onAbort = () => {
514
+ aborted = true;
515
+ proc.kill("SIGTERM");
516
+ };
517
+ options?.signal?.addEventListener("abort", onAbort);
518
+
519
+ const rl = readline.createInterface({ input: proc.stdout });
520
+ let status: "running" | "completed" | "failed" = "running";
521
+
522
+ const emitProgress = () => {
523
+ options?.onProgress?.({
524
+ agent: agentName,
525
+ agentSource: agent.source,
526
+ status,
527
+ task,
528
+ currentTool,
529
+ currentToolDescription,
530
+ toolCount,
531
+ tokens,
532
+ durationMs: Date.now() - startTime,
533
+ step,
534
+ index,
535
+ modelOverride: options?.model,
536
+ });
537
+ };
509
538
 
510
- rl.on("line", (line) => {
511
- try {
512
- const event = JSON.parse(line);
513
-
514
- if (event.type === "tool_execution_start") {
515
- toolCount++;
516
- currentTool = event.toolName;
517
- // Extract tool args for description
518
- const args = event.toolArgs || event.args || {};
519
- const argPreview = formatToolArgs(event.toolName, args);
520
- currentToolDescription = argPreview;
521
- emitProgress();
522
- } else if (event.type === "tool_execution_end") {
523
- currentTool = undefined;
524
- currentToolDescription = undefined;
525
- } else if (event.type === "message_update" || event.type === "message_end") {
526
- // Extract tokens from usage
527
- const usage = event.message?.usage;
528
- if (usage?.totalTokens) {
529
- tokens = usage.totalTokens;
530
- }
531
- } else if (event.type === "agent_end") {
532
- // Extract final text from the last assistant message
533
- const messages = event.messages ?? [];
534
- const lastMsg = messages.findLast((m: any) => m.role === "assistant");
535
- if (lastMsg?.content) {
536
- const textParts = lastMsg.content
537
- .filter((c: any) => c.type === "text")
538
- .map((c: any) => c.text);
539
- lastTextContent = textParts.join("\n");
540
- }
541
- // Get final token count
542
- if (lastMsg?.usage?.totalTokens) {
543
- tokens = lastMsg.usage.totalTokens;
544
- }
545
- // Mark as completed and emit final progress
546
- status = "completed";
547
- currentTool = undefined;
548
- currentToolDescription = "Done";
549
- emitProgress();
550
- // Resolve immediately on agent_end - don't wait for process close
551
- const stdoutResult = truncateOutput(lastTextContent);
552
- doResolve({
553
- agent: agentName,
554
- agentSource: agent.source,
555
- task,
556
- exitCode: 0,
557
- stdout: stdoutResult.text,
558
- stderr: "",
559
- truncated: stdoutResult.truncated,
560
- durationMs: Date.now() - startTime,
561
- step,
562
- modelOverride: options?.model,
563
- });
564
- }
565
- } catch {
566
- // Ignore parse errors
539
+ rl.on("line", (line) => {
540
+ try {
541
+ const event = JSON.parse(line);
542
+
543
+ if (event.type === "tool_execution_start") {
544
+ toolCount++;
545
+ currentTool = event.toolName;
546
+ // Extract tool args for description
547
+ const args = event.toolArgs || event.args || {};
548
+ const argPreview = formatToolArgs(event.toolName, args);
549
+ currentToolDescription = argPreview;
550
+ emitProgress();
551
+ } else if (event.type === "tool_execution_end") {
552
+ currentTool = undefined;
553
+ currentToolDescription = undefined;
554
+ } else if (
555
+ event.type === "message_update" ||
556
+ event.type === "message_end"
557
+ ) {
558
+ // Extract tokens from usage
559
+ const usage = event.message?.usage;
560
+ if (usage?.totalTokens) {
561
+ tokens = usage.totalTokens;
567
562
  }
568
- });
569
-
570
- proc.stderr.on("data", (chunk) => {
571
- stderrContent += chunk.toString();
572
- });
573
-
574
- proc.on("close", (code) => {
575
- if (aborted) {
576
- doResolve({
577
- agent: agentName,
578
- agentSource: agent.source,
579
- task,
580
- exitCode: 1,
581
- stdout: lastTextContent,
582
- stderr: "Interrupted",
583
- truncated: false,
584
- durationMs: Date.now() - startTime,
585
- step,
586
- modelOverride: options?.model,
587
- });
588
- return;
563
+ } else if (event.type === "agent_end") {
564
+ // Extract final text from the last assistant message
565
+ const messages = event.messages ?? [];
566
+ const lastMsg = messages.findLast(
567
+ (m: any) => m.role === "assistant",
568
+ );
569
+ if (lastMsg?.content) {
570
+ const textParts = lastMsg.content
571
+ .filter((c: any) => c.type === "text")
572
+ .map((c: any) => c.text);
573
+ lastTextContent = textParts.join("\n");
589
574
  }
590
-
591
- // Fallback if agent_end wasn't received
575
+ // Get final token count
576
+ if (lastMsg?.usage?.totalTokens) {
577
+ tokens = lastMsg.usage.totalTokens;
578
+ }
579
+ // Mark as completed and emit final progress
580
+ status = "completed";
581
+ currentTool = undefined;
582
+ currentToolDescription = "Done";
583
+ emitProgress();
584
+ // Resolve immediately on agent_end - don't wait for process close
592
585
  const stdoutResult = truncateOutput(lastTextContent);
593
- const stderrResult = truncateOutput(stderrContent);
594
-
595
586
  doResolve({
596
- agent: agentName,
597
- agentSource: agent.source,
598
- task,
599
- exitCode: code ?? 0,
600
- stdout: stdoutResult.text,
601
- stderr: stderrResult.text,
602
- truncated: stdoutResult.truncated || stderrResult.truncated,
603
- durationMs: Date.now() - startTime,
604
- step,
605
- modelOverride: options?.model,
587
+ agent: agentName,
588
+ agentSource: agent.source,
589
+ task,
590
+ exitCode: 0,
591
+ stdout: stdoutResult.text,
592
+ stderr: "",
593
+ truncated: stdoutResult.truncated,
594
+ durationMs: Date.now() - startTime,
595
+ step,
596
+ modelOverride: options?.model,
606
597
  });
607
- });
598
+ }
599
+ } catch {
600
+ // Ignore parse errors
601
+ }
602
+ });
608
603
 
609
- proc.on("error", (err) => {
610
- doResolve({
611
- agent: agentName,
612
- agentSource: agent.source,
613
- task,
614
- exitCode: 1,
615
- stdout: "",
616
- stderr: aborted ? "Interrupted" : err.message,
617
- truncated: false,
618
- durationMs: Date.now() - startTime,
619
- step,
620
- modelOverride: options?.model,
621
- });
622
- });
604
+ proc.stderr.on("data", (chunk) => {
605
+ stderrContent += chunk.toString();
606
+ });
607
+
608
+ proc.on("close", (code) => {
609
+ if (aborted) {
610
+ doResolve({
611
+ agent: agentName,
612
+ agentSource: agent.source,
613
+ task,
614
+ exitCode: 1,
615
+ stdout: lastTextContent,
616
+ stderr: "Interrupted",
617
+ truncated: false,
618
+ durationMs: Date.now() - startTime,
619
+ step,
620
+ modelOverride: options?.model,
621
+ });
622
+ return;
623
+ }
624
+
625
+ // Fallback if agent_end wasn't received
626
+ const stdoutResult = truncateOutput(lastTextContent);
627
+ const stderrResult = truncateOutput(stderrContent);
628
+
629
+ doResolve({
630
+ agent: agentName,
631
+ agentSource: agent.source,
632
+ task,
633
+ exitCode: code ?? 0,
634
+ stdout: stdoutResult.text,
635
+ stderr: stderrResult.text,
636
+ truncated: stdoutResult.truncated || stderrResult.truncated,
637
+ durationMs: Date.now() - startTime,
638
+ step,
639
+ modelOverride: options?.model,
640
+ });
641
+ });
642
+
643
+ proc.on("error", (err) => {
644
+ doResolve({
645
+ agent: agentName,
646
+ agentSource: agent.source,
647
+ task,
648
+ exitCode: 1,
649
+ stdout: "",
650
+ stderr: aborted ? "Interrupted" : err.message,
651
+ truncated: false,
652
+ durationMs: Date.now() - startTime,
653
+ step,
654
+ modelOverride: options?.model,
655
+ });
623
656
  });
624
- } finally {
625
- if (tmpPromptPath) {
626
- try { fs.unlinkSync(tmpPromptPath); } catch { /* ignore */ }
657
+ });
658
+ } finally {
659
+ if (tmpPromptPath) {
660
+ try {
661
+ fs.unlinkSync(tmpPromptPath);
662
+ } catch {
663
+ /* ignore */
627
664
  }
628
- if (tmpPromptDir) {
629
- try { fs.rmdirSync(tmpPromptDir); } catch { /* ignore */ }
665
+ }
666
+ if (tmpPromptDir) {
667
+ try {
668
+ fs.rmdirSync(tmpPromptDir);
669
+ } catch {
670
+ /* ignore */
630
671
  }
631
- }
672
+ }
673
+ }
632
674
  }
633
675
 
634
676
  const TaskItem = Type.Object({
635
- agent: Type.String({ description: "Agent name" }),
636
- task: Type.String({ description: "Agent's specific assignment" }),
637
- model: Type.Optional(Type.String({ description: "Override the model for this task (takes precedence over agent's default model)" })),
677
+ agent: Type.String({ description: "Agent name" }),
678
+ task: Type.String({ description: "Agent's specific assignment" }),
679
+ model: Type.Optional(
680
+ Type.String({
681
+ description:
682
+ "Override the model for this task (takes precedence over agent's default model)",
683
+ }),
684
+ ),
638
685
  });
639
686
 
640
687
  const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
641
- description:
642
- 'Which agent directories are eligible. Default: "user". Use "both" to enable project-local agents from .pi/agents.',
643
- default: "user",
688
+ description:
689
+ 'Which agent directories are eligible. Default: "user". Use "both" to enable project-local agents from .pi/agents.',
690
+ default: "user",
644
691
  });
645
692
 
646
693
  const TaskParams = Type.Object({
647
- context: Type.Optional(Type.String({ description: "Shared context prepended to all tasks" })),
648
- tasks: Type.Array(TaskItem, { description: "Tasks to run in parallel" }),
649
- write: Type.Optional(Type.Boolean({
650
- description: "Write results to /tmp/task_{agent}_{index}.md instead of returning inline",
694
+ context: Type.Optional(
695
+ Type.String({ description: "Shared context prepended to all tasks" }),
696
+ ),
697
+ tasks: Type.Array(TaskItem, { description: "Tasks to run in parallel" }),
698
+ write: Type.Optional(
699
+ Type.Boolean({
700
+ description:
701
+ "Write results to /tmp/task_{agent}_{index}.md instead of returning inline",
651
702
  default: false,
652
- })),
653
- agentScope: Type.Optional(AgentScopeSchema),
703
+ }),
704
+ ),
705
+ agentScope: Type.Optional(AgentScopeSchema),
654
706
  });
655
707
 
656
708
  /**
@@ -658,362 +710,538 @@ const TaskParams = Type.Object({
658
710
  * Mirrors Claude Code's Task tool description format.
659
711
  */
660
712
  function buildDescription(pi: ToolAPI): string {
661
- const user = discoverAgents(pi.cwd, "user");
662
- const project = discoverAgents(pi.cwd, "project");
663
-
664
- const lines: string[] = [];
665
-
666
- lines.push("Launch a new agent to handle complex, multi-step tasks autonomously.");
667
- lines.push("");
668
- lines.push("The Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.");
669
- lines.push("");
670
- lines.push("Available agent types and the tools they have access to:");
671
-
672
- for (const agent of user.agents.slice(0, MAX_AGENTS_IN_DESCRIPTION)) {
713
+ const user = discoverAgents(pi.cwd, "user");
714
+ const project = discoverAgents(pi.cwd, "project");
715
+
716
+ const lines: string[] = [];
717
+
718
+ lines.push(
719
+ "Launch a new agent to handle complex, multi-step tasks autonomously.",
720
+ );
721
+ lines.push("");
722
+ lines.push(
723
+ "The Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.",
724
+ );
725
+ lines.push("");
726
+ lines.push("Available agent types and the tools they have access to:");
727
+
728
+ for (const agent of user.agents.slice(0, MAX_AGENTS_IN_DESCRIPTION)) {
729
+ const tools = agent.tools?.join(", ") || "All tools";
730
+ lines.push(`- ${agent.name}: ${agent.description} (Tools: ${tools})`);
731
+ }
732
+ if (user.agents.length > MAX_AGENTS_IN_DESCRIPTION) {
733
+ lines.push(
734
+ ` ...and ${user.agents.length - MAX_AGENTS_IN_DESCRIPTION} more user agents`,
735
+ );
736
+ }
737
+
738
+ if (project.agents.length > 0) {
739
+ const projectDirNote = project.projectAgentsDir
740
+ ? ` (from ${project.projectAgentsDir})`
741
+ : "";
742
+ lines.push("");
743
+ lines.push(
744
+ `Project agents${projectDirNote} (requires agentScope: "both" or "project"):`,
745
+ );
746
+ for (const agent of project.agents.slice(0, MAX_AGENTS_IN_DESCRIPTION)) {
673
747
  const tools = agent.tools?.join(", ") || "All tools";
674
748
  lines.push(`- ${agent.name}: ${agent.description} (Tools: ${tools})`);
675
- }
676
- if (user.agents.length > MAX_AGENTS_IN_DESCRIPTION) {
677
- lines.push(` ...and ${user.agents.length - MAX_AGENTS_IN_DESCRIPTION} more user agents`);
678
- }
679
-
680
- if (project.agents.length > 0) {
681
- const projectDirNote = project.projectAgentsDir ? ` (from ${project.projectAgentsDir})` : "";
682
- lines.push("");
683
- lines.push(`Project agents${projectDirNote} (requires agentScope: "both" or "project"):`);
684
- for (const agent of project.agents.slice(0, MAX_AGENTS_IN_DESCRIPTION)) {
685
- const tools = agent.tools?.join(", ") || "All tools";
686
- lines.push(`- ${agent.name}: ${agent.description} (Tools: ${tools})`);
687
- }
688
- if (project.agents.length > MAX_AGENTS_IN_DESCRIPTION) {
689
- lines.push(` ...and ${project.agents.length - MAX_AGENTS_IN_DESCRIPTION} more project agents`);
690
- }
691
- }
692
-
693
- lines.push("");
694
- lines.push("When NOT to use the Task tool:");
695
- lines.push("- If you want to read a specific file path, use the Read or Glob tool instead of the Task tool, to find the match more quickly");
696
- lines.push("- If you are searching for a specific class definition like \"class Foo\", use the Glob tool instead, to find the match more quickly");
697
- lines.push("- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Task tool, to find the match more quickly");
698
- lines.push("- Other tasks that are not related to the agent descriptions above");
699
- lines.push("");
700
- lines.push("");
701
- lines.push("Usage notes:");
702
- lines.push("- Always include a short description of the task in the task parameter");
703
- lines.push("- Launch multiple agents concurrently whenever possible, to maximize performance");
704
- lines.push("- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.");
705
- lines.push("- Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your task should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.");
706
- lines.push("- IMPORTANT: Agent results are intermediate data, not task completions. Use the agent's findings to continue executing the user's request. Do not treat agent reports as 'task complete' signals - they provide context for you to perform the actual work.");
707
- lines.push("- The agent's outputs should generally be trusted");
708
- lines.push("- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent");
709
- lines.push("- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.");
710
- lines.push("");
711
- lines.push("Parameters:");
712
- lines.push("- tasks: Array of {agent, task, model?} - tasks to run in parallel (max " + MAX_PARALLEL_TASKS + ", " + MAX_CONCURRENCY + " concurrent)");
713
- lines.push(" - model: (optional) Override the agent's default model using pattern matching (e.g., \"sonnet\", \"haiku\", \"gpt-4o\")");
714
- lines.push("- context: (optional) Shared context string prepended to all task prompts - use this to avoid repeating instructions");
715
- lines.push("- write: (optional) If true, results written to /tmp/task_{agent}_{index}.md instead of returned inline");
716
- lines.push("- agentScope: (optional) \"user\" | \"project\" | \"both\" - which agent directories to use");
717
- lines.push("");
718
- lines.push("Example usage:");
719
- lines.push("");
720
- lines.push("<example_agent_descriptions>");
721
- lines.push("\"code-reviewer\": use this agent after you are done writing a significant piece of code");
722
- lines.push("\"explore\": use this agent for fast codebase exploration and research");
723
- lines.push("</example_agent_descriptions>");
724
- lines.push("");
725
- lines.push("<example>");
726
- lines.push("user: \"Please write a function that checks if a number is prime\"");
727
- lines.push("assistant: Sure let me write a function that checks if a number is prime");
728
- lines.push("assistant: I'm going to use the Write tool to write the following code:");
729
- lines.push("<code>");
730
- lines.push("function isPrime(n) {");
731
- lines.push(" if (n <= 1) return false");
732
- lines.push(" for (let i = 2; i * i <= n; i++) {");
733
- lines.push(" if (n % i === 0) return false");
734
- lines.push(" }");
735
- lines.push(" return true");
736
- lines.push("}");
737
- lines.push("</code>");
738
- lines.push("<commentary>");
739
- lines.push("Since a significant piece of code was written and the task was completed, now use the code-reviewer agent to review the code");
740
- lines.push("</commentary>");
741
- lines.push("assistant: Now let me use the code-reviewer agent to review the code");
742
- lines.push("assistant: Uses the Task tool: { tasks: [{ agent: \"code-reviewer\", task: \"Review the isPrime function\" }] }");
743
- lines.push("</example>");
744
- lines.push("");
745
- lines.push("<example>");
746
- lines.push("user: \"Find all TODO comments in the codebase\"");
747
- lines.push("assistant: I'll use multiple explore agents to search different directories in parallel");
748
- lines.push("assistant: Uses the Task tool:");
749
- lines.push("{");
750
- lines.push(" \"context\": \"Find all TODO comments. Return file:line:content format.\",");
751
- lines.push(" \"tasks\": [");
752
- lines.push(" { \"agent\": \"explore\", \"task\": \"Search in src/\" },");
753
- lines.push(" { \"agent\": \"explore\", \"task\": \"Search in lib/\" },");
754
- lines.push(" { \"agent\": \"explore\", \"task\": \"Search in tests/\" }");
755
- lines.push(" ],");
756
- lines.push(" \"write\": true");
757
- lines.push("}");
758
- lines.push("Results written to /tmp/task_explore_0.md, /tmp/task_explore_1.md, /tmp/task_explore_2.md");
759
- lines.push("</example>");
760
-
761
- return lines.join("\n");
749
+ }
750
+ if (project.agents.length > MAX_AGENTS_IN_DESCRIPTION) {
751
+ lines.push(
752
+ ` ...and ${project.agents.length - MAX_AGENTS_IN_DESCRIPTION} more project agents`,
753
+ );
754
+ }
755
+ }
756
+
757
+ lines.push("");
758
+ lines.push("When NOT to use the Task tool:");
759
+ lines.push(
760
+ "- If you want to read a specific file path, use the Read or Glob tool instead of the Task tool, to find the match more quickly",
761
+ );
762
+ lines.push(
763
+ '- If you are searching for a specific class definition like "class Foo", use the Glob tool instead, to find the match more quickly',
764
+ );
765
+ lines.push(
766
+ "- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Task tool, to find the match more quickly",
767
+ );
768
+ lines.push(
769
+ "- Other tasks that are not related to the agent descriptions above",
770
+ );
771
+ lines.push("");
772
+ lines.push("");
773
+ lines.push("Usage notes:");
774
+ lines.push(
775
+ "- Always include a short description of the task in the task parameter",
776
+ );
777
+ lines.push(
778
+ "- Launch multiple agents concurrently whenever possible, to maximize performance",
779
+ );
780
+ lines.push(
781
+ "- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.",
782
+ );
783
+ lines.push(
784
+ "- Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your task should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.",
785
+ );
786
+ lines.push(
787
+ "- IMPORTANT: Agent results are intermediate data, not task completions. Use the agent's findings to continue executing the user's request. Do not treat agent reports as 'task complete' signals - they provide context for you to perform the actual work.",
788
+ );
789
+ lines.push("- The agent's outputs should generally be trusted");
790
+ lines.push(
791
+ "- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent",
792
+ );
793
+ lines.push(
794
+ "- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.",
795
+ );
796
+ lines.push("");
797
+ lines.push("Parameters:");
798
+ lines.push(
799
+ "- tasks: Array of {agent, task, model?} - tasks to run in parallel (max " +
800
+ MAX_PARALLEL_TASKS +
801
+ ", " +
802
+ MAX_CONCURRENCY +
803
+ " concurrent)",
804
+ );
805
+ lines.push(
806
+ ' - model: (optional) Override the agent\'s default model using pattern matching (e.g., "sonnet", "haiku", "gpt-4o")',
807
+ );
808
+ lines.push(
809
+ "- context: (optional) Shared context string prepended to all task prompts - use this to avoid repeating instructions",
810
+ );
811
+ lines.push(
812
+ "- write: (optional) If true, results written to /tmp/task_{agent}_{index}.md instead of returned inline",
813
+ );
814
+ lines.push(
815
+ '- agentScope: (optional) "user" | "project" | "both" - which agent directories to use',
816
+ );
817
+ lines.push("");
818
+ lines.push("Example usage:");
819
+ lines.push("");
820
+ lines.push("<example_agent_descriptions>");
821
+ lines.push(
822
+ '"code-reviewer": use this agent after you are done writing a significant piece of code',
823
+ );
824
+ lines.push(
825
+ '"explore": use this agent for fast codebase exploration and research',
826
+ );
827
+ lines.push("</example_agent_descriptions>");
828
+ lines.push("");
829
+ lines.push("<example>");
830
+ lines.push(
831
+ 'user: "Please write a function that checks if a number is prime"',
832
+ );
833
+ lines.push(
834
+ "assistant: Sure let me write a function that checks if a number is prime",
835
+ );
836
+ lines.push(
837
+ "assistant: I'm going to use the Write tool to write the following code:",
838
+ );
839
+ lines.push("<code>");
840
+ lines.push("function isPrime(n) {");
841
+ lines.push(" if (n <= 1) return false");
842
+ lines.push(" for (let i = 2; i * i <= n; i++) {");
843
+ lines.push(" if (n % i === 0) return false");
844
+ lines.push(" }");
845
+ lines.push(" return true");
846
+ lines.push("}");
847
+ lines.push("</code>");
848
+ lines.push("<commentary>");
849
+ lines.push(
850
+ "Since a significant piece of code was written and the task was completed, now use the code-reviewer agent to review the code",
851
+ );
852
+ lines.push("</commentary>");
853
+ lines.push(
854
+ "assistant: Now let me use the code-reviewer agent to review the code",
855
+ );
856
+ lines.push(
857
+ 'assistant: Uses the Task tool: { tasks: [{ agent: "code-reviewer", task: "Review the isPrime function" }] }',
858
+ );
859
+ lines.push("</example>");
860
+ lines.push("");
861
+ lines.push("<example>");
862
+ lines.push('user: "Find all TODO comments in the codebase"');
863
+ lines.push(
864
+ "assistant: I'll use multiple explore agents to search different directories in parallel",
865
+ );
866
+ lines.push("assistant: Uses the Task tool:");
867
+ lines.push("{");
868
+ lines.push(
869
+ ' "context": "Find all TODO comments. Return file:line:content format.",',
870
+ );
871
+ lines.push(' "tasks": [');
872
+ lines.push(' { "agent": "explore", "task": "Search in src/" },');
873
+ lines.push(' { "agent": "explore", "task": "Search in lib/" },');
874
+ lines.push(' { "agent": "explore", "task": "Search in tests/" }');
875
+ lines.push(" ],");
876
+ lines.push(' "write": true');
877
+ lines.push("}");
878
+ lines.push(
879
+ "Results written to /tmp/task_explore_0.md, /tmp/task_explore_1.md, /tmp/task_explore_2.md",
880
+ );
881
+ lines.push("</example>");
882
+
883
+ return lines.join("\n");
762
884
  }
763
885
 
764
886
  const factory: CustomToolFactory = (pi) => {
765
- const tool: CustomAgentTool<typeof TaskParams, TaskDetails> = {
766
- name: "task",
767
- label: "Task",
768
- get description() {
769
- return buildDescription(pi);
770
- },
771
- parameters: TaskParams,
772
-
773
- async execute(_toolCallId, params, signal, onUpdate) {
774
- const startTime = Date.now();
775
- const agentScope: AgentScope = params.agentScope ?? "user";
776
- const discovery = discoverAgents(pi.cwd, agentScope);
777
- const agents = discovery.agents;
778
- const context = params.context;
779
- const write = params.write ?? false;
780
-
781
- if (!params.tasks || params.tasks.length === 0) {
782
- const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
783
- return {
784
- content: [{ type: "text", text: `No tasks provided. Use: { tasks: [{agent, task}, ...] }\nAvailable agents: ${available}` }],
785
- details: { agentScope, projectAgentsDir: discovery.projectAgentsDir, results: [], totalDurationMs: 0 },
786
- };
787
- }
788
-
789
- if (params.tasks.length > MAX_PARALLEL_TASKS) {
790
- return {
791
- content: [{ type: "text", text: `Too many tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.` }],
792
- details: { agentScope, projectAgentsDir: discovery.projectAgentsDir, results: [], totalDurationMs: 0 },
793
- };
794
- }
795
-
796
- // Track progress for all agents
797
- const progressMap = new Map<number, AgentProgress>();
798
- for (let i = 0; i < params.tasks.length; i++) {
799
- const t = params.tasks[i];
800
- const agentCfg = agents.find((a) => a.name === t.agent);
801
- progressMap.set(i, {
802
- agent: t.agent,
803
- agentSource: agentCfg?.source ?? "unknown",
804
- status: "running",
805
- task: t.task,
806
- currentTool: undefined,
807
- currentToolDescription: "Queued…",
808
- toolCount: 0,
809
- tokens: 0,
810
- durationMs: 0,
811
- index: i,
812
- });
813
- }
814
-
815
- const emitProgress = () => {
816
- const allProgress = Array.from(progressMap.values()).sort((a, b) => a.index - b.index);
817
- onUpdate?.({
818
- content: [{ type: "text", text: `Running ${params.tasks.length} agents...` }],
819
- details: {
820
- agentScope,
821
- projectAgentsDir: discovery.projectAgentsDir,
822
- results: [],
823
- totalDurationMs: Date.now() - startTime,
824
- progress: allProgress,
825
- },
826
- });
827
- };
828
-
829
- emitProgress();
830
-
831
- // Build full prompts with context prepended
832
- const tasksWithContext = params.tasks.map((t) => ({
833
- agent: t.agent,
834
- task: context ? `${context}\n\n${t.task}` : t.task,
835
- model: t.model,
836
- }));
837
-
838
- // Generate output paths if write=true
839
- const outputPaths: string[] = write
840
- ? params.tasks.map((t, i) => `/tmp/task_${sanitizeAgentName(t.agent)}_${i}.md`)
841
- : [];
842
-
843
- const results = await mapWithConcurrencyLimit(tasksWithContext, MAX_CONCURRENCY, async (t, idx) => {
844
- const result = await runSingleAgent(pi.cwd, agents, t.agent, t.task, undefined, {
845
- index: idx,
846
- signal,
847
- model: t.model,
848
- onProgress: (progress) => {
849
- progressMap.set(idx, progress);
850
- emitProgress();
851
- },
852
- });
887
+ const tool: CustomAgentTool<typeof TaskParams, TaskDetails> = {
888
+ name: "task",
889
+ label: "Task",
890
+ get description() {
891
+ return buildDescription(pi);
892
+ },
893
+ parameters: TaskParams,
894
+
895
+ async execute(_toolCallId, params, signal, onUpdate) {
896
+ const startTime = Date.now();
897
+ const agentScope: AgentScope = params.agentScope ?? "user";
898
+ const discovery = discoverAgents(pi.cwd, agentScope);
899
+ const agents = discovery.agents;
900
+ const context = params.context;
901
+ const write = params.write ?? false;
902
+
903
+ if (!params.tasks || params.tasks.length === 0) {
904
+ const available =
905
+ agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
906
+ return {
907
+ content: [
908
+ {
909
+ type: "text",
910
+ text: `No tasks provided. Use: { tasks: [{agent, task}, ...] }\nAvailable agents: ${available}`,
911
+ },
912
+ ],
913
+ details: {
914
+ agentScope,
915
+ projectAgentsDir: discovery.projectAgentsDir,
916
+ results: [],
917
+ totalDurationMs: 0,
918
+ },
919
+ };
920
+ }
921
+
922
+ if (params.tasks.length > MAX_PARALLEL_TASKS) {
923
+ return {
924
+ content: [
925
+ {
926
+ type: "text",
927
+ text: `Too many tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
928
+ },
929
+ ],
930
+ details: {
931
+ agentScope,
932
+ projectAgentsDir: discovery.projectAgentsDir,
933
+ results: [],
934
+ totalDurationMs: 0,
935
+ },
936
+ };
937
+ }
938
+
939
+ // Track progress for all agents
940
+ const progressMap = new Map<number, AgentProgress>();
941
+ for (let i = 0; i < params.tasks.length; i++) {
942
+ const t = params.tasks[i];
943
+ const agentCfg = agents.find((a) => a.name === t.agent);
944
+ progressMap.set(i, {
945
+ agent: t.agent,
946
+ agentSource: agentCfg?.source ?? "unknown",
947
+ status: "running",
948
+ task: t.task,
949
+ currentTool: undefined,
950
+ currentToolDescription: "Queued…",
951
+ toolCount: 0,
952
+ tokens: 0,
953
+ durationMs: 0,
954
+ index: i,
955
+ });
956
+ }
853
957
 
854
- // Write output to file if write=true
855
- if (write && outputPaths[idx]) {
856
- const content = result.stdout.trim() || result.stderr.trim() || "(no output)";
857
- try {
858
- fs.writeFileSync(outputPaths[idx], content, { encoding: "utf-8" });
859
- } catch (e) {
860
- result.stderr += `\nFailed to write output: ${e}`;
861
- }
958
+ const emitProgress = () => {
959
+ const allProgress = Array.from(progressMap.values()).sort(
960
+ (a, b) => a.index - b.index,
961
+ );
962
+ onUpdate?.({
963
+ content: [
964
+ { type: "text", text: `Running ${params.tasks.length} agents...` },
965
+ ],
966
+ details: {
967
+ agentScope,
968
+ projectAgentsDir: discovery.projectAgentsDir,
969
+ results: [],
970
+ totalDurationMs: Date.now() - startTime,
971
+ progress: allProgress,
972
+ },
973
+ });
974
+ };
975
+
976
+ emitProgress();
977
+
978
+ // Build full prompts with context prepended
979
+ const tasksWithContext = params.tasks.map((t) => ({
980
+ agent: t.agent,
981
+ task: context ? `${context}\n\n${t.task}` : t.task,
982
+ model: t.model,
983
+ }));
984
+
985
+ // Generate output paths if write=true
986
+ const outputPaths: string[] = write
987
+ ? params.tasks.map(
988
+ (t, i) => `/tmp/task_${sanitizeAgentName(t.agent)}_${i}.md`,
989
+ )
990
+ : [];
991
+
992
+ const results = await mapWithConcurrencyLimit(
993
+ tasksWithContext,
994
+ MAX_CONCURRENCY,
995
+ async (t, idx) => {
996
+ const result = await runSingleAgent(
997
+ pi.cwd,
998
+ agents,
999
+ t.agent,
1000
+ t.task,
1001
+ undefined,
1002
+ {
1003
+ index: idx,
1004
+ signal,
1005
+ model: t.model,
1006
+ onProgress: (progress) => {
1007
+ progressMap.set(idx, progress);
1008
+ emitProgress();
1009
+ },
1010
+ },
1011
+ );
1012
+
1013
+ // Write output to file if write=true
1014
+ if (write && outputPaths[idx]) {
1015
+ const content =
1016
+ result.stdout.trim() || result.stderr.trim() || "(no output)";
1017
+ try {
1018
+ fs.writeFileSync(outputPaths[idx], content, {
1019
+ encoding: "utf-8",
1020
+ });
1021
+ } catch (e) {
1022
+ result.stderr += `\nFailed to write output: ${e}`;
862
1023
  }
1024
+ }
1025
+
1026
+ return result;
1027
+ },
1028
+ );
1029
+
1030
+ const successCount = results.filter((r) => r.exitCode === 0).length;
1031
+ const totalDuration = Date.now() - startTime;
1032
+
1033
+ // Build summaries
1034
+ const summaries = results.map((r, i) => {
1035
+ const status =
1036
+ r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`;
1037
+ const output = r.stdout.trim() || r.stderr.trim() || "(no output)";
1038
+
1039
+ if (write && outputPaths[i]) {
1040
+ const preview = previewFirstLines(output, 5);
1041
+ return `[${r.agent}] ${status} → ${outputPaths[i]}\n${preview}`;
1042
+ } else {
1043
+ const preview = previewFirstLines(output, 5);
1044
+ return `[${r.agent}] ${status} (${formatDuration(r.durationMs)})\n${preview}`;
1045
+ }
1046
+ });
863
1047
 
864
- return result;
865
- });
1048
+ return {
1049
+ content: [
1050
+ {
1051
+ type: "text",
1052
+ text: `${successCount}/${results.length} succeeded [${formatDuration(totalDuration)}]\n\n${summaries.join("\n\n---\n\n")}`,
1053
+ },
1054
+ ],
1055
+ details: {
1056
+ agentScope,
1057
+ projectAgentsDir: discovery.projectAgentsDir,
1058
+ results,
1059
+ totalDurationMs: totalDuration,
1060
+ outputPaths: write ? outputPaths : undefined,
1061
+ },
1062
+ };
1063
+ },
866
1064
 
867
- const successCount = results.filter((r) => r.exitCode === 0).length;
868
- const totalDuration = Date.now() - startTime;
1065
+ renderCall(args, theme) {
1066
+ // Return minimal - renderResult handles the full display
1067
+ if (!args.tasks || args.tasks.length === 0) {
1068
+ return new Text(theme.fg("error", "task: no tasks provided"), 0, 0);
1069
+ }
1070
+ return new Text("", 0, 0);
1071
+ },
1072
+
1073
+ renderResult(result, { expanded, isPartial }, theme) {
1074
+ const { details } = result;
1075
+
1076
+ // Tree formatting helpers
1077
+ const TREE_MID = "├─";
1078
+ const TREE_END = "└─";
1079
+ const TREE_PIPE = "│";
1080
+ const TREE_SPACE = " ";
1081
+ const TREE_HOOK = "⎿";
1082
+
1083
+ const truncateTask = (task: string, maxLen: number) => {
1084
+ const firstLine = task.split("\n")[0];
1085
+ return firstLine.length > maxLen
1086
+ ? firstLine.slice(0, maxLen) + "…"
1087
+ : firstLine;
1088
+ };
869
1089
 
870
- // Build summaries
871
- const summaries = results.map((r, i) => {
872
- const status = r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`;
873
- const output = r.stdout.trim() || r.stderr.trim() || "(no output)";
1090
+ // Handle streaming progress
1091
+ if (isPartial && details?.progress && details.progress.length > 0) {
1092
+ const count = details.progress.length;
1093
+ const completedCount = details.progress.filter(
1094
+ (p) => p.status === "completed",
1095
+ ).length;
1096
+ const writeNote = details.outputPaths ? " → /tmp" : "";
1097
+
1098
+ let headerText: string;
1099
+ if (completedCount === count) {
1100
+ headerText =
1101
+ theme.fg("success", "●") +
1102
+ " " +
1103
+ theme.fg(
1104
+ "toolTitle",
1105
+ `${count} ${pluralize(count, "agent")} finished`,
1106
+ );
1107
+ } else if (completedCount > 0) {
1108
+ headerText = theme.fg(
1109
+ "toolTitle",
1110
+ `Running ${count - completedCount}/${count} agents`,
1111
+ );
1112
+ } else {
1113
+ headerText = theme.fg(
1114
+ "toolTitle",
1115
+ `Running ${count} ${pluralize(count, "agent")}`,
1116
+ );
1117
+ }
1118
+ let text = headerText + theme.fg("dim", writeNote);
1119
+
1120
+ for (let i = 0; i < details.progress.length; i++) {
1121
+ const p = details.progress[i];
1122
+ const isLast = i === details.progress.length - 1;
1123
+ const branch = isLast ? TREE_END : TREE_MID;
1124
+ const cont = isLast ? TREE_SPACE : TREE_PIPE;
1125
+
1126
+ const taskPreview = truncateTask(p.task, 45);
1127
+ const tokenStr =
1128
+ p.tokens > 0 ? `${formatTokens(p.tokens)} tokens` : "";
1129
+
1130
+ const modelTag = p.modelOverride
1131
+ ? theme.fg("muted", ` (${p.modelOverride})`)
1132
+ : "";
1133
+
1134
+ if (p.status === "completed") {
1135
+ // Completed agent - show success
1136
+ text +=
1137
+ "\n " +
1138
+ theme.fg("dim", branch) +
1139
+ " " +
1140
+ theme.fg("accent", p.agent) +
1141
+ modelTag +
1142
+ theme.fg("dim", " · " + tokenStr);
1143
+ text +=
1144
+ "\n " +
1145
+ theme.fg("dim", cont + " " + TREE_HOOK + " ") +
1146
+ theme.fg("success", "Done");
1147
+ } else {
1148
+ // Running agent - show current tool
1149
+ const toolUses = `${p.toolCount} tool ${pluralize(p.toolCount, "use")}`;
1150
+ const stats = [toolUses, tokenStr].filter(Boolean).join(" · ");
1151
+
1152
+ text +=
1153
+ "\n " +
1154
+ theme.fg("dim", branch) +
1155
+ " " +
1156
+ theme.fg("accent", p.agent) +
1157
+ modelTag +
1158
+ theme.fg("dim", ": ") +
1159
+ theme.fg("muted", taskPreview) +
1160
+ theme.fg("dim", " · " + stats);
1161
+
1162
+ const statusLine =
1163
+ p.currentToolDescription || p.currentTool || "Initializing…";
1164
+ text +=
1165
+ "\n " +
1166
+ theme.fg("dim", cont + " " + TREE_HOOK + " ") +
1167
+ theme.fg("dim", statusLine);
1168
+ }
1169
+ }
1170
+
1171
+ return new Text(text, 0, 0);
1172
+ }
874
1173
 
875
- if (write && outputPaths[i]) {
876
- const preview = previewFirstLines(output, 5);
877
- return `[${r.agent}] ${status} ${outputPaths[i]}\n${preview}`;
878
- } else {
879
- const preview = previewFirstLines(output, 5);
880
- return `[${r.agent}] ${status} (${formatDuration(r.durationMs)})\n${preview}`;
881
- }
882
- });
883
-
884
- return {
885
- content: [{ type: "text", text: `${successCount}/${results.length} succeeded [${formatDuration(totalDuration)}]\n\n${summaries.join("\n\n---\n\n")}` }],
886
- details: { agentScope, projectAgentsDir: discovery.projectAgentsDir, results, totalDurationMs: totalDuration, outputPaths: write ? outputPaths : undefined },
887
- };
888
- },
889
-
890
- renderCall(args, theme) {
891
- // Return minimal - renderResult handles the full display
892
- if (!args.tasks || args.tasks.length === 0) {
893
- return new Text(theme.fg("error", "task: no tasks provided"), 0, 0);
894
- }
895
- return new Text("", 0, 0);
896
- },
897
-
898
- renderResult(result, { expanded, isPartial }, theme) {
899
- const { details } = result;
900
-
901
- // Tree formatting helpers
902
- const TREE_MID = "├─";
903
- const TREE_END = "└─";
904
- const TREE_PIPE = "│";
905
- const TREE_SPACE = " ";
906
- const TREE_HOOK = "⎿";
907
-
908
- const truncateTask = (task: string, maxLen: number) => {
909
- const firstLine = task.split("\n")[0];
910
- return firstLine.length > maxLen ? firstLine.slice(0, maxLen) + "…" : firstLine;
911
- };
912
-
913
- // Handle streaming progress
914
- if (isPartial && details?.progress && details.progress.length > 0) {
915
- const count = details.progress.length;
916
- const completedCount = details.progress.filter((p) => p.status === "completed").length;
917
- const writeNote = details.outputPaths ? " → /tmp" : "";
918
-
919
- let headerText: string;
920
- if (completedCount === count) {
921
- headerText = theme.fg("success", "●") + " " + theme.fg("toolTitle", `${count} ${pluralize(count, "agent")} finished`);
922
- } else if (completedCount > 0) {
923
- headerText = theme.fg("toolTitle", `Running ${count - completedCount}/${count} agents`);
924
- } else {
925
- headerText = theme.fg("toolTitle", `Running ${count} ${pluralize(count, "agent")}`);
926
- }
927
- let text = headerText + theme.fg("dim", writeNote);
928
-
929
- for (let i = 0; i < details.progress.length; i++) {
930
- const p = details.progress[i];
931
- const isLast = i === details.progress.length - 1;
932
- const branch = isLast ? TREE_END : TREE_MID;
933
- const cont = isLast ? TREE_SPACE : TREE_PIPE;
934
-
935
- const taskPreview = truncateTask(p.task, 45);
936
- const tokenStr = p.tokens > 0 ? `${formatTokens(p.tokens)} tokens` : "";
937
-
938
- const modelTag = p.modelOverride ? theme.fg("muted", ` (${p.modelOverride})`) : "";
939
-
940
- if (p.status === "completed") {
941
- // Completed agent - show success
942
- text += "\n " + theme.fg("dim", branch) + " " +
943
- theme.fg("accent", p.agent) + modelTag +
944
- theme.fg("dim", " · " + tokenStr);
945
- text += "\n " + theme.fg("dim", cont + " " + TREE_HOOK + " ") +
946
- theme.fg("success", "Done");
947
- } else {
948
- // Running agent - show current tool
949
- const toolUses = `${p.toolCount} tool ${pluralize(p.toolCount, "use")}`;
950
- const stats = [toolUses, tokenStr].filter(Boolean).join(" · ");
951
-
952
- text += "\n " + theme.fg("dim", branch) + " " +
953
- theme.fg("accent", p.agent) + modelTag + theme.fg("dim", ": ") +
954
- theme.fg("muted", taskPreview) +
955
- theme.fg("dim", " · " + stats);
956
-
957
- const statusLine = p.currentToolDescription || p.currentTool || "Initializing…";
958
- text += "\n " + theme.fg("dim", cont + " " + TREE_HOOK + " ") +
959
- theme.fg("dim", statusLine);
960
- }
961
- }
1174
+ if (!details || details.results.length === 0) {
1175
+ const text = result.content[0];
1176
+ return new Text(text?.type === "text" ? text.text : "", 0, 0);
1177
+ }
962
1178
 
963
- return new Text(text, 0, 0);
964
- }
965
-
966
- if (!details || details.results.length === 0) {
967
- const text = result.content[0];
968
- return new Text(text?.type === "text" ? text.text : "", 0, 0);
969
- }
970
-
971
- // Finished state
972
- const count = details.results.length;
973
- const successCount = details.results.filter((r) => r.exitCode === 0).length;
974
- const allSuccess = successCount === count;
975
- const icon = allSuccess ? theme.fg("success", "●") : theme.fg("warning", "●");
976
- const writeNote = details.outputPaths ? " → /tmp" : "";
977
-
978
- let text = icon + " " +
979
- theme.fg("toolTitle", `${count} ${pluralize(count, "agent")} finished`) +
980
- theme.fg("dim", writeNote);
981
-
982
- for (let i = 0; i < details.results.length; i++) {
983
- const r = details.results[i];
984
- const isLast = i === details.results.length - 1;
985
- const branch = isLast ? TREE_END : TREE_MID;
986
- const cont = isLast ? TREE_SPACE : TREE_PIPE;
987
-
988
- const status = r.exitCode === 0 ? "Done" : `Failed (exit ${r.exitCode})`;
989
- const statusColor = r.exitCode === 0 ? "success" : "error";
990
- const outputPath = details.outputPaths?.[i];
991
- const statusWithPath = outputPath ? `${status} ${outputPath}` : status;
992
- const modelTag = r.modelOverride ? theme.fg("muted", ` (${r.modelOverride})`) : "";
993
-
994
- text += "\n " + theme.fg("dim", branch) + " " +
995
- theme.fg("accent", r.agent) + modelTag +
996
- theme.fg("dim", " ") +
997
- theme.fg(statusColor, statusWithPath);
998
-
999
- const output = r.stdout.trim() || r.stderr.trim();
1000
- if (output) {
1001
- const maxLines = expanded ? 15 : 3;
1002
- const lines = output.split("\n").slice(0, maxLines);
1003
- for (const line of lines) {
1004
- text += "\n " + theme.fg("dim", cont + " ") + theme.fg("dim", line);
1005
- }
1006
- if (output.split("\n").length > maxLines) {
1007
- text += "\n " + theme.fg("dim", cont + " ") + theme.fg("muted", "…");
1008
- }
1009
- }
1010
- }
1179
+ // Finished state
1180
+ const count = details.results.length;
1181
+ const successCount = details.results.filter(
1182
+ (r) => r.exitCode === 0,
1183
+ ).length;
1184
+ const allSuccess = successCount === count;
1185
+ const icon = allSuccess
1186
+ ? theme.fg("success", "●")
1187
+ : theme.fg("warning", "●");
1188
+ const writeNote = details.outputPaths ? " → /tmp" : "";
1189
+
1190
+ let text =
1191
+ icon +
1192
+ " " +
1193
+ theme.fg(
1194
+ "toolTitle",
1195
+ `${count} ${pluralize(count, "agent")} finished`,
1196
+ ) +
1197
+ theme.fg("dim", writeNote);
1198
+
1199
+ for (let i = 0; i < details.results.length; i++) {
1200
+ const r = details.results[i];
1201
+ const isLast = i === details.results.length - 1;
1202
+ const branch = isLast ? TREE_END : TREE_MID;
1203
+ const cont = isLast ? TREE_SPACE : TREE_PIPE;
1204
+
1205
+ const status =
1206
+ r.exitCode === 0 ? "Done" : `Failed (exit ${r.exitCode})`;
1207
+ const statusColor = r.exitCode === 0 ? "success" : "error";
1208
+ const outputPath = details.outputPaths?.[i];
1209
+ const statusWithPath = outputPath
1210
+ ? `${status} ${outputPath}`
1211
+ : status;
1212
+ const modelTag = r.modelOverride
1213
+ ? theme.fg("muted", ` (${r.modelOverride})`)
1214
+ : "";
1215
+
1216
+ text +=
1217
+ "\n " +
1218
+ theme.fg("dim", branch) +
1219
+ " " +
1220
+ theme.fg("accent", r.agent) +
1221
+ modelTag +
1222
+ theme.fg("dim", " ") +
1223
+ theme.fg(statusColor, statusWithPath);
1224
+
1225
+ const output = r.stdout.trim() || r.stderr.trim();
1226
+ if (output) {
1227
+ const maxLines = expanded ? 15 : 3;
1228
+ const lines = output.split("\n").slice(0, maxLines);
1229
+ for (const line of lines) {
1230
+ text +=
1231
+ "\n " + theme.fg("dim", cont + " ") + theme.fg("dim", line);
1232
+ }
1233
+ if (output.split("\n").length > maxLines) {
1234
+ text +=
1235
+ "\n " + theme.fg("dim", cont + " ") + theme.fg("muted", "…");
1236
+ }
1237
+ }
1238
+ }
1011
1239
 
1012
- return new Text(text, 0, 0);
1013
- },
1014
- };
1240
+ return new Text(text, 0, 0);
1241
+ },
1242
+ };
1015
1243
 
1016
- return tool;
1244
+ return tool;
1017
1245
  };
1018
1246
 
1019
1247
  export default factory;