@oh-my-pi/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +32 -0
- package/.github/workflows/publish.yml +42 -0
- package/CHECK.md +352 -0
- package/README.md +224 -0
- package/biome.json +29 -0
- package/bun.lock +50 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3941 -0
- package/dist/commands/create.d.ts +9 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/enable.d.ts +13 -0
- package/dist/commands/enable.d.ts.map +1 -0
- package/dist/commands/info.d.ts +9 -0
- package/dist/commands/info.d.ts.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/install.d.ts +13 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/link.d.ts +10 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/list.d.ts +9 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/outdated.d.ts +9 -0
- package/dist/commands/outdated.d.ts.map +1 -0
- package/dist/commands/search.d.ts +9 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/uninstall.d.ts +9 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/update.d.ts +9 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/why.d.ts +9 -0
- package/dist/commands/why.d.ts.map +1 -0
- package/dist/conflicts.d.ts +21 -0
- package/dist/conflicts.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/manifest.d.ts +81 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/migrate.d.ts +9 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/npm.d.ts +77 -0
- package/dist/npm.d.ts.map +1 -0
- package/dist/paths.d.ts +27 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/symlinks.d.ts +33 -0
- package/dist/symlinks.d.ts.map +1 -0
- package/package.json +36 -0
- package/plugins/metal-theme/README.md +13 -0
- package/plugins/metal-theme/omp.json +8 -0
- package/plugins/metal-theme/package.json +14 -0
- package/plugins/metal-theme/themes/metal.json +79 -0
- package/plugins/subagents/README.md +25 -0
- package/plugins/subagents/agents/explore.md +71 -0
- package/plugins/subagents/agents/planner.md +51 -0
- package/plugins/subagents/agents/reviewer.md +53 -0
- package/plugins/subagents/agents/task.md +46 -0
- package/plugins/subagents/commands/architect-plan.md +9 -0
- package/plugins/subagents/commands/implement-with-critic.md +10 -0
- package/plugins/subagents/commands/implement.md +10 -0
- package/plugins/subagents/omp.json +15 -0
- package/plugins/subagents/package.json +21 -0
- package/plugins/subagents/tools/task/index.ts +1019 -0
- package/scripts/bump-version.sh +52 -0
- package/scripts/publish.sh +35 -0
- package/src/cli.ts +167 -0
- package/src/commands/create.ts +153 -0
- package/src/commands/doctor.ts +217 -0
- package/src/commands/enable.ts +105 -0
- package/src/commands/info.ts +84 -0
- package/src/commands/init.ts +42 -0
- package/src/commands/install.ts +327 -0
- package/src/commands/link.ts +108 -0
- package/src/commands/list.ts +71 -0
- package/src/commands/outdated.ts +76 -0
- package/src/commands/search.ts +60 -0
- package/src/commands/uninstall.ts +73 -0
- package/src/commands/update.ts +112 -0
- package/src/commands/why.ts +105 -0
- package/src/conflicts.ts +84 -0
- package/src/index.ts +53 -0
- package/src/manifest.ts +212 -0
- package/src/migrate.ts +181 -0
- package/src/npm.ts +150 -0
- package/src/paths.ts +72 -0
- package/src/symlinks.ts +199 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Tool - Delegate tasks to specialized agents
|
|
3
|
+
*
|
|
4
|
+
* Discovers agent definitions from:
|
|
5
|
+
* - ~/.pi/agent/agents/*.md (user-level)
|
|
6
|
+
* - .pi/agents/*.md (project-level, opt-in via agentScope)
|
|
7
|
+
*
|
|
8
|
+
* Agent files use markdown with YAML frontmatter:
|
|
9
|
+
*
|
|
10
|
+
* ---
|
|
11
|
+
* name: explore
|
|
12
|
+
* description: Fast codebase recon
|
|
13
|
+
* tools: read, grep, find, ls, bash
|
|
14
|
+
* model: claude-haiku-4-5
|
|
15
|
+
* ---
|
|
16
|
+
*
|
|
17
|
+
* You are a scout. Quickly investigate and return findings.
|
|
18
|
+
*
|
|
19
|
+
* The tool spawns a separate `pi` process for each task, giving it an
|
|
20
|
+
* isolated context window. All tasks run in parallel.
|
|
21
|
+
*
|
|
22
|
+
* Parameters:
|
|
23
|
+
* - tasks: Array of {agent, task} to run in parallel
|
|
24
|
+
* - context: (optional) Shared context prepended to all task prompts
|
|
25
|
+
* - write: (optional) Write results to /tmp/task_{agent}_{i}.md
|
|
26
|
+
* - agentScope: "user" | "project" | "both"
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import * as fs from "node:fs";
|
|
30
|
+
import * as os from "node:os";
|
|
31
|
+
import * as path from "node:path";
|
|
32
|
+
import { spawn } from "node:child_process";
|
|
33
|
+
import * as readline from "node:readline";
|
|
34
|
+
import { Type } from "@sinclair/typebox";
|
|
35
|
+
import { StringEnum, type AgentToolUpdateCallback } from "@mariozechner/pi-ai";
|
|
36
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
37
|
+
import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
|
|
38
|
+
|
|
39
|
+
const MAX_OUTPUT_LINES = 5000;
|
|
40
|
+
const MAX_OUTPUT_BYTES = 500_000;
|
|
41
|
+
const MAX_PARALLEL_TASKS = 32;
|
|
42
|
+
const MAX_CONCURRENCY = 16;
|
|
43
|
+
const MAX_AGENTS_IN_DESCRIPTION = 10;
|
|
44
|
+
|
|
45
|
+
type AgentScope = "user" | "project" | "both";
|
|
46
|
+
|
|
47
|
+
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;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
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;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
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;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
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[];
|
|
95
|
+
}
|
|
96
|
+
|
|
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;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { frontmatter, body };
|
|
125
|
+
}
|
|
126
|
+
|
|
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;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isDirectory(p: string): boolean {
|
|
190
|
+
try {
|
|
191
|
+
return fs.statSync(p).isDirectory();
|
|
192
|
+
} catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
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
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
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 };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
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
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
i++;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (i < output.length) {
|
|
256
|
+
truncated = true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (truncated && lineBudget <= 0 && lastNewlineIndex >= 0) {
|
|
260
|
+
output = output.slice(0, lastNewlineIndex);
|
|
261
|
+
} else {
|
|
262
|
+
output = output.slice(0, i);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { text: output, truncated };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
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;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function sanitizeAgentName(name: string): string {
|
|
283
|
+
return name.replace(/[^\w.-]+/g, "_").slice(0, 50);
|
|
284
|
+
}
|
|
285
|
+
|
|
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
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!preview) {
|
|
316
|
+
return toolName;
|
|
317
|
+
}
|
|
318
|
+
|
|
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
|
+
}
|
|
324
|
+
|
|
325
|
+
return `${toolName}: ${preview}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
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`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
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`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function pluralize(count: number, singular: string, plural?: string): string {
|
|
343
|
+
return count === 1 ? singular : (plural ?? singular + "s");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
347
|
+
items: TIn[],
|
|
348
|
+
concurrency: number,
|
|
349
|
+
fn: (item: TIn, index: number) => Promise<TOut>
|
|
350
|
+
): 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;
|
|
366
|
+
}
|
|
367
|
+
|
|
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 };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
interface RunAgentOptions {
|
|
377
|
+
onProgress?: (progress: AgentProgress) => void;
|
|
378
|
+
index?: number;
|
|
379
|
+
signal?: AbortSignal;
|
|
380
|
+
model?: string;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function runSingleAgent(
|
|
384
|
+
cwd: string,
|
|
385
|
+
agents: AgentConfig[],
|
|
386
|
+
agentName: string,
|
|
387
|
+
task: string,
|
|
388
|
+
step?: number,
|
|
389
|
+
options?: RunAgentOptions
|
|
390
|
+
): 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;
|
|
408
|
+
|
|
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,
|
|
420
|
+
};
|
|
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
|
+
|
|
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
|
+
};
|
|
509
|
+
|
|
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
|
|
567
|
+
}
|
|
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;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Fallback if agent_end wasn't received
|
|
592
|
+
const stdoutResult = truncateOutput(lastTextContent);
|
|
593
|
+
const stderrResult = truncateOutput(stderrContent);
|
|
594
|
+
|
|
595
|
+
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,
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
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
|
+
});
|
|
623
|
+
});
|
|
624
|
+
} finally {
|
|
625
|
+
if (tmpPromptPath) {
|
|
626
|
+
try { fs.unlinkSync(tmpPromptPath); } catch { /* ignore */ }
|
|
627
|
+
}
|
|
628
|
+
if (tmpPromptDir) {
|
|
629
|
+
try { fs.rmdirSync(tmpPromptDir); } catch { /* ignore */ }
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
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)" })),
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
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",
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
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",
|
|
651
|
+
default: false,
|
|
652
|
+
})),
|
|
653
|
+
agentScope: Type.Optional(AgentScopeSchema),
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Builds the dynamic tool description based on discovered agents.
|
|
658
|
+
* Mirrors Claude Code's Task tool description format.
|
|
659
|
+
*/
|
|
660
|
+
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)) {
|
|
673
|
+
const tools = agent.tools?.join(", ") || "All tools";
|
|
674
|
+
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");
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
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
|
+
});
|
|
853
|
+
|
|
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
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return result;
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
868
|
+
const totalDuration = Date.now() - startTime;
|
|
869
|
+
|
|
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)";
|
|
874
|
+
|
|
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
|
+
}
|
|
962
|
+
|
|
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
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return new Text(text, 0, 0);
|
|
1013
|
+
},
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
return tool;
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
export default factory;
|