@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.
- package/README.md +3 -0
- package/agents/explore.md +11 -0
- package/agents/planner.md +3 -0
- package/agents/reviewer.md +6 -0
- package/agents/task.md +8 -1
- package/commands/architect-plan.md +1 -0
- package/commands/implement-with-critic.md +1 -0
- package/commands/implement.md +1 -0
- package/package.json +49 -12
- package/tools/task/index.ts +1089 -861
package/tools/task/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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): {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
134
|
+
return { frontmatter, body };
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
function loadAgentsFromDir(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
253
|
-
|
|
272
|
+
i++;
|
|
273
|
+
}
|
|
254
274
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
275
|
+
if (i < output.length) {
|
|
276
|
+
truncated = true;
|
|
277
|
+
}
|
|
258
278
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
285
|
+
return { text: output, truncated };
|
|
266
286
|
}
|
|
267
287
|
|
|
268
288
|
function previewFirstLines(text: string, maxLines: number): string {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
303
|
+
return name.replace(/[^\w.-]+/g, "_").slice(0, 50);
|
|
284
304
|
}
|
|
285
305
|
|
|
286
|
-
function formatToolArgs(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
338
|
+
if (!preview) {
|
|
339
|
+
return toolName;
|
|
340
|
+
}
|
|
318
341
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
348
|
+
return `${toolName}: ${preview}`;
|
|
326
349
|
}
|
|
327
350
|
|
|
328
351
|
function formatDuration(ms: number): string {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
366
|
+
return count === 1 ? singular : (plural ?? singular + "s");
|
|
344
367
|
}
|
|
345
368
|
|
|
346
369
|
async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
370
|
+
items: TIn[],
|
|
371
|
+
concurrency: number,
|
|
372
|
+
fn: (item: TIn, index: number) => Promise<TOut>,
|
|
350
373
|
): Promise<TOut[]> {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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(
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
403
|
+
onProgress?: (progress: AgentProgress) => void;
|
|
404
|
+
index?: number;
|
|
405
|
+
signal?: AbortSignal;
|
|
406
|
+
model?: string;
|
|
381
407
|
}
|
|
382
408
|
|
|
383
409
|
async function runSingleAgent(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
410
|
+
cwd: string,
|
|
411
|
+
agents: AgentConfig[],
|
|
412
|
+
agentName: string,
|
|
413
|
+
task: string,
|
|
414
|
+
step?: number,
|
|
415
|
+
options?: RunAgentOptions,
|
|
390
416
|
): Promise<SingleResult> {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
657
|
+
});
|
|
658
|
+
} finally {
|
|
659
|
+
if (tmpPromptPath) {
|
|
660
|
+
try {
|
|
661
|
+
fs.unlinkSync(tmpPromptPath);
|
|
662
|
+
} catch {
|
|
663
|
+
/* ignore */
|
|
627
664
|
}
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
677
|
-
lines.push(
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
|
|
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
|
-
|
|
868
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1240
|
+
return new Text(text, 0, 0);
|
|
1241
|
+
},
|
|
1242
|
+
};
|
|
1015
1243
|
|
|
1016
|
-
|
|
1244
|
+
return tool;
|
|
1017
1245
|
};
|
|
1018
1246
|
|
|
1019
1247
|
export default factory;
|