@narumitw/pi-subagents 0.16.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/execution.ts +29 -1
- package/src/render.ts +460 -345
- package/src/runner.ts +192 -38
package/package.json
CHANGED
package/src/execution.ts
CHANGED
|
@@ -286,6 +286,34 @@ export async function executeSubagent(
|
|
|
286
286
|
const emitParallelUpdate = () => {
|
|
287
287
|
status.update(parallelStatus(doneCount, allResults.length, runningCount));
|
|
288
288
|
if (onUpdate) {
|
|
289
|
+
const aggregator = params.aggregator;
|
|
290
|
+
const pendingAggregator: SingleResult | undefined =
|
|
291
|
+
aggregator && !signal?.aborted && doneCount === allResults.length
|
|
292
|
+
? {
|
|
293
|
+
agent: aggregator.agent,
|
|
294
|
+
agentSource:
|
|
295
|
+
agents.find((agent) => agent.name === aggregator.agent)?.source ?? "unknown",
|
|
296
|
+
task: aggregator.task,
|
|
297
|
+
exitCode: -1,
|
|
298
|
+
messages: [],
|
|
299
|
+
stderr: "",
|
|
300
|
+
usage: {
|
|
301
|
+
input: 0,
|
|
302
|
+
output: 0,
|
|
303
|
+
cacheRead: 0,
|
|
304
|
+
cacheWrite: 0,
|
|
305
|
+
cost: 0,
|
|
306
|
+
contextTokens: 0,
|
|
307
|
+
turns: 0,
|
|
308
|
+
},
|
|
309
|
+
thinkingLevel: resolveThinkingLevel(
|
|
310
|
+
aggregator.agent,
|
|
311
|
+
aggregator.thinkingLevel,
|
|
312
|
+
),
|
|
313
|
+
timeoutMs: resolveTimeoutMs(aggregator.agent, aggregator.timeoutMs),
|
|
314
|
+
finalOutput: "",
|
|
315
|
+
}
|
|
316
|
+
: undefined;
|
|
289
317
|
onUpdate({
|
|
290
318
|
content: [
|
|
291
319
|
{
|
|
@@ -293,7 +321,7 @@ export async function executeSubagent(
|
|
|
293
321
|
text: `Parallel: ${doneCount}/${allResults.length} done, ${runningCount} running...`,
|
|
294
322
|
},
|
|
295
323
|
],
|
|
296
|
-
details: makeDetails("parallel")([...allResults]),
|
|
324
|
+
details: makeDetails("parallel")([...allResults], pendingAggregator),
|
|
297
325
|
});
|
|
298
326
|
}
|
|
299
327
|
};
|
package/src/render.ts
CHANGED
|
@@ -38,6 +38,8 @@ export function formatUsageStats(
|
|
|
38
38
|
},
|
|
39
39
|
model?: string,
|
|
40
40
|
thinkingLevel?: SubagentThinkingLevel,
|
|
41
|
+
actualProvider?: string,
|
|
42
|
+
actualModel?: string,
|
|
41
43
|
): string {
|
|
42
44
|
const parts: string[] = [];
|
|
43
45
|
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
@@ -46,14 +48,27 @@ export function formatUsageStats(
|
|
|
46
48
|
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
47
49
|
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
48
50
|
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
49
|
-
if (usage.contextTokens && usage.contextTokens > 0)
|
|
51
|
+
if (usage.contextTokens && usage.contextTokens > 0)
|
|
50
52
|
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
const actual =
|
|
54
|
+
actualProvider && actualModel
|
|
55
|
+
? `${actualProvider}/${actualModel}`
|
|
56
|
+
: (actualModel ?? actualProvider);
|
|
57
|
+
if (actual ?? model) parts.push(actual ?? model ?? "");
|
|
58
|
+
if (thinkingLevel) parts.push(`requested-thinking:${thinkingLevel}`);
|
|
54
59
|
return parts.join(" ");
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
function formatResultUsageStats(result: SingleResult): string {
|
|
63
|
+
return formatUsageStats(
|
|
64
|
+
result.usage,
|
|
65
|
+
result.model,
|
|
66
|
+
result.thinkingLevel,
|
|
67
|
+
result.actualProvider,
|
|
68
|
+
result.actualModel,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
function formatToolCall(
|
|
58
73
|
toolName: string,
|
|
59
74
|
args: Record<string, unknown>,
|
|
@@ -103,7 +118,11 @@ function formatToolCall(
|
|
|
103
118
|
case "find": {
|
|
104
119
|
const pattern = (args.pattern || "*") as string;
|
|
105
120
|
const rawPath = (args.path || ".") as string;
|
|
106
|
-
return
|
|
121
|
+
return (
|
|
122
|
+
themeFg("muted", "find ") +
|
|
123
|
+
themeFg("accent", pattern) +
|
|
124
|
+
themeFg("dim", ` in ${shortenPath(rawPath)}`)
|
|
125
|
+
);
|
|
107
126
|
}
|
|
108
127
|
case "grep": {
|
|
109
128
|
const pattern = (args.pattern || "") as string;
|
|
@@ -131,400 +150,496 @@ function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
|
131
150
|
for (const msg of messages) {
|
|
132
151
|
if (msg.role === "assistant") {
|
|
133
152
|
for (const part of msg.content) {
|
|
134
|
-
if (part.type === "text")
|
|
135
|
-
|
|
153
|
+
if (part.type === "text") {
|
|
154
|
+
const text = part.text.trim();
|
|
155
|
+
if (text) items.push({ type: "text", text });
|
|
156
|
+
} else if (part.type === "toolCall") {
|
|
157
|
+
items.push({ type: "toolCall", name: part.name, args: part.arguments });
|
|
158
|
+
}
|
|
136
159
|
}
|
|
137
160
|
}
|
|
138
161
|
}
|
|
139
162
|
return items;
|
|
140
163
|
}
|
|
164
|
+
function getCollapsedDisplayItems(result: SingleResult): { items: DisplayItem[]; total: number } {
|
|
165
|
+
if (result.recentActivity && result.recentActivity.length > 0) {
|
|
166
|
+
return {
|
|
167
|
+
items: result.recentActivity,
|
|
168
|
+
total: Math.max(result.recentActivity.length, result.recentActivityTotal ?? 0),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const items = getDisplayItems(result.messages);
|
|
172
|
+
return { items, total: items.length };
|
|
173
|
+
}
|
|
141
174
|
|
|
142
175
|
export function renderSubagentCall(args: SubagentParams, theme: Theme) {
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
176
|
+
const scope: AgentScope = args.agentScope ?? "user";
|
|
177
|
+
if (args.chain && args.chain.length > 0) {
|
|
178
|
+
let text =
|
|
179
|
+
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
180
|
+
theme.fg("accent", `chain (${args.chain.length} steps)`) +
|
|
181
|
+
theme.fg("muted", ` [${scope}]`);
|
|
182
|
+
for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
|
|
183
|
+
const step = args.chain[i];
|
|
184
|
+
// Clean up {previous} placeholder for display
|
|
185
|
+
const cleanTask = step.task.replace(/\{previous\}/g, "").trim();
|
|
186
|
+
const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask;
|
|
187
|
+
text +=
|
|
188
|
+
"\n " +
|
|
189
|
+
theme.fg("muted", `${i + 1}.`) +
|
|
190
|
+
" " +
|
|
191
|
+
theme.fg("accent", step.agent) +
|
|
192
|
+
theme.fg("dim", ` ${preview}`);
|
|
193
|
+
}
|
|
194
|
+
if (args.chain.length > 3)
|
|
195
|
+
text += `\n ${theme.fg("muted", `... +${args.chain.length - 3} more`)}`;
|
|
196
|
+
return new Text(text, 0, 0);
|
|
197
|
+
}
|
|
198
|
+
if (args.tasks && args.tasks.length > 0) {
|
|
199
|
+
let text =
|
|
200
|
+
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
201
|
+
theme.fg("accent", `parallel (${args.tasks.length} tasks)`) +
|
|
202
|
+
theme.fg("muted", ` [${scope}]`);
|
|
203
|
+
for (const t of args.tasks.slice(0, 3)) {
|
|
204
|
+
const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task;
|
|
205
|
+
text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", ` ${preview}`)}`;
|
|
206
|
+
}
|
|
207
|
+
if (args.tasks.length > 3)
|
|
208
|
+
text += `\n ${theme.fg("muted", `... +${args.tasks.length - 3} more`)}`;
|
|
209
|
+
if (args.aggregator) {
|
|
210
|
+
const preview =
|
|
211
|
+
args.aggregator.task.length > 40
|
|
212
|
+
? `${args.aggregator.task.slice(0, 40)}...`
|
|
213
|
+
: args.aggregator.task;
|
|
214
|
+
text += `\n ${theme.fg("muted", "fan-in → ")}${theme.fg("accent", args.aggregator.agent)}${theme.fg(
|
|
215
|
+
"dim",
|
|
216
|
+
` ${preview}`,
|
|
217
|
+
)}`;
|
|
218
|
+
}
|
|
219
|
+
return new Text(text, 0, 0);
|
|
220
|
+
}
|
|
221
|
+
const agentName = args.agent || "...";
|
|
222
|
+
const preview = args.task
|
|
223
|
+
? args.task.length > 60
|
|
224
|
+
? `${args.task.slice(0, 60)}...`
|
|
225
|
+
: args.task
|
|
226
|
+
: "...";
|
|
227
|
+
let text =
|
|
228
|
+
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
229
|
+
theme.fg("accent", agentName) +
|
|
230
|
+
theme.fg("muted", ` [${scope}]`);
|
|
231
|
+
text += `\n ${theme.fg("dim", preview)}`;
|
|
232
|
+
return new Text(text, 0, 0);
|
|
192
233
|
}
|
|
193
234
|
|
|
194
235
|
export function renderSubagentResult(
|
|
195
236
|
result: AgentToolResult<SubagentDetails>,
|
|
196
|
-
{ expanded }: ToolRenderResultOptions,
|
|
237
|
+
{ expanded, isPartial }: ToolRenderResultOptions,
|
|
197
238
|
theme: Theme,
|
|
198
239
|
) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
240
|
+
const details = result.details as SubagentDetails | undefined;
|
|
241
|
+
if (!details || details.results.length === 0) {
|
|
242
|
+
const text = result.content[0];
|
|
243
|
+
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const mdTheme = getMarkdownTheme();
|
|
247
|
+
|
|
248
|
+
const renderDisplayItems = (items: DisplayItem[], limit?: number, total = items.length) => {
|
|
249
|
+
const toShow = limit ? items.slice(-limit) : items;
|
|
250
|
+
const skipped = Math.max(0, total - toShow.length);
|
|
251
|
+
let text = "";
|
|
252
|
+
if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
|
|
253
|
+
for (const item of toShow) {
|
|
254
|
+
if (item.type === "text") {
|
|
255
|
+
const preview = expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n");
|
|
256
|
+
text += `${theme.fg("toolOutput", preview)}\n`;
|
|
257
|
+
} else {
|
|
258
|
+
text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
|
|
203
259
|
}
|
|
260
|
+
}
|
|
261
|
+
return text.trimEnd();
|
|
262
|
+
};
|
|
204
263
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
264
|
+
if (details.mode === "single" && details.results.length === 1) {
|
|
265
|
+
const r = details.results[0];
|
|
266
|
+
const isError = isResultError(r);
|
|
267
|
+
const icon = isError
|
|
268
|
+
? theme.fg("error", "✗")
|
|
269
|
+
: isPartial
|
|
270
|
+
? theme.fg("warning", "⏳")
|
|
271
|
+
: theme.fg("success", "✓");
|
|
272
|
+
const displayItems = getDisplayItems(r.messages);
|
|
273
|
+
const finalOutput = getResultFinalOutput(r);
|
|
274
|
+
|
|
275
|
+
if (expanded) {
|
|
276
|
+
const container = new Container();
|
|
277
|
+
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
278
|
+
if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
279
|
+
container.addChild(new Text(header, 0, 0));
|
|
280
|
+
if (isError && r.errorMessage)
|
|
281
|
+
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
282
|
+
container.addChild(new Spacer(1));
|
|
283
|
+
container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
|
|
284
|
+
container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
|
|
285
|
+
container.addChild(new Spacer(1));
|
|
286
|
+
container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
|
|
287
|
+
if (displayItems.length === 0 && !finalOutput) {
|
|
288
|
+
container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
|
|
289
|
+
} else {
|
|
290
|
+
for (const item of displayItems) {
|
|
291
|
+
if (item.type === "toolCall")
|
|
292
|
+
container.addChild(
|
|
293
|
+
new Text(
|
|
294
|
+
theme.fg("muted", "→ ") +
|
|
295
|
+
formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
296
|
+
0,
|
|
297
|
+
0,
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
if (finalOutput) {
|
|
302
|
+
container.addChild(new Spacer(1));
|
|
303
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
219
304
|
}
|
|
220
|
-
|
|
221
|
-
|
|
305
|
+
}
|
|
306
|
+
const usageStr = formatResultUsageStats(r);
|
|
307
|
+
if (usageStr) {
|
|
308
|
+
container.addChild(new Spacer(1));
|
|
309
|
+
container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
310
|
+
}
|
|
311
|
+
return container;
|
|
312
|
+
}
|
|
222
313
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
314
|
+
const collapsed = getCollapsedDisplayItems(r);
|
|
315
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
316
|
+
if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
317
|
+
if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
318
|
+
if (collapsed.items.length > 0) {
|
|
319
|
+
text += `\n${renderDisplayItems(collapsed.items, COLLAPSED_ITEM_COUNT, collapsed.total)}`;
|
|
320
|
+
if (collapsed.total > COLLAPSED_ITEM_COUNT)
|
|
321
|
+
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
322
|
+
} else if (finalOutput.trim()) {
|
|
323
|
+
text += `\n${theme.fg("toolOutput", finalOutput.trim().split("\n").slice(0, 3).join("\n"))}`;
|
|
324
|
+
} else if (!isError || !r.errorMessage) {
|
|
325
|
+
text += `\n${theme.fg("muted", isPartial && !isError ? "(running...)" : "(no output)")}`;
|
|
326
|
+
}
|
|
327
|
+
const usageStr = formatResultUsageStats(r);
|
|
328
|
+
if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
|
|
329
|
+
return new Text(text, 0, 0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const aggregateUsage = (results: SingleResult[]) => {
|
|
333
|
+
const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
334
|
+
for (const r of results) {
|
|
335
|
+
total.input += r.usage.input;
|
|
336
|
+
total.output += r.usage.output;
|
|
337
|
+
total.cacheRead += r.usage.cacheRead;
|
|
338
|
+
total.cacheWrite += r.usage.cacheWrite;
|
|
339
|
+
total.cost += r.usage.cost;
|
|
340
|
+
total.turns += r.usage.turns;
|
|
341
|
+
}
|
|
342
|
+
return total;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
if (details.mode === "chain") {
|
|
346
|
+
const currentResult = details.results.at(-1);
|
|
347
|
+
const currentIsRunning =
|
|
348
|
+
isPartial && currentResult !== undefined && !isResultError(currentResult);
|
|
349
|
+
const successCount = details.results.filter(
|
|
350
|
+
(result) => !isResultError(result) && (!currentIsRunning || result !== currentResult),
|
|
351
|
+
).length;
|
|
352
|
+
const icon = currentIsRunning
|
|
353
|
+
? theme.fg("warning", "⏳")
|
|
354
|
+
: successCount === details.results.length
|
|
355
|
+
? theme.fg("success", "✓")
|
|
356
|
+
: theme.fg("error", "✗");
|
|
357
|
+
|
|
358
|
+
if (expanded) {
|
|
359
|
+
const container = new Container();
|
|
360
|
+
container.addChild(
|
|
361
|
+
new Text(
|
|
362
|
+
icon +
|
|
363
|
+
" " +
|
|
364
|
+
theme.fg("toolTitle", theme.bold("chain ")) +
|
|
365
|
+
theme.fg("accent", `${successCount}/${details.results.length} steps`),
|
|
366
|
+
0,
|
|
367
|
+
0,
|
|
368
|
+
),
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
for (const r of details.results) {
|
|
372
|
+
const rFailed = isResultError(r);
|
|
373
|
+
const rIcon = rFailed
|
|
374
|
+
? theme.fg("error", "✗")
|
|
375
|
+
: currentIsRunning && r === currentResult
|
|
376
|
+
? theme.fg("warning", "⏳")
|
|
377
|
+
: theme.fg("success", "✓");
|
|
227
378
|
const displayItems = getDisplayItems(r.messages);
|
|
228
379
|
const finalOutput = getResultFinalOutput(r);
|
|
229
380
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
container.addChild(new
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
if (finalOutput) {
|
|
256
|
-
container.addChild(new Spacer(1));
|
|
257
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
const usageStr = formatUsageStats(r.usage, r.model, r.thinkingLevel);
|
|
261
|
-
if (usageStr) {
|
|
262
|
-
container.addChild(new Spacer(1));
|
|
263
|
-
container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
381
|
+
container.addChild(new Spacer(1));
|
|
382
|
+
container.addChild(
|
|
383
|
+
new Text(
|
|
384
|
+
`${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`,
|
|
385
|
+
0,
|
|
386
|
+
0,
|
|
387
|
+
),
|
|
388
|
+
);
|
|
389
|
+
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
390
|
+
if (rFailed && r.errorMessage)
|
|
391
|
+
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
392
|
+
|
|
393
|
+
// Show tool calls
|
|
394
|
+
for (const item of displayItems) {
|
|
395
|
+
if (item.type === "toolCall") {
|
|
396
|
+
container.addChild(
|
|
397
|
+
new Text(
|
|
398
|
+
theme.fg("muted", "→ ") +
|
|
399
|
+
formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
400
|
+
0,
|
|
401
|
+
0,
|
|
402
|
+
),
|
|
403
|
+
);
|
|
264
404
|
}
|
|
265
|
-
return container;
|
|
266
405
|
}
|
|
267
406
|
|
|
268
|
-
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
else {
|
|
273
|
-
text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
|
|
274
|
-
if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
407
|
+
// Show final output as markdown
|
|
408
|
+
if (finalOutput) {
|
|
409
|
+
container.addChild(new Spacer(1));
|
|
410
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
275
411
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
412
|
+
|
|
413
|
+
const stepUsage = formatResultUsageStats(r);
|
|
414
|
+
if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
|
|
279
415
|
}
|
|
280
416
|
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
total.cost += r.usage.cost;
|
|
289
|
-
total.turns += r.usage.turns;
|
|
290
|
-
}
|
|
291
|
-
return total;
|
|
292
|
-
};
|
|
417
|
+
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
418
|
+
if (usageStr) {
|
|
419
|
+
container.addChild(new Spacer(1));
|
|
420
|
+
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
|
|
421
|
+
}
|
|
422
|
+
return container;
|
|
423
|
+
}
|
|
293
424
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
425
|
+
// Collapsed view
|
|
426
|
+
let text =
|
|
427
|
+
icon +
|
|
428
|
+
" " +
|
|
429
|
+
theme.fg("toolTitle", theme.bold("chain ")) +
|
|
430
|
+
theme.fg("accent", `${successCount}/${details.results.length} steps`);
|
|
431
|
+
for (const r of details.results) {
|
|
432
|
+
const rFailed = isResultError(r);
|
|
433
|
+
const rIcon = rFailed
|
|
434
|
+
? theme.fg("error", "✗")
|
|
435
|
+
: currentIsRunning && r === currentResult
|
|
436
|
+
? theme.fg("warning", "⏳")
|
|
437
|
+
: theme.fg("success", "✓");
|
|
438
|
+
const collapsed = getCollapsedDisplayItems(r);
|
|
439
|
+
const finalOutput = getResultFinalOutput(r).trim();
|
|
440
|
+
text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
441
|
+
if (rFailed && r.errorMessage)
|
|
442
|
+
text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
443
|
+
if (collapsed.items.length > 0)
|
|
444
|
+
text += `\n${renderDisplayItems(collapsed.items, 5, collapsed.total)}`;
|
|
445
|
+
else if (currentIsRunning && r === currentResult)
|
|
446
|
+
text += `\n${theme.fg("muted", "(running...)")}`;
|
|
447
|
+
else if (finalOutput)
|
|
448
|
+
text += `\n${theme.fg("toolOutput", finalOutput.split("\n").slice(0, 3).join("\n"))}`;
|
|
449
|
+
else if (!rFailed || !r.errorMessage) text += `\n${theme.fg("muted", "(no output)")}`;
|
|
450
|
+
}
|
|
451
|
+
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
452
|
+
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
453
|
+
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
454
|
+
return new Text(text, 0, 0);
|
|
455
|
+
}
|
|
297
456
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
457
|
+
if (details.mode === "parallel") {
|
|
458
|
+
const resultIsRunning = (result: SingleResult) =>
|
|
459
|
+
result.exitCode === -1 && !isResultError(result);
|
|
460
|
+
const running = details.results.filter(resultIsRunning).length;
|
|
461
|
+
const successCount = details.results.filter(
|
|
462
|
+
(result) => result.exitCode !== -1 && !isResultError(result),
|
|
463
|
+
).length;
|
|
464
|
+
const failCount = details.results.filter(isResultError).length;
|
|
465
|
+
const aggregator = details.aggregator;
|
|
466
|
+
const aggregatorFailed = aggregator ? isResultError(aggregator) : false;
|
|
467
|
+
const aggregatorRunning = aggregator
|
|
468
|
+
? !aggregatorFailed && (isPartial || aggregator.exitCode === -1)
|
|
469
|
+
: false;
|
|
470
|
+
const pendingSuccessfulSettlement =
|
|
471
|
+
isPartial && !aggregator && running === 0 && failCount === 0;
|
|
472
|
+
const isRunning = running > 0 || aggregatorRunning || pendingSuccessfulSettlement;
|
|
473
|
+
const icon = isRunning
|
|
474
|
+
? theme.fg("warning", "⏳")
|
|
475
|
+
: failCount > 0 || aggregatorFailed
|
|
476
|
+
? theme.fg("warning", "◐")
|
|
477
|
+
: theme.fg("success", "✓");
|
|
478
|
+
const status = isRunning
|
|
479
|
+
? aggregatorRunning
|
|
480
|
+
? `${successCount + failCount}/${details.results.length} done, fan-in running`
|
|
481
|
+
: running > 0
|
|
482
|
+
? `${successCount + failCount}/${details.results.length} done, ${running} running`
|
|
483
|
+
: `${successCount + failCount}/${details.results.length} done, running`
|
|
484
|
+
: aggregator
|
|
485
|
+
? `${successCount}/${details.results.length} tasks + fan-in`
|
|
486
|
+
: `${successCount}/${details.results.length} tasks`;
|
|
487
|
+
|
|
488
|
+
if (expanded && !isRunning) {
|
|
489
|
+
const container = new Container();
|
|
490
|
+
container.addChild(
|
|
491
|
+
new Text(
|
|
492
|
+
`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
|
|
493
|
+
0,
|
|
494
|
+
0,
|
|
495
|
+
),
|
|
496
|
+
);
|
|
310
497
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
498
|
+
for (const r of details.results) {
|
|
499
|
+
const rFailed = isResultError(r);
|
|
500
|
+
const rIcon = rFailed
|
|
501
|
+
? theme.fg("error", "✗")
|
|
502
|
+
: resultIsRunning(r)
|
|
503
|
+
? theme.fg("warning", "⏳")
|
|
504
|
+
: theme.fg("success", "✓");
|
|
505
|
+
const displayItems = getDisplayItems(r.messages);
|
|
506
|
+
const finalOutput = getResultFinalOutput(r);
|
|
315
507
|
|
|
316
|
-
|
|
508
|
+
container.addChild(new Spacer(1));
|
|
509
|
+
container.addChild(
|
|
510
|
+
new Text(`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0),
|
|
511
|
+
);
|
|
512
|
+
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
513
|
+
if (rFailed && r.errorMessage)
|
|
514
|
+
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
515
|
+
|
|
516
|
+
// Show tool calls
|
|
517
|
+
for (const item of displayItems) {
|
|
518
|
+
if (item.type === "toolCall") {
|
|
317
519
|
container.addChild(
|
|
318
520
|
new Text(
|
|
319
|
-
|
|
521
|
+
theme.fg("muted", "→ ") +
|
|
522
|
+
formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
320
523
|
0,
|
|
321
524
|
0,
|
|
322
525
|
),
|
|
323
526
|
);
|
|
324
|
-
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
325
|
-
|
|
326
|
-
// Show tool calls
|
|
327
|
-
for (const item of displayItems) {
|
|
328
|
-
if (item.type === "toolCall") {
|
|
329
|
-
container.addChild(
|
|
330
|
-
new Text(
|
|
331
|
-
theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
332
|
-
0,
|
|
333
|
-
0,
|
|
334
|
-
),
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Show final output as markdown
|
|
340
|
-
if (finalOutput) {
|
|
341
|
-
container.addChild(new Spacer(1));
|
|
342
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const stepUsage = formatUsageStats(r.usage, r.model, r.thinkingLevel);
|
|
346
|
-
if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
350
|
-
if (usageStr) {
|
|
351
|
-
container.addChild(new Spacer(1));
|
|
352
|
-
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
|
|
353
527
|
}
|
|
354
|
-
return container;
|
|
355
528
|
}
|
|
356
529
|
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
theme.fg("toolTitle", theme.bold("chain ")) +
|
|
362
|
-
theme.fg("accent", `${successCount}/${details.results.length} steps`);
|
|
363
|
-
for (const r of details.results) {
|
|
364
|
-
const rIcon = !isResultError(r) ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
365
|
-
const displayItems = getDisplayItems(r.messages);
|
|
366
|
-
text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
367
|
-
if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
|
|
368
|
-
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
|
530
|
+
// Show final output as markdown
|
|
531
|
+
if (finalOutput) {
|
|
532
|
+
container.addChild(new Spacer(1));
|
|
533
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
369
534
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return new Text(text, 0, 0);
|
|
535
|
+
|
|
536
|
+
const taskUsage = formatResultUsageStats(r);
|
|
537
|
+
if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
|
|
374
538
|
}
|
|
375
539
|
|
|
376
|
-
if (
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const failCount = details.results.filter(
|
|
382
|
-
(result) => result.exitCode !== -1 && isResultError(result),
|
|
383
|
-
).length;
|
|
384
|
-
const aggregator = details.aggregator;
|
|
385
|
-
const aggregatorRunning = aggregator?.exitCode === -1;
|
|
386
|
-
const aggregatorFailed = aggregator
|
|
387
|
-
? aggregator.exitCode !== -1 && isResultError(aggregator)
|
|
388
|
-
: false;
|
|
389
|
-
const isRunning = running > 0 || aggregatorRunning;
|
|
390
|
-
const icon = isRunning
|
|
391
|
-
? theme.fg("warning", "⏳")
|
|
392
|
-
: failCount > 0 || aggregatorFailed
|
|
393
|
-
? theme.fg("warning", "◐")
|
|
540
|
+
if (aggregator) {
|
|
541
|
+
const rIcon = aggregatorFailed
|
|
542
|
+
? theme.fg("error", "✗")
|
|
543
|
+
: aggregatorRunning
|
|
544
|
+
? theme.fg("warning", "⏳")
|
|
394
545
|
: theme.fg("success", "✓");
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
546
|
+
const displayItems = getDisplayItems(aggregator.messages);
|
|
547
|
+
const finalOutput = getResultFinalOutput(aggregator);
|
|
548
|
+
|
|
549
|
+
container.addChild(new Spacer(1));
|
|
550
|
+
container.addChild(
|
|
551
|
+
new Text(
|
|
552
|
+
`${theme.fg("muted", "─── fan-in → ") + theme.fg("accent", aggregator.agent)} ${rIcon}`,
|
|
553
|
+
0,
|
|
554
|
+
0,
|
|
555
|
+
),
|
|
556
|
+
);
|
|
557
|
+
container.addChild(
|
|
558
|
+
new Text(theme.fg("muted", "Task: ") + theme.fg("dim", aggregator.task), 0, 0),
|
|
559
|
+
);
|
|
560
|
+
if (aggregatorFailed && aggregator.errorMessage)
|
|
405
561
|
container.addChild(
|
|
406
|
-
new Text(
|
|
407
|
-
`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
|
|
408
|
-
0,
|
|
409
|
-
0,
|
|
410
|
-
),
|
|
562
|
+
new Text(theme.fg("error", `Error: ${aggregator.errorMessage}`), 0, 0),
|
|
411
563
|
);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const rIcon = !isResultError(r) ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
415
|
-
const displayItems = getDisplayItems(r.messages);
|
|
416
|
-
const finalOutput = getResultFinalOutput(r);
|
|
417
|
-
|
|
418
|
-
container.addChild(new Spacer(1));
|
|
419
|
-
container.addChild(
|
|
420
|
-
new Text(`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0),
|
|
421
|
-
);
|
|
422
|
-
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
423
|
-
|
|
424
|
-
// Show tool calls
|
|
425
|
-
for (const item of displayItems) {
|
|
426
|
-
if (item.type === "toolCall") {
|
|
427
|
-
container.addChild(
|
|
428
|
-
new Text(
|
|
429
|
-
theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
430
|
-
0,
|
|
431
|
-
0,
|
|
432
|
-
),
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Show final output as markdown
|
|
438
|
-
if (finalOutput) {
|
|
439
|
-
container.addChild(new Spacer(1));
|
|
440
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const taskUsage = formatUsageStats(r.usage, r.model, r.thinkingLevel);
|
|
444
|
-
if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
if (aggregator) {
|
|
448
|
-
const rIcon = !isResultError(aggregator)
|
|
449
|
-
? theme.fg("success", "✓")
|
|
450
|
-
: theme.fg("error", "✗");
|
|
451
|
-
const displayItems = getDisplayItems(aggregator.messages);
|
|
452
|
-
const finalOutput = getResultFinalOutput(aggregator);
|
|
453
|
-
|
|
454
|
-
container.addChild(new Spacer(1));
|
|
564
|
+
for (const item of displayItems) {
|
|
565
|
+
if (item.type === "toolCall") {
|
|
455
566
|
container.addChild(
|
|
456
567
|
new Text(
|
|
457
|
-
|
|
568
|
+
theme.fg("muted", "→ ") +
|
|
569
|
+
formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
458
570
|
0,
|
|
459
571
|
0,
|
|
460
572
|
),
|
|
461
573
|
);
|
|
462
|
-
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", aggregator.task), 0, 0));
|
|
463
|
-
for (const item of displayItems) {
|
|
464
|
-
if (item.type === "toolCall") {
|
|
465
|
-
container.addChild(
|
|
466
|
-
new Text(
|
|
467
|
-
theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
468
|
-
0,
|
|
469
|
-
0,
|
|
470
|
-
),
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
if (finalOutput) {
|
|
475
|
-
container.addChild(new Spacer(1));
|
|
476
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
477
|
-
}
|
|
478
|
-
const fanInUsage = formatUsageStats(aggregator.usage, aggregator.model, aggregator.thinkingLevel);
|
|
479
|
-
if (fanInUsage) container.addChild(new Text(theme.fg("dim", fanInUsage), 0, 0));
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const usageResults = aggregator ? [...details.results, aggregator] : details.results;
|
|
483
|
-
const usageStr = formatUsageStats(aggregateUsage(usageResults));
|
|
484
|
-
if (usageStr) {
|
|
485
|
-
container.addChild(new Spacer(1));
|
|
486
|
-
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
|
|
487
574
|
}
|
|
488
|
-
return container;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Collapsed view (or still running)
|
|
492
|
-
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
|
|
493
|
-
for (const r of details.results) {
|
|
494
|
-
const rIcon =
|
|
495
|
-
r.exitCode === -1
|
|
496
|
-
? theme.fg("warning", "⏳")
|
|
497
|
-
: !isResultError(r)
|
|
498
|
-
? theme.fg("success", "✓")
|
|
499
|
-
: theme.fg("error", "✗");
|
|
500
|
-
const displayItems = getDisplayItems(r.messages);
|
|
501
|
-
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
502
|
-
if (displayItems.length === 0)
|
|
503
|
-
text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
|
|
504
|
-
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
|
505
575
|
}
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
? theme.fg("warning", "⏳")
|
|
510
|
-
: !isResultError(aggregator)
|
|
511
|
-
? theme.fg("success", "✓")
|
|
512
|
-
: theme.fg("error", "✗");
|
|
513
|
-
const displayItems = getDisplayItems(aggregator.messages);
|
|
514
|
-
text += `\n\n${theme.fg("muted", "─── fan-in → ")}${theme.fg("accent", aggregator.agent)} ${rIcon}`;
|
|
515
|
-
if (displayItems.length === 0)
|
|
516
|
-
text += `\n${theme.fg("muted", aggregator.exitCode === -1 ? "(running...)" : "(no output)")}`;
|
|
517
|
-
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
|
518
|
-
}
|
|
519
|
-
if (!isRunning) {
|
|
520
|
-
const usageResults = aggregator ? [...details.results, aggregator] : details.results;
|
|
521
|
-
const usageStr = formatUsageStats(aggregateUsage(usageResults));
|
|
522
|
-
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
576
|
+
if (finalOutput) {
|
|
577
|
+
container.addChild(new Spacer(1));
|
|
578
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
523
579
|
}
|
|
524
|
-
|
|
525
|
-
|
|
580
|
+
const fanInUsage = formatResultUsageStats(aggregator);
|
|
581
|
+
if (fanInUsage) container.addChild(new Text(theme.fg("dim", fanInUsage), 0, 0));
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const usageResults = aggregator ? [...details.results, aggregator] : details.results;
|
|
585
|
+
const usageStr = formatUsageStats(aggregateUsage(usageResults));
|
|
586
|
+
if (usageStr) {
|
|
587
|
+
container.addChild(new Spacer(1));
|
|
588
|
+
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
|
|
526
589
|
}
|
|
590
|
+
return container;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Collapsed view (or still running)
|
|
594
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
|
|
595
|
+
for (const r of details.results) {
|
|
596
|
+
const rFailed = isResultError(r);
|
|
597
|
+
const rRunning = resultIsRunning(r);
|
|
598
|
+
const rIcon = rFailed
|
|
599
|
+
? theme.fg("error", "✗")
|
|
600
|
+
: rRunning
|
|
601
|
+
? theme.fg("warning", "⏳")
|
|
602
|
+
: theme.fg("success", "✓");
|
|
603
|
+
const collapsed = getCollapsedDisplayItems(r);
|
|
604
|
+
const finalOutput = getResultFinalOutput(r).trim();
|
|
605
|
+
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
606
|
+
if (rFailed && r.errorMessage)
|
|
607
|
+
text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
608
|
+
if (collapsed.items.length > 0)
|
|
609
|
+
text += `\n${renderDisplayItems(collapsed.items, 5, collapsed.total)}`;
|
|
610
|
+
else if (rRunning) text += `\n${theme.fg("muted", "(running...)")}`;
|
|
611
|
+
else if (finalOutput)
|
|
612
|
+
text += `\n${theme.fg("toolOutput", finalOutput.split("\n").slice(0, 3).join("\n"))}`;
|
|
613
|
+
else if (!rFailed || !r.errorMessage) text += `\n${theme.fg("muted", "(no output)")}`;
|
|
614
|
+
}
|
|
615
|
+
if (aggregator) {
|
|
616
|
+
const rIcon = aggregatorFailed
|
|
617
|
+
? theme.fg("error", "✗")
|
|
618
|
+
: aggregatorRunning
|
|
619
|
+
? theme.fg("warning", "⏳")
|
|
620
|
+
: theme.fg("success", "✓");
|
|
621
|
+
const collapsed = getCollapsedDisplayItems(aggregator);
|
|
622
|
+
const finalOutput = getResultFinalOutput(aggregator).trim();
|
|
623
|
+
text += `\n\n${theme.fg("muted", "─── fan-in → ")}${theme.fg("accent", aggregator.agent)} ${rIcon}`;
|
|
624
|
+
if (aggregatorFailed && aggregator.errorMessage)
|
|
625
|
+
text += `\n${theme.fg("error", `Error: ${aggregator.errorMessage}`)}`;
|
|
626
|
+
if (collapsed.items.length > 0)
|
|
627
|
+
text += `\n${renderDisplayItems(collapsed.items, 5, collapsed.total)}`;
|
|
628
|
+
else if (aggregatorRunning) text += `\n${theme.fg("muted", "(running...)")}`;
|
|
629
|
+
else if (finalOutput)
|
|
630
|
+
text += `\n${theme.fg("toolOutput", finalOutput.split("\n").slice(0, 3).join("\n"))}`;
|
|
631
|
+
else if (!aggregatorFailed || !aggregator.errorMessage)
|
|
632
|
+
text += `\n${theme.fg("muted", "(no output)")}`;
|
|
633
|
+
}
|
|
634
|
+
if (!isRunning) {
|
|
635
|
+
const usageResults = aggregator ? [...details.results, aggregator] : details.results;
|
|
636
|
+
const usageStr = formatUsageStats(aggregateUsage(usageResults));
|
|
637
|
+
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
638
|
+
}
|
|
639
|
+
if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
640
|
+
return new Text(text, 0, 0);
|
|
641
|
+
}
|
|
527
642
|
|
|
528
|
-
|
|
529
|
-
|
|
643
|
+
const text = result.content[0];
|
|
644
|
+
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
530
645
|
}
|
package/src/runner.ts
CHANGED
|
@@ -5,12 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
6
6
|
import type { Message } from "@earendil-works/pi-ai";
|
|
7
7
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import type {
|
|
9
|
-
AgentConfig,
|
|
10
|
-
AgentScope,
|
|
11
|
-
AgentSource,
|
|
12
|
-
SubagentThinkingLevel,
|
|
13
|
-
} from "./agents.js";
|
|
8
|
+
import type { AgentConfig, AgentScope, AgentSource, SubagentThinkingLevel } from "./agents.js";
|
|
14
9
|
import {
|
|
15
10
|
appendBounded,
|
|
16
11
|
DEFAULT_MAX_CONTEXT_BYTES,
|
|
@@ -32,6 +27,13 @@ export interface UsageStats {
|
|
|
32
27
|
contextTokens: number;
|
|
33
28
|
turns: number;
|
|
34
29
|
}
|
|
30
|
+
export type RecentActivityItem =
|
|
31
|
+
| { type: "text"; text: string }
|
|
32
|
+
| { type: "toolCall"; name: string; args: Record<string, unknown> };
|
|
33
|
+
|
|
34
|
+
const MAX_RECENT_ACTIVITY_ITEMS = 10;
|
|
35
|
+
const MAX_RECENT_ACTIVITY_BYTES = 8 * 1024;
|
|
36
|
+
const MAX_RECENT_ACTIVITY_ARGUMENT_BYTES = 1024;
|
|
35
37
|
|
|
36
38
|
export interface SingleResult {
|
|
37
39
|
agent: string;
|
|
@@ -42,6 +44,10 @@ export interface SingleResult {
|
|
|
42
44
|
stderr: string;
|
|
43
45
|
usage: UsageStats;
|
|
44
46
|
model?: string;
|
|
47
|
+
actualProvider?: string;
|
|
48
|
+
actualModel?: string;
|
|
49
|
+
recentActivity?: RecentActivityItem[];
|
|
50
|
+
recentActivityTotal?: number;
|
|
45
51
|
thinkingLevel?: SubagentThinkingLevel;
|
|
46
52
|
stopReason?: string;
|
|
47
53
|
errorMessage?: string;
|
|
@@ -89,6 +95,8 @@ export function getResultFinalOutput(result: SingleResult): string {
|
|
|
89
95
|
export function isResultError(result: SingleResult): boolean {
|
|
90
96
|
return (
|
|
91
97
|
(result.exitCode !== 0 && result.exitCode !== -1) ||
|
|
98
|
+
result.timedOut === true ||
|
|
99
|
+
result.stopReason === "timeout" ||
|
|
92
100
|
result.stopReason === "error" ||
|
|
93
101
|
result.stopReason === "aborted"
|
|
94
102
|
);
|
|
@@ -102,27 +110,124 @@ export function formatResultFailure(result: SingleResult): string {
|
|
|
102
110
|
return truncateUtf8(combined, DEFAULT_MAX_CONTEXT_BYTES).text;
|
|
103
111
|
}
|
|
104
112
|
|
|
105
|
-
function boundMessageText(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
113
|
+
function boundMessageText(
|
|
114
|
+
message: Message,
|
|
115
|
+
maxBytes: number,
|
|
116
|
+
): { message?: Message; bytes: number; truncated: boolean } {
|
|
117
|
+
const originalBytes = Buffer.byteLength(JSON.stringify(message), "utf8");
|
|
118
|
+
if (Number.isSafeInteger(maxBytes) && maxBytes >= 0 && originalBytes <= maxBytes) {
|
|
119
|
+
return { message, bytes: originalBytes, truncated: false };
|
|
120
|
+
}
|
|
121
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) return { bytes: 0, truncated: true };
|
|
122
|
+
|
|
123
|
+
const content: Array<
|
|
124
|
+
| { type: "text"; text: string }
|
|
125
|
+
| { type: "toolCall"; id: string; name: string; arguments: Record<string, unknown> }
|
|
126
|
+
> = [];
|
|
127
|
+
const bounded = () => ({ ...message, content }) as Message;
|
|
128
|
+
const fits = () => Buffer.byteLength(JSON.stringify(bounded()), "utf8") <= maxBytes;
|
|
129
|
+
const addText = (text: string, prepend = false) => {
|
|
130
|
+
if (!text.trim()) return;
|
|
131
|
+
const part = { type: "text" as const, text: "" };
|
|
132
|
+
if (prepend) content.unshift(part);
|
|
133
|
+
else content.push(part);
|
|
134
|
+
if (!fits()) {
|
|
135
|
+
content.splice(content.indexOf(part), 1);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
let low = 0;
|
|
139
|
+
let high = Buffer.byteLength(text, "utf8");
|
|
140
|
+
while (low < high) {
|
|
141
|
+
const middle = Math.ceil((low + high) / 2);
|
|
142
|
+
part.text = truncateUtf8(text, middle).text;
|
|
143
|
+
if (fits()) low = middle;
|
|
144
|
+
else high = middle - 1;
|
|
145
|
+
}
|
|
146
|
+
part.text = truncateUtf8(text, low).text;
|
|
147
|
+
if (!part.text.trim()) content.splice(content.indexOf(part), 1);
|
|
148
|
+
};
|
|
149
|
+
const addToolCall = (part: Extract<Message["content"][number], { type: "toolCall" }>) => {
|
|
150
|
+
const toolCall = {
|
|
151
|
+
type: "toolCall" as const,
|
|
152
|
+
id: part.id,
|
|
153
|
+
name: part.name,
|
|
154
|
+
arguments: part.arguments,
|
|
155
|
+
};
|
|
156
|
+
content.unshift(toolCall);
|
|
157
|
+
if (fits()) return;
|
|
158
|
+
const arguments_: Record<string, unknown> = {};
|
|
159
|
+
for (const key of ["command", "path", "file_path", "pattern", "url"]) {
|
|
160
|
+
const value = part.arguments[key];
|
|
161
|
+
if (typeof value === "string") arguments_[key] = truncateUtf8(value, 256).text;
|
|
162
|
+
}
|
|
163
|
+
toolCall.arguments = arguments_;
|
|
164
|
+
if (fits()) return;
|
|
165
|
+
toolCall.arguments = {};
|
|
166
|
+
if (!fits()) content.shift();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (message.role === "assistant") {
|
|
170
|
+
for (let index = message.content.length - 1; index >= 0; index--) {
|
|
171
|
+
const part = message.content[index];
|
|
172
|
+
if (part.type === "text") addText(part.text, true);
|
|
173
|
+
else if (part.type === "toolCall") addToolCall(part);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
for (const part of message.content) {
|
|
177
|
+
if (typeof part === "object" && part && part.type === "text") addText(part.text);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (content.length === 0) return { bytes: 0, truncated: true };
|
|
182
|
+
const result = bounded();
|
|
183
|
+
const bytes = Buffer.byteLength(JSON.stringify(result), "utf8");
|
|
184
|
+
return { message: result, bytes, truncated: true };
|
|
185
|
+
}
|
|
186
|
+
function compactRecentActivityArguments(args: Record<string, unknown>): Record<string, unknown> {
|
|
187
|
+
if (Buffer.byteLength(JSON.stringify(args), "utf8") <= MAX_RECENT_ACTIVITY_ARGUMENT_BYTES)
|
|
188
|
+
return args;
|
|
189
|
+
const compact: Record<string, unknown> = {};
|
|
190
|
+
for (const key of ["command", "path", "file_path", "pattern", "url", "selector"]) {
|
|
191
|
+
const value = args[key];
|
|
192
|
+
if (typeof value === "string") compact[key] = truncateUtf8(value, 256).text;
|
|
193
|
+
else if (typeof value === "number" || typeof value === "boolean") compact[key] = value;
|
|
194
|
+
}
|
|
195
|
+
return compact;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function appendRecentActivity(result: SingleResult, message: Message): void {
|
|
199
|
+
if (message.role !== "assistant") return;
|
|
200
|
+
const append = (item: RecentActivityItem) => {
|
|
201
|
+
result.recentActivity ??= [];
|
|
202
|
+
result.recentActivityTotal = (result.recentActivityTotal ?? 0) + 1;
|
|
203
|
+
result.recentActivity.push(item);
|
|
204
|
+
if (result.recentActivity.length > MAX_RECENT_ACTIVITY_ITEMS) {
|
|
205
|
+
result.recentActivity.splice(0, result.recentActivity.length - MAX_RECENT_ACTIVITY_ITEMS);
|
|
206
|
+
}
|
|
207
|
+
while (
|
|
208
|
+
Buffer.byteLength(JSON.stringify(result.recentActivity), "utf8") > MAX_RECENT_ACTIVITY_BYTES
|
|
209
|
+
) {
|
|
210
|
+
result.recentActivity.shift();
|
|
211
|
+
}
|
|
122
212
|
};
|
|
213
|
+
for (const part of message.content) {
|
|
214
|
+
if (part.type === "text") {
|
|
215
|
+
const text = part.text.trim();
|
|
216
|
+
if (text) append({ type: "text", text: truncateUtf8(text, 1024).text });
|
|
217
|
+
} else if (part.type === "toolCall") {
|
|
218
|
+
append({
|
|
219
|
+
type: "toolCall",
|
|
220
|
+
name: part.name,
|
|
221
|
+
args: compactRecentActivityArguments(part.arguments),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
123
225
|
}
|
|
124
226
|
|
|
125
|
-
export function buildFanInContext(
|
|
227
|
+
export function buildFanInContext(
|
|
228
|
+
results: SingleResult[],
|
|
229
|
+
maxBytes = DEFAULT_MAX_CONTEXT_BYTES,
|
|
230
|
+
): string {
|
|
126
231
|
const text = results
|
|
127
232
|
.map((result, index) => {
|
|
128
233
|
const failed = isResultError(result);
|
|
@@ -170,7 +275,10 @@ export async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
|
170
275
|
return results;
|
|
171
276
|
}
|
|
172
277
|
|
|
173
|
-
async function writePromptToTempFile(
|
|
278
|
+
async function writePromptToTempFile(
|
|
279
|
+
agentName: string,
|
|
280
|
+
prompt: string,
|
|
281
|
+
): Promise<{ dir: string; filePath: string }> {
|
|
174
282
|
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-"));
|
|
175
283
|
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
176
284
|
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
|
@@ -231,8 +339,15 @@ function signalProcess(proc: ReturnType<typeof spawn>, signal: NodeJS.Signals):
|
|
|
231
339
|
}
|
|
232
340
|
}
|
|
233
341
|
|
|
234
|
-
export function terminateProcess(
|
|
235
|
-
|
|
342
|
+
export function terminateProcess(
|
|
343
|
+
proc: ReturnType<typeof spawn>,
|
|
344
|
+
graceMs = KILL_GRACE_MS,
|
|
345
|
+
): () => void {
|
|
346
|
+
const leaderExited = proc.exitCode !== null || proc.signalCode !== null;
|
|
347
|
+
const capturedOutputClosed = [proc.stdout, proc.stderr].every(
|
|
348
|
+
(stream) => !stream || stream.readableEnded || stream.destroyed,
|
|
349
|
+
);
|
|
350
|
+
let closed = leaderExited && capturedOutputClosed;
|
|
236
351
|
const onClose = () => {
|
|
237
352
|
closed = true;
|
|
238
353
|
};
|
|
@@ -275,7 +390,15 @@ export async function runSingleAgent(
|
|
|
275
390
|
exitCode: 1,
|
|
276
391
|
messages: [],
|
|
277
392
|
stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
|
|
278
|
-
usage: {
|
|
393
|
+
usage: {
|
|
394
|
+
input: 0,
|
|
395
|
+
output: 0,
|
|
396
|
+
cacheRead: 0,
|
|
397
|
+
cacheWrite: 0,
|
|
398
|
+
cost: 0,
|
|
399
|
+
contextTokens: 0,
|
|
400
|
+
turns: 0,
|
|
401
|
+
},
|
|
279
402
|
thinkingLevel,
|
|
280
403
|
step,
|
|
281
404
|
finalOutput: "",
|
|
@@ -295,7 +418,15 @@ export async function runSingleAgent(
|
|
|
295
418
|
exitCode: 0,
|
|
296
419
|
messages: [],
|
|
297
420
|
stderr: "",
|
|
298
|
-
usage: {
|
|
421
|
+
usage: {
|
|
422
|
+
input: 0,
|
|
423
|
+
output: 0,
|
|
424
|
+
cacheRead: 0,
|
|
425
|
+
cacheWrite: 0,
|
|
426
|
+
cost: 0,
|
|
427
|
+
contextTokens: 0,
|
|
428
|
+
turns: 0,
|
|
429
|
+
},
|
|
299
430
|
model: agent.model ?? undefined,
|
|
300
431
|
thinkingLevel,
|
|
301
432
|
step,
|
|
@@ -394,21 +525,37 @@ export async function runSingleAgent(
|
|
|
394
525
|
},
|
|
395
526
|
});
|
|
396
527
|
} catch (error) {
|
|
397
|
-
currentResult.stderr = setErrorMessage(
|
|
528
|
+
currentResult.stderr = setErrorMessage(
|
|
529
|
+
error instanceof Error ? error.message : String(error),
|
|
530
|
+
);
|
|
398
531
|
finish(1);
|
|
399
532
|
return;
|
|
400
533
|
}
|
|
401
534
|
|
|
402
|
-
let capturedMessageBytes = 0;
|
|
403
535
|
const addMessage = (msg: Message) => {
|
|
404
|
-
|
|
536
|
+
const boundedMessage = boundMessageText(msg, DEFAULT_MAX_OUTPUT_BYTES - 2);
|
|
537
|
+
currentResult.truncated ||= boundedMessage.truncated;
|
|
538
|
+
if (!boundedMessage.message) return;
|
|
539
|
+
while (
|
|
540
|
+
currentResult.messages.length >= DEFAULT_MAX_MESSAGES ||
|
|
541
|
+
Buffer.byteLength(
|
|
542
|
+
JSON.stringify([...currentResult.messages, boundedMessage.message]),
|
|
543
|
+
"utf8",
|
|
544
|
+
) > DEFAULT_MAX_OUTPUT_BYTES
|
|
545
|
+
) {
|
|
546
|
+
const removed = currentResult.messages.shift();
|
|
547
|
+
if (!removed) break;
|
|
548
|
+
currentResult.truncated = true;
|
|
549
|
+
}
|
|
550
|
+
if (
|
|
551
|
+
Buffer.byteLength(
|
|
552
|
+
JSON.stringify([...currentResult.messages, boundedMessage.message]),
|
|
553
|
+
"utf8",
|
|
554
|
+
) > DEFAULT_MAX_OUTPUT_BYTES
|
|
555
|
+
) {
|
|
405
556
|
currentResult.truncated = true;
|
|
406
557
|
return;
|
|
407
558
|
}
|
|
408
|
-
const remaining = Math.max(0, DEFAULT_MAX_OUTPUT_BYTES - capturedMessageBytes);
|
|
409
|
-
const boundedMessage = boundMessageText(msg, remaining);
|
|
410
|
-
capturedMessageBytes += boundedMessage.bytes;
|
|
411
|
-
currentResult.truncated ||= boundedMessage.truncated;
|
|
412
559
|
currentResult.messages.push(boundedMessage.message);
|
|
413
560
|
};
|
|
414
561
|
const processEvent = (raw: unknown) => {
|
|
@@ -424,6 +571,7 @@ export async function runSingleAgent(
|
|
|
424
571
|
terminalAssistantOutput = output.text;
|
|
425
572
|
}
|
|
426
573
|
}
|
|
574
|
+
appendRecentActivity(currentResult, msg);
|
|
427
575
|
addMessage(msg);
|
|
428
576
|
if (msg.role === "assistant") {
|
|
429
577
|
currentResult.usage.turns++;
|
|
@@ -436,7 +584,9 @@ export async function runSingleAgent(
|
|
|
436
584
|
currentResult.usage.cost += usage.cost?.total || 0;
|
|
437
585
|
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
438
586
|
}
|
|
439
|
-
if (
|
|
587
|
+
if (msg.provider) currentResult.actualProvider = msg.provider;
|
|
588
|
+
if (msg.responseModel ?? msg.model)
|
|
589
|
+
currentResult.actualModel = msg.responseModel ?? msg.model;
|
|
440
590
|
if (msg.stopReason) currentResult.stopReason = msg.stopReason;
|
|
441
591
|
if (msg.errorMessage) setErrorMessage(msg.errorMessage);
|
|
442
592
|
}
|
|
@@ -475,7 +625,11 @@ export async function runSingleAgent(
|
|
|
475
625
|
|
|
476
626
|
proc.stdout?.on("data", (data) => decoder.push(data));
|
|
477
627
|
proc.stderr?.on("data", (data) => {
|
|
478
|
-
const bounded = appendBounded(
|
|
628
|
+
const bounded = appendBounded(
|
|
629
|
+
currentResult.stderr,
|
|
630
|
+
data.toString(),
|
|
631
|
+
DEFAULT_MAX_STDERR_BYTES,
|
|
632
|
+
);
|
|
479
633
|
currentResult.stderr = bounded.text;
|
|
480
634
|
currentResult.truncated ||= bounded.truncated;
|
|
481
635
|
});
|