pi-subagents 0.3.0 → 0.3.2
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/CHANGELOG.md +35 -0
- package/agents.ts +0 -10
- package/async-execution.ts +261 -0
- package/chain-execution.ts +444 -0
- package/execution.ts +383 -0
- package/formatters.ts +111 -0
- package/index.ts +92 -1615
- package/package.json +2 -2
- package/render.ts +308 -0
- package/schemas.ts +90 -0
- package/settings.ts +2 -166
- package/types.ts +166 -0
- package/utils.ts +325 -0
package/execution.ts
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core execution logic for running subagents
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
8
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
9
|
+
import type { AgentConfig } from "./agents.js";
|
|
10
|
+
import {
|
|
11
|
+
appendJsonl,
|
|
12
|
+
ensureArtifactsDir,
|
|
13
|
+
getArtifactPaths,
|
|
14
|
+
writeArtifact,
|
|
15
|
+
writeMetadata,
|
|
16
|
+
} from "./artifacts.js";
|
|
17
|
+
import {
|
|
18
|
+
type AgentProgress,
|
|
19
|
+
type ArtifactPaths,
|
|
20
|
+
type Details,
|
|
21
|
+
type MaxOutputConfig,
|
|
22
|
+
type RunSyncOptions,
|
|
23
|
+
type SingleResult,
|
|
24
|
+
DEFAULT_MAX_OUTPUT,
|
|
25
|
+
truncateOutput,
|
|
26
|
+
} from "./types.js";
|
|
27
|
+
import {
|
|
28
|
+
writePrompt,
|
|
29
|
+
getFinalOutput,
|
|
30
|
+
findLatestSessionFile,
|
|
31
|
+
detectSubagentError,
|
|
32
|
+
extractToolArgsPreview,
|
|
33
|
+
extractTextFromContent,
|
|
34
|
+
} from "./utils.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run a subagent synchronously (blocking until complete)
|
|
38
|
+
*/
|
|
39
|
+
export async function runSync(
|
|
40
|
+
runtimeCwd: string,
|
|
41
|
+
agents: AgentConfig[],
|
|
42
|
+
agentName: string,
|
|
43
|
+
task: string,
|
|
44
|
+
options: RunSyncOptions,
|
|
45
|
+
): Promise<SingleResult> {
|
|
46
|
+
const { cwd, signal, onUpdate, maxOutput, artifactsDir, artifactConfig, runId, index } = options;
|
|
47
|
+
const agent = agents.find((a) => a.name === agentName);
|
|
48
|
+
if (!agent) {
|
|
49
|
+
return {
|
|
50
|
+
agent: agentName,
|
|
51
|
+
task,
|
|
52
|
+
exitCode: 1,
|
|
53
|
+
messages: [],
|
|
54
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
55
|
+
error: `Unknown agent: ${agentName}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const args = ["--mode", "json", "-p"];
|
|
60
|
+
const shareEnabled = options.share === true;
|
|
61
|
+
const sessionEnabled = Boolean(options.sessionDir) || shareEnabled;
|
|
62
|
+
if (!sessionEnabled) {
|
|
63
|
+
args.push("--no-session");
|
|
64
|
+
}
|
|
65
|
+
if (options.sessionDir) {
|
|
66
|
+
try {
|
|
67
|
+
fs.mkdirSync(options.sessionDir, { recursive: true });
|
|
68
|
+
} catch {}
|
|
69
|
+
args.push("--session-dir", options.sessionDir);
|
|
70
|
+
}
|
|
71
|
+
if (agent.model) args.push("--model", agent.model);
|
|
72
|
+
if (agent.tools?.length) {
|
|
73
|
+
const builtinTools: string[] = [];
|
|
74
|
+
const extensionPaths: string[] = [];
|
|
75
|
+
for (const tool of agent.tools) {
|
|
76
|
+
if (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")) {
|
|
77
|
+
extensionPaths.push(tool);
|
|
78
|
+
} else {
|
|
79
|
+
builtinTools.push(tool);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (builtinTools.length > 0) {
|
|
83
|
+
args.push("--tools", builtinTools.join(","));
|
|
84
|
+
}
|
|
85
|
+
for (const extPath of extensionPaths) {
|
|
86
|
+
args.push("--extension", extPath);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let tmpDir: string | null = null;
|
|
91
|
+
if (agent.systemPrompt?.trim()) {
|
|
92
|
+
const tmp = writePrompt(agent.name, agent.systemPrompt);
|
|
93
|
+
tmpDir = tmp.dir;
|
|
94
|
+
args.push("--append-system-prompt", tmp.path);
|
|
95
|
+
}
|
|
96
|
+
args.push(`Task: ${task}`);
|
|
97
|
+
|
|
98
|
+
const result: SingleResult = {
|
|
99
|
+
agent: agentName,
|
|
100
|
+
task,
|
|
101
|
+
exitCode: 0,
|
|
102
|
+
messages: [],
|
|
103
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const progress: AgentProgress = {
|
|
107
|
+
index: index ?? 0,
|
|
108
|
+
agent: agentName,
|
|
109
|
+
status: "running",
|
|
110
|
+
task,
|
|
111
|
+
recentTools: [],
|
|
112
|
+
recentOutput: [],
|
|
113
|
+
toolCount: 0,
|
|
114
|
+
tokens: 0,
|
|
115
|
+
durationMs: 0,
|
|
116
|
+
};
|
|
117
|
+
result.progress = progress;
|
|
118
|
+
|
|
119
|
+
const startTime = Date.now();
|
|
120
|
+
const jsonlLines: string[] = [];
|
|
121
|
+
|
|
122
|
+
let artifactPathsResult: ArtifactPaths | undefined;
|
|
123
|
+
if (artifactsDir && artifactConfig?.enabled !== false) {
|
|
124
|
+
artifactPathsResult = getArtifactPaths(artifactsDir, runId, agentName, index);
|
|
125
|
+
ensureArtifactsDir(artifactsDir);
|
|
126
|
+
if (artifactConfig?.includeInput !== false) {
|
|
127
|
+
writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${task}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
132
|
+
const proc = spawn("pi", args, { cwd: cwd ?? runtimeCwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
133
|
+
let buf = "";
|
|
134
|
+
|
|
135
|
+
// Throttled update mechanism - consolidates all updates
|
|
136
|
+
let lastUpdateTime = 0;
|
|
137
|
+
let updatePending = false;
|
|
138
|
+
let pendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
139
|
+
let processClosed = false;
|
|
140
|
+
const UPDATE_THROTTLE_MS = 50; // Reduced from 75ms for faster responsiveness
|
|
141
|
+
|
|
142
|
+
const scheduleUpdate = () => {
|
|
143
|
+
if (!onUpdate || processClosed) return;
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
const elapsed = now - lastUpdateTime;
|
|
146
|
+
|
|
147
|
+
if (elapsed >= UPDATE_THROTTLE_MS) {
|
|
148
|
+
// Enough time passed, update immediately
|
|
149
|
+
// Clear any pending timer to avoid double-updates
|
|
150
|
+
if (pendingTimer) {
|
|
151
|
+
clearTimeout(pendingTimer);
|
|
152
|
+
pendingTimer = null;
|
|
153
|
+
}
|
|
154
|
+
lastUpdateTime = now;
|
|
155
|
+
updatePending = false;
|
|
156
|
+
progress.durationMs = now - startTime;
|
|
157
|
+
onUpdate({
|
|
158
|
+
content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
|
|
159
|
+
details: { mode: "single", results: [result], progress: [progress] },
|
|
160
|
+
});
|
|
161
|
+
} else if (!updatePending) {
|
|
162
|
+
// Schedule update for later
|
|
163
|
+
updatePending = true;
|
|
164
|
+
pendingTimer = setTimeout(() => {
|
|
165
|
+
pendingTimer = null;
|
|
166
|
+
if (updatePending && !processClosed) {
|
|
167
|
+
updatePending = false;
|
|
168
|
+
lastUpdateTime = Date.now();
|
|
169
|
+
progress.durationMs = Date.now() - startTime;
|
|
170
|
+
onUpdate({
|
|
171
|
+
content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
|
|
172
|
+
details: { mode: "single", results: [result], progress: [progress] },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}, UPDATE_THROTTLE_MS - elapsed);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const processLine = (line: string) => {
|
|
180
|
+
if (!line.trim()) return;
|
|
181
|
+
jsonlLines.push(line);
|
|
182
|
+
try {
|
|
183
|
+
const evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown };
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
progress.durationMs = now - startTime;
|
|
186
|
+
|
|
187
|
+
if (evt.type === "tool_execution_start") {
|
|
188
|
+
progress.toolCount++;
|
|
189
|
+
progress.currentTool = evt.toolName;
|
|
190
|
+
progress.currentToolArgs = extractToolArgsPreview((evt.args || {}) as Record<string, unknown>);
|
|
191
|
+
// Tool start is important - update immediately by forcing throttle reset
|
|
192
|
+
lastUpdateTime = 0;
|
|
193
|
+
scheduleUpdate();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (evt.type === "tool_execution_end") {
|
|
197
|
+
if (progress.currentTool) {
|
|
198
|
+
progress.recentTools.unshift({
|
|
199
|
+
tool: progress.currentTool,
|
|
200
|
+
args: progress.currentToolArgs || "",
|
|
201
|
+
endMs: now,
|
|
202
|
+
});
|
|
203
|
+
if (progress.recentTools.length > 5) {
|
|
204
|
+
progress.recentTools.pop();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
progress.currentTool = undefined;
|
|
208
|
+
progress.currentToolArgs = undefined;
|
|
209
|
+
scheduleUpdate();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (evt.type === "message_end" && evt.message) {
|
|
213
|
+
result.messages.push(evt.message);
|
|
214
|
+
if (evt.message.role === "assistant") {
|
|
215
|
+
result.usage.turns++;
|
|
216
|
+
const u = evt.message.usage;
|
|
217
|
+
if (u) {
|
|
218
|
+
result.usage.input += u.input || 0;
|
|
219
|
+
result.usage.output += u.output || 0;
|
|
220
|
+
result.usage.cacheRead += u.cacheRead || 0;
|
|
221
|
+
result.usage.cacheWrite += u.cacheWrite || 0;
|
|
222
|
+
result.usage.cost += u.cost?.total || 0;
|
|
223
|
+
progress.tokens = result.usage.input + result.usage.output;
|
|
224
|
+
}
|
|
225
|
+
if (!result.model && evt.message.model) result.model = evt.message.model;
|
|
226
|
+
if (evt.message.errorMessage) result.error = evt.message.errorMessage;
|
|
227
|
+
|
|
228
|
+
const text = extractTextFromContent(evt.message.content);
|
|
229
|
+
if (text) {
|
|
230
|
+
const lines = text
|
|
231
|
+
.split("\n")
|
|
232
|
+
.filter((l) => l.trim())
|
|
233
|
+
.slice(-10);
|
|
234
|
+
// Append to existing recentOutput (keep last 50 total) - mutate in place for efficiency
|
|
235
|
+
progress.recentOutput.push(...lines);
|
|
236
|
+
if (progress.recentOutput.length > 50) {
|
|
237
|
+
progress.recentOutput.splice(0, progress.recentOutput.length - 50);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
scheduleUpdate();
|
|
242
|
+
}
|
|
243
|
+
if (evt.type === "tool_result_end" && evt.message) {
|
|
244
|
+
result.messages.push(evt.message);
|
|
245
|
+
// Also capture tool result text in recentOutput for streaming display
|
|
246
|
+
const toolText = extractTextFromContent(evt.message.content);
|
|
247
|
+
if (toolText) {
|
|
248
|
+
const toolLines = toolText
|
|
249
|
+
.split("\n")
|
|
250
|
+
.filter((l) => l.trim())
|
|
251
|
+
.slice(-10);
|
|
252
|
+
// Append to existing recentOutput (keep last 50 total) - mutate in place for efficiency
|
|
253
|
+
progress.recentOutput.push(...toolLines);
|
|
254
|
+
if (progress.recentOutput.length > 50) {
|
|
255
|
+
progress.recentOutput.splice(0, progress.recentOutput.length - 50);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
scheduleUpdate();
|
|
259
|
+
}
|
|
260
|
+
} catch {}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
let stderrBuf = "";
|
|
264
|
+
|
|
265
|
+
proc.stdout.on("data", (d) => {
|
|
266
|
+
buf += d.toString();
|
|
267
|
+
const lines = buf.split("\n");
|
|
268
|
+
buf = lines.pop() || "";
|
|
269
|
+
lines.forEach(processLine);
|
|
270
|
+
|
|
271
|
+
// Also schedule an update on data received (handles streaming output)
|
|
272
|
+
scheduleUpdate();
|
|
273
|
+
});
|
|
274
|
+
proc.stderr.on("data", (d) => {
|
|
275
|
+
stderrBuf += d.toString();
|
|
276
|
+
});
|
|
277
|
+
proc.on("close", (code) => {
|
|
278
|
+
processClosed = true;
|
|
279
|
+
if (pendingTimer) {
|
|
280
|
+
clearTimeout(pendingTimer);
|
|
281
|
+
pendingTimer = null;
|
|
282
|
+
}
|
|
283
|
+
if (buf.trim()) processLine(buf);
|
|
284
|
+
if (code !== 0 && stderrBuf.trim() && !result.error) {
|
|
285
|
+
result.error = stderrBuf.trim();
|
|
286
|
+
}
|
|
287
|
+
resolve(code ?? 0);
|
|
288
|
+
});
|
|
289
|
+
proc.on("error", () => resolve(1));
|
|
290
|
+
|
|
291
|
+
if (signal) {
|
|
292
|
+
const kill = () => {
|
|
293
|
+
proc.kill("SIGTERM");
|
|
294
|
+
setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
|
|
295
|
+
};
|
|
296
|
+
if (signal.aborted) kill();
|
|
297
|
+
else signal.addEventListener("abort", kill, { once: true });
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
302
|
+
result.exitCode = exitCode;
|
|
303
|
+
|
|
304
|
+
if (exitCode === 0 && !result.error) {
|
|
305
|
+
const errInfo = detectSubagentError(result.messages);
|
|
306
|
+
if (errInfo.hasError) {
|
|
307
|
+
result.exitCode = errInfo.exitCode ?? 1;
|
|
308
|
+
result.error = errInfo.details
|
|
309
|
+
? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
|
|
310
|
+
: `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
progress.status = result.exitCode === 0 ? "completed" : "failed";
|
|
315
|
+
progress.durationMs = Date.now() - startTime;
|
|
316
|
+
if (result.error) {
|
|
317
|
+
progress.error = result.error;
|
|
318
|
+
if (progress.currentTool) {
|
|
319
|
+
progress.failedTool = progress.currentTool;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
result.progress = progress;
|
|
324
|
+
result.progressSummary = {
|
|
325
|
+
toolCount: progress.toolCount,
|
|
326
|
+
tokens: progress.tokens,
|
|
327
|
+
durationMs: progress.durationMs,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (artifactPathsResult && artifactConfig?.enabled !== false) {
|
|
331
|
+
result.artifactPaths = artifactPathsResult;
|
|
332
|
+
const fullOutput = getFinalOutput(result.messages);
|
|
333
|
+
|
|
334
|
+
if (artifactConfig?.includeOutput !== false) {
|
|
335
|
+
writeArtifact(artifactPathsResult.outputPath, fullOutput);
|
|
336
|
+
}
|
|
337
|
+
if (artifactConfig?.includeJsonl !== false) {
|
|
338
|
+
for (const line of jsonlLines) {
|
|
339
|
+
appendJsonl(artifactPathsResult.jsonlPath, line);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (artifactConfig?.includeMetadata !== false) {
|
|
343
|
+
writeMetadata(artifactPathsResult.metadataPath, {
|
|
344
|
+
runId,
|
|
345
|
+
agent: agentName,
|
|
346
|
+
task,
|
|
347
|
+
exitCode: result.exitCode,
|
|
348
|
+
usage: result.usage,
|
|
349
|
+
model: result.model,
|
|
350
|
+
durationMs: progress.durationMs,
|
|
351
|
+
toolCount: progress.toolCount,
|
|
352
|
+
error: result.error,
|
|
353
|
+
timestamp: Date.now(),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (maxOutput) {
|
|
358
|
+
const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
|
|
359
|
+
const truncationResult = truncateOutput(fullOutput, config, artifactPathsResult.outputPath);
|
|
360
|
+
if (truncationResult.truncated) {
|
|
361
|
+
result.truncation = truncationResult;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} else if (maxOutput) {
|
|
365
|
+
const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
|
|
366
|
+
const fullOutput = getFinalOutput(result.messages);
|
|
367
|
+
const truncationResult = truncateOutput(fullOutput, config);
|
|
368
|
+
if (truncationResult.truncated) {
|
|
369
|
+
result.truncation = truncationResult;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (shareEnabled && options.sessionDir) {
|
|
374
|
+
const sessionFile = findLatestSessionFile(options.sessionDir);
|
|
375
|
+
if (sessionFile) {
|
|
376
|
+
result.sessionFile = sessionFile;
|
|
377
|
+
// HTML export disabled - module resolution issues with global pi installation
|
|
378
|
+
// Users can still access the session file directly
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return result;
|
|
383
|
+
}
|
package/formatters.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting utilities for display output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import type { Usage, SingleResult } from "./types.js";
|
|
8
|
+
import type { ChainStep, SequentialStep } from "./settings.js";
|
|
9
|
+
import { isParallelStep } from "./settings.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format token count with k suffix for large numbers
|
|
13
|
+
*/
|
|
14
|
+
export function formatTokens(n: number): string {
|
|
15
|
+
return n < 1000 ? String(n) : n < 10000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n / 1000)}k`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format usage statistics into a compact string
|
|
20
|
+
*/
|
|
21
|
+
export function formatUsage(u: Usage, model?: string): string {
|
|
22
|
+
const parts: string[] = [];
|
|
23
|
+
if (u.turns) parts.push(`${u.turns} turn${u.turns > 1 ? "s" : ""}`);
|
|
24
|
+
if (u.input) parts.push(`in:${formatTokens(u.input)}`);
|
|
25
|
+
if (u.output) parts.push(`out:${formatTokens(u.output)}`);
|
|
26
|
+
if (u.cacheRead) parts.push(`R${formatTokens(u.cacheRead)}`);
|
|
27
|
+
if (u.cacheWrite) parts.push(`W${formatTokens(u.cacheWrite)}`);
|
|
28
|
+
if (u.cost) parts.push(`$${u.cost.toFixed(4)}`);
|
|
29
|
+
if (model) parts.push(model);
|
|
30
|
+
return parts.join(" ");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format duration in human-readable form
|
|
35
|
+
*/
|
|
36
|
+
export function formatDuration(ms: number): string {
|
|
37
|
+
if (ms < 1000) return `${ms}ms`;
|
|
38
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
39
|
+
return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build a summary string for a completed/failed chain
|
|
44
|
+
*/
|
|
45
|
+
export function buildChainSummary(
|
|
46
|
+
steps: ChainStep[],
|
|
47
|
+
results: SingleResult[],
|
|
48
|
+
chainDir: string,
|
|
49
|
+
status: "completed" | "failed",
|
|
50
|
+
failedStep?: { index: number; error: string },
|
|
51
|
+
): string {
|
|
52
|
+
// Build step names for display
|
|
53
|
+
const stepNames = steps
|
|
54
|
+
.map((s) => (isParallelStep(s) ? `parallel[${s.parallel.length}]` : (s as SequentialStep).agent))
|
|
55
|
+
.join(" → ");
|
|
56
|
+
|
|
57
|
+
// Calculate total duration from results
|
|
58
|
+
const totalDuration = results.reduce((sum, r) => sum + (r.progress?.durationMs || 0), 0);
|
|
59
|
+
const durationStr = formatDuration(totalDuration);
|
|
60
|
+
|
|
61
|
+
// Check for progress.md
|
|
62
|
+
const progressPath = path.join(chainDir, "progress.md");
|
|
63
|
+
const hasProgress = fs.existsSync(progressPath);
|
|
64
|
+
|
|
65
|
+
if (status === "completed") {
|
|
66
|
+
const stepWord = results.length === 1 ? "step" : "steps";
|
|
67
|
+
return `✅ Chain completed: ${stepNames} (${results.length} ${stepWord}, ${durationStr})
|
|
68
|
+
|
|
69
|
+
📋 Progress: ${hasProgress ? progressPath : "(none)"}
|
|
70
|
+
📁 Artifacts: ${chainDir}`;
|
|
71
|
+
} else {
|
|
72
|
+
const stepInfo = failedStep ? ` at step ${failedStep.index + 1}` : "";
|
|
73
|
+
const errorInfo = failedStep?.error ? `: ${failedStep.error}` : "";
|
|
74
|
+
return `❌ Chain failed${stepInfo}${errorInfo}
|
|
75
|
+
|
|
76
|
+
📋 Progress: ${hasProgress ? progressPath : "(none)"}
|
|
77
|
+
📁 Artifacts: ${chainDir}`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Format a tool call for display
|
|
83
|
+
*/
|
|
84
|
+
export function formatToolCall(name: string, args: Record<string, unknown>): string {
|
|
85
|
+
switch (name) {
|
|
86
|
+
case "bash":
|
|
87
|
+
return `$ ${((args.command as string) || "").slice(0, 60)}${(args.command as string)?.length > 60 ? "..." : ""}`;
|
|
88
|
+
case "read":
|
|
89
|
+
return `read ${shortenPath((args.path || args.file_path || "") as string)}`;
|
|
90
|
+
case "write":
|
|
91
|
+
return `write ${shortenPath((args.path || args.file_path || "") as string)}`;
|
|
92
|
+
case "edit":
|
|
93
|
+
return `edit ${shortenPath((args.path || args.file_path || "") as string)}`;
|
|
94
|
+
default: {
|
|
95
|
+
const s = JSON.stringify(args);
|
|
96
|
+
return `${name} ${s.slice(0, 40)}${s.length > 40 ? "..." : ""}`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Shorten a path by replacing home directory with ~
|
|
103
|
+
*/
|
|
104
|
+
export function shortenPath(p: string): string {
|
|
105
|
+
const home = process.env.HOME;
|
|
106
|
+
// Only shorten if HOME is defined and non-empty, and path starts with it
|
|
107
|
+
if (home && p.startsWith(home)) {
|
|
108
|
+
return `~${p.slice(home.length)}`;
|
|
109
|
+
}
|
|
110
|
+
return p;
|
|
111
|
+
}
|