pi-subagents 0.3.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/CHANGELOG.md +94 -0
- package/README.md +300 -0
- package/agents.ts +172 -0
- package/artifacts.ts +70 -0
- package/chain-clarify.ts +612 -0
- package/index.ts +2186 -0
- package/install.mjs +93 -0
- package/notify.ts +87 -0
- package/package.json +38 -0
- package/settings.ts +492 -0
- package/subagent-runner.ts +608 -0
- package/types.ts +114 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { appendJsonl, getArtifactPaths } from "./artifacts.js";
|
|
8
|
+
import {
|
|
9
|
+
type ArtifactConfig,
|
|
10
|
+
type ArtifactPaths,
|
|
11
|
+
DEFAULT_MAX_OUTPUT,
|
|
12
|
+
type MaxOutputConfig,
|
|
13
|
+
truncateOutput,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
interface SubagentStep {
|
|
17
|
+
agent: string;
|
|
18
|
+
task: string;
|
|
19
|
+
cwd?: string;
|
|
20
|
+
model?: string;
|
|
21
|
+
tools?: string[];
|
|
22
|
+
systemPrompt?: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SubagentRunConfig {
|
|
26
|
+
id: string;
|
|
27
|
+
steps: SubagentStep[];
|
|
28
|
+
resultPath: string;
|
|
29
|
+
cwd: string;
|
|
30
|
+
placeholder: string;
|
|
31
|
+
taskIndex?: number;
|
|
32
|
+
totalTasks?: number;
|
|
33
|
+
maxOutput?: MaxOutputConfig;
|
|
34
|
+
artifactsDir?: string;
|
|
35
|
+
artifactConfig?: Partial<ArtifactConfig>;
|
|
36
|
+
share?: boolean;
|
|
37
|
+
sessionDir?: string;
|
|
38
|
+
asyncDir: string;
|
|
39
|
+
sessionId?: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface StepResult {
|
|
43
|
+
agent: string;
|
|
44
|
+
output: string;
|
|
45
|
+
success: boolean;
|
|
46
|
+
artifactPaths?: ArtifactPaths;
|
|
47
|
+
truncated?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const require = createRequire(import.meta.url);
|
|
51
|
+
|
|
52
|
+
function findLatestSessionFile(sessionDir: string): string | null {
|
|
53
|
+
try {
|
|
54
|
+
const files = fs
|
|
55
|
+
.readdirSync(sessionDir)
|
|
56
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
57
|
+
.map((f) => path.join(sessionDir, f));
|
|
58
|
+
if (files.length === 0) return null;
|
|
59
|
+
files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
60
|
+
return files[0] ?? null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface TokenUsage {
|
|
67
|
+
input: number;
|
|
68
|
+
output: number;
|
|
69
|
+
total: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseSessionTokens(sessionDir: string): TokenUsage | null {
|
|
73
|
+
const sessionFile = findLatestSessionFile(sessionDir);
|
|
74
|
+
if (!sessionFile) return null;
|
|
75
|
+
try {
|
|
76
|
+
const content = fs.readFileSync(sessionFile, "utf-8");
|
|
77
|
+
let input = 0;
|
|
78
|
+
let output = 0;
|
|
79
|
+
for (const line of content.split("\n")) {
|
|
80
|
+
if (!line.trim()) continue;
|
|
81
|
+
try {
|
|
82
|
+
const entry = JSON.parse(line);
|
|
83
|
+
if (entry.usage) {
|
|
84
|
+
input += entry.usage.inputTokens ?? entry.usage.input ?? 0;
|
|
85
|
+
output += entry.usage.outputTokens ?? entry.usage.output ?? 0;
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
return { input, output, total: input + output };
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function runPiStreaming(
|
|
96
|
+
args: string[],
|
|
97
|
+
cwd: string,
|
|
98
|
+
outputFile: string,
|
|
99
|
+
): Promise<{ stdout: string; exitCode: number | null }> {
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
102
|
+
const child = spawn("pi", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
103
|
+
let stdout = "";
|
|
104
|
+
|
|
105
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
106
|
+
const text = chunk.toString();
|
|
107
|
+
stdout += text;
|
|
108
|
+
outputStream.write(text);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
112
|
+
outputStream.write(chunk.toString());
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
child.on("close", (exitCode) => {
|
|
116
|
+
outputStream.end();
|
|
117
|
+
resolve({ stdout, exitCode });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
child.on("error", () => {
|
|
121
|
+
outputStream.end();
|
|
122
|
+
resolve({ stdout, exitCode: 1 });
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function exportSessionHtml(sessionFile: string, outputDir: string): Promise<string> {
|
|
128
|
+
const pkgRoot = path.dirname(require.resolve("@mariozechner/pi-coding-agent/package.json"));
|
|
129
|
+
const exportModulePath = path.join(pkgRoot, "dist", "core", "export-html", "index.js");
|
|
130
|
+
const moduleUrl = pathToFileURL(exportModulePath).href;
|
|
131
|
+
const mod = await import(moduleUrl);
|
|
132
|
+
const exportFromFile = (mod as { exportFromFile?: (inputPath: string, options?: { outputPath?: string }) => string })
|
|
133
|
+
.exportFromFile;
|
|
134
|
+
if (typeof exportFromFile !== "function") {
|
|
135
|
+
throw new Error("exportFromFile not available");
|
|
136
|
+
}
|
|
137
|
+
const outputPath = path.join(outputDir, `${path.basename(sessionFile, ".jsonl")}.html`);
|
|
138
|
+
return exportFromFile(sessionFile, { outputPath });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createShareLink(htmlPath: string): { shareUrl: string; gistUrl: string } | { error: string } {
|
|
142
|
+
try {
|
|
143
|
+
const auth = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" });
|
|
144
|
+
if (auth.status !== 0) {
|
|
145
|
+
return { error: "GitHub CLI is not logged in. Run 'gh auth login' first." };
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
return { error: "GitHub CLI (gh) is not installed." };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const result = spawnSync("gh", ["gist", "create", htmlPath], { encoding: "utf-8" });
|
|
153
|
+
if (result.status !== 0) {
|
|
154
|
+
const err = (result.stderr || "").trim() || "Failed to create gist.";
|
|
155
|
+
return { error: err };
|
|
156
|
+
}
|
|
157
|
+
const gistUrl = (result.stdout || "").trim();
|
|
158
|
+
const gistId = gistUrl.split("/").pop();
|
|
159
|
+
if (!gistId) return { error: "Failed to parse gist ID." };
|
|
160
|
+
const shareUrl = `https://shittycodingagent.ai/session/?${gistId}`;
|
|
161
|
+
return { shareUrl, gistUrl };
|
|
162
|
+
} catch (err) {
|
|
163
|
+
return { error: String(err) };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function writeJson(filePath: string, payload: object): void {
|
|
168
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
169
|
+
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function formatDuration(ms: number): string {
|
|
173
|
+
if (ms < 1000) return `${ms}ms`;
|
|
174
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
175
|
+
const minutes = Math.floor(ms / 60000);
|
|
176
|
+
const seconds = Math.floor((ms % 60000) / 1000);
|
|
177
|
+
return `${minutes}m${seconds}s`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function writeRunLog(
|
|
181
|
+
logPath: string,
|
|
182
|
+
input: {
|
|
183
|
+
id: string;
|
|
184
|
+
mode: "single" | "chain";
|
|
185
|
+
cwd: string;
|
|
186
|
+
startedAt: number;
|
|
187
|
+
endedAt: number;
|
|
188
|
+
steps: Array<{
|
|
189
|
+
agent: string;
|
|
190
|
+
status: string;
|
|
191
|
+
durationMs?: number;
|
|
192
|
+
}>;
|
|
193
|
+
summary: string;
|
|
194
|
+
truncated: boolean;
|
|
195
|
+
artifactsDir?: string;
|
|
196
|
+
sessionFile?: string;
|
|
197
|
+
shareUrl?: string;
|
|
198
|
+
shareError?: string;
|
|
199
|
+
},
|
|
200
|
+
): void {
|
|
201
|
+
const lines: string[] = [];
|
|
202
|
+
lines.push(`# Subagent run ${input.id}`);
|
|
203
|
+
lines.push("");
|
|
204
|
+
lines.push(`- **Mode:** ${input.mode}`);
|
|
205
|
+
lines.push(`- **CWD:** ${input.cwd}`);
|
|
206
|
+
lines.push(`- **Started:** ${new Date(input.startedAt).toISOString()}`);
|
|
207
|
+
lines.push(`- **Ended:** ${new Date(input.endedAt).toISOString()}`);
|
|
208
|
+
lines.push(`- **Duration:** ${formatDuration(input.endedAt - input.startedAt)}`);
|
|
209
|
+
if (input.sessionFile) lines.push(`- **Session:** ${input.sessionFile}`);
|
|
210
|
+
if (input.shareUrl) lines.push(`- **Share:** ${input.shareUrl}`);
|
|
211
|
+
if (input.shareError) lines.push(`- **Share error:** ${input.shareError}`);
|
|
212
|
+
if (input.artifactsDir) lines.push(`- **Artifacts:** ${input.artifactsDir}`);
|
|
213
|
+
lines.push("");
|
|
214
|
+
lines.push("## Steps");
|
|
215
|
+
lines.push("| Step | Agent | Status | Duration |");
|
|
216
|
+
lines.push("| --- | --- | --- | --- |");
|
|
217
|
+
input.steps.forEach((step, i) => {
|
|
218
|
+
const duration = step.durationMs !== undefined ? formatDuration(step.durationMs) : "-";
|
|
219
|
+
lines.push(`| ${i + 1} | ${step.agent} | ${step.status} | ${duration} |`);
|
|
220
|
+
});
|
|
221
|
+
lines.push("");
|
|
222
|
+
lines.push("## Summary");
|
|
223
|
+
if (input.truncated) {
|
|
224
|
+
lines.push("_Output truncated_");
|
|
225
|
+
lines.push("");
|
|
226
|
+
}
|
|
227
|
+
lines.push(input.summary.trim() || "(no output)");
|
|
228
|
+
lines.push("");
|
|
229
|
+
fs.writeFileSync(logPath, lines.join("\n"), "utf-8");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
233
|
+
const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
|
|
234
|
+
config;
|
|
235
|
+
let previousOutput = "";
|
|
236
|
+
const results: StepResult[] = [];
|
|
237
|
+
const overallStartTime = Date.now();
|
|
238
|
+
const shareEnabled = config.share === true;
|
|
239
|
+
const sessionEnabled = Boolean(config.sessionDir) || shareEnabled;
|
|
240
|
+
const asyncDir = config.asyncDir;
|
|
241
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
242
|
+
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
243
|
+
const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
|
|
244
|
+
let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
|
|
245
|
+
|
|
246
|
+
const outputFile = path.join(asyncDir, "output.log");
|
|
247
|
+
const statusPayload: {
|
|
248
|
+
runId: string;
|
|
249
|
+
mode: "single" | "chain";
|
|
250
|
+
state: "queued" | "running" | "complete" | "failed";
|
|
251
|
+
startedAt: number;
|
|
252
|
+
endedAt?: number;
|
|
253
|
+
lastUpdate: number;
|
|
254
|
+
pid: number;
|
|
255
|
+
cwd: string;
|
|
256
|
+
currentStep: number;
|
|
257
|
+
steps: Array<{
|
|
258
|
+
agent: string;
|
|
259
|
+
status: "pending" | "running" | "complete" | "failed";
|
|
260
|
+
startedAt?: number;
|
|
261
|
+
endedAt?: number;
|
|
262
|
+
durationMs?: number;
|
|
263
|
+
exitCode?: number | null;
|
|
264
|
+
error?: string;
|
|
265
|
+
tokens?: TokenUsage;
|
|
266
|
+
}>;
|
|
267
|
+
artifactsDir?: string;
|
|
268
|
+
sessionDir?: string;
|
|
269
|
+
outputFile?: string;
|
|
270
|
+
totalTokens?: TokenUsage;
|
|
271
|
+
sessionFile?: string;
|
|
272
|
+
shareUrl?: string;
|
|
273
|
+
gistUrl?: string;
|
|
274
|
+
shareError?: string;
|
|
275
|
+
error?: string;
|
|
276
|
+
} = {
|
|
277
|
+
runId: id,
|
|
278
|
+
mode: steps.length > 1 ? "chain" : "single",
|
|
279
|
+
state: "running",
|
|
280
|
+
startedAt: overallStartTime,
|
|
281
|
+
lastUpdate: overallStartTime,
|
|
282
|
+
pid: process.pid,
|
|
283
|
+
cwd,
|
|
284
|
+
currentStep: 0,
|
|
285
|
+
steps: steps.map((step) => ({ agent: step.agent, status: "pending" })),
|
|
286
|
+
artifactsDir,
|
|
287
|
+
sessionDir: config.sessionDir,
|
|
288
|
+
outputFile,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
fs.mkdirSync(asyncDir, { recursive: true });
|
|
292
|
+
writeJson(statusPath, statusPayload);
|
|
293
|
+
appendJsonl(
|
|
294
|
+
eventsPath,
|
|
295
|
+
JSON.stringify({
|
|
296
|
+
type: "subagent.run.started",
|
|
297
|
+
ts: overallStartTime,
|
|
298
|
+
runId: id,
|
|
299
|
+
mode: statusPayload.mode,
|
|
300
|
+
cwd,
|
|
301
|
+
pid: process.pid,
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
|
|
306
|
+
const step = steps[stepIndex];
|
|
307
|
+
const stepStartTime = Date.now();
|
|
308
|
+
statusPayload.currentStep = stepIndex;
|
|
309
|
+
statusPayload.steps[stepIndex].status = "running";
|
|
310
|
+
statusPayload.steps[stepIndex].startedAt = stepStartTime;
|
|
311
|
+
statusPayload.lastUpdate = stepStartTime;
|
|
312
|
+
writeJson(statusPath, statusPayload);
|
|
313
|
+
appendJsonl(
|
|
314
|
+
eventsPath,
|
|
315
|
+
JSON.stringify({
|
|
316
|
+
type: "subagent.step.started",
|
|
317
|
+
ts: stepStartTime,
|
|
318
|
+
runId: id,
|
|
319
|
+
stepIndex,
|
|
320
|
+
agent: step.agent,
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
const args = ["-p"];
|
|
324
|
+
if (!sessionEnabled) {
|
|
325
|
+
args.push("--no-session");
|
|
326
|
+
}
|
|
327
|
+
if (config.sessionDir) {
|
|
328
|
+
try {
|
|
329
|
+
fs.mkdirSync(config.sessionDir, { recursive: true });
|
|
330
|
+
} catch {}
|
|
331
|
+
args.push("--session-dir", config.sessionDir);
|
|
332
|
+
}
|
|
333
|
+
if (step.model) args.push("--model", step.model);
|
|
334
|
+
if (step.tools?.length) {
|
|
335
|
+
const builtinTools: string[] = [];
|
|
336
|
+
const extensionPaths: string[] = [];
|
|
337
|
+
for (const tool of step.tools) {
|
|
338
|
+
if (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")) {
|
|
339
|
+
extensionPaths.push(tool);
|
|
340
|
+
} else {
|
|
341
|
+
builtinTools.push(tool);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (builtinTools.length > 0) args.push("--tools", builtinTools.join(","));
|
|
345
|
+
for (const extPath of extensionPaths) args.push("--extension", extPath);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
let tmpDir: string | null = null;
|
|
349
|
+
if (step.systemPrompt) {
|
|
350
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
351
|
+
const promptPath = path.join(tmpDir, "prompt.md");
|
|
352
|
+
fs.writeFileSync(promptPath, step.systemPrompt);
|
|
353
|
+
args.push("--append-system-prompt", promptPath);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const placeholderRegex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
|
|
357
|
+
const task = step.task.replace(placeholderRegex, () => previousOutput);
|
|
358
|
+
args.push(`Task: ${task}`);
|
|
359
|
+
|
|
360
|
+
let artifactPaths: ArtifactPaths | undefined;
|
|
361
|
+
if (artifactsDir && artifactConfig?.enabled !== false) {
|
|
362
|
+
const index = taskIndex !== undefined ? taskIndex : steps.length > 1 ? stepIndex : undefined;
|
|
363
|
+
artifactPaths = getArtifactPaths(artifactsDir, id, step.agent, index);
|
|
364
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
365
|
+
|
|
366
|
+
if (artifactConfig?.includeInput !== false) {
|
|
367
|
+
fs.writeFileSync(artifactPaths.inputPath, `# Task for ${step.agent}\n\n${task}`, "utf-8");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const result = await runPiStreaming(args, step.cwd ?? cwd, outputFile);
|
|
372
|
+
|
|
373
|
+
if (tmpDir) {
|
|
374
|
+
try {
|
|
375
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
376
|
+
} catch {}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const output = (result.stdout || "").trim();
|
|
380
|
+
previousOutput = output;
|
|
381
|
+
|
|
382
|
+
const cumulativeTokens = config.sessionDir ? parseSessionTokens(config.sessionDir) : null;
|
|
383
|
+
const stepTokens: TokenUsage | null = cumulativeTokens
|
|
384
|
+
? {
|
|
385
|
+
input: cumulativeTokens.input - previousCumulativeTokens.input,
|
|
386
|
+
output: cumulativeTokens.output - previousCumulativeTokens.output,
|
|
387
|
+
total: cumulativeTokens.total - previousCumulativeTokens.total,
|
|
388
|
+
}
|
|
389
|
+
: null;
|
|
390
|
+
if (cumulativeTokens) {
|
|
391
|
+
previousCumulativeTokens = cumulativeTokens;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const stepResult: StepResult = {
|
|
395
|
+
agent: step.agent,
|
|
396
|
+
output,
|
|
397
|
+
success: result.exitCode === 0,
|
|
398
|
+
artifactPaths,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
if (artifactPaths && artifactConfig?.enabled !== false) {
|
|
402
|
+
if (artifactConfig?.includeOutput !== false) {
|
|
403
|
+
fs.writeFileSync(artifactPaths.outputPath, output, "utf-8");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (artifactConfig?.includeMetadata !== false) {
|
|
407
|
+
fs.writeFileSync(
|
|
408
|
+
artifactPaths.metadataPath,
|
|
409
|
+
JSON.stringify(
|
|
410
|
+
{
|
|
411
|
+
runId: id,
|
|
412
|
+
agent: step.agent,
|
|
413
|
+
task,
|
|
414
|
+
exitCode: result.exitCode,
|
|
415
|
+
durationMs: Date.now() - stepStartTime,
|
|
416
|
+
timestamp: Date.now(),
|
|
417
|
+
},
|
|
418
|
+
null,
|
|
419
|
+
2,
|
|
420
|
+
),
|
|
421
|
+
"utf-8",
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
results.push(stepResult);
|
|
427
|
+
const stepEndTime = Date.now();
|
|
428
|
+
statusPayload.steps[stepIndex].status = result.exitCode === 0 ? "complete" : "failed";
|
|
429
|
+
statusPayload.steps[stepIndex].endedAt = stepEndTime;
|
|
430
|
+
statusPayload.steps[stepIndex].durationMs = stepEndTime - stepStartTime;
|
|
431
|
+
statusPayload.steps[stepIndex].exitCode = result.exitCode;
|
|
432
|
+
if (stepTokens) {
|
|
433
|
+
statusPayload.steps[stepIndex].tokens = stepTokens;
|
|
434
|
+
statusPayload.totalTokens = { ...previousCumulativeTokens };
|
|
435
|
+
}
|
|
436
|
+
statusPayload.lastUpdate = stepEndTime;
|
|
437
|
+
writeJson(statusPath, statusPayload);
|
|
438
|
+
appendJsonl(
|
|
439
|
+
eventsPath,
|
|
440
|
+
JSON.stringify({
|
|
441
|
+
type: result.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
442
|
+
ts: stepEndTime,
|
|
443
|
+
runId: id,
|
|
444
|
+
stepIndex,
|
|
445
|
+
agent: step.agent,
|
|
446
|
+
exitCode: result.exitCode,
|
|
447
|
+
durationMs: stepEndTime - stepStartTime,
|
|
448
|
+
tokens: stepTokens,
|
|
449
|
+
}),
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
if (result.exitCode !== 0) break;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let summary = results.map((r) => `${r.agent}:\n${r.output}`).join("\n\n");
|
|
456
|
+
let truncated = false;
|
|
457
|
+
|
|
458
|
+
if (maxOutput) {
|
|
459
|
+
const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
|
|
460
|
+
const lastArtifactPath = results[results.length - 1]?.artifactPaths?.outputPath;
|
|
461
|
+
const truncResult = truncateOutput(summary, config, lastArtifactPath);
|
|
462
|
+
if (truncResult.truncated) {
|
|
463
|
+
summary = truncResult.text;
|
|
464
|
+
truncated = true;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const agentName = steps.length === 1 ? steps[0].agent : `chain:${steps.map((s) => s.agent).join("->")}`;
|
|
469
|
+
let sessionFile: string | undefined;
|
|
470
|
+
let shareUrl: string | undefined;
|
|
471
|
+
let gistUrl: string | undefined;
|
|
472
|
+
let shareError: string | undefined;
|
|
473
|
+
|
|
474
|
+
if (shareEnabled && config.sessionDir) {
|
|
475
|
+
sessionFile = findLatestSessionFile(config.sessionDir) ?? undefined;
|
|
476
|
+
if (sessionFile) {
|
|
477
|
+
try {
|
|
478
|
+
const htmlPath = await exportSessionHtml(sessionFile, config.sessionDir);
|
|
479
|
+
const share = createShareLink(htmlPath);
|
|
480
|
+
if ("error" in share) shareError = share.error;
|
|
481
|
+
else {
|
|
482
|
+
shareUrl = share.shareUrl;
|
|
483
|
+
gistUrl = share.gistUrl;
|
|
484
|
+
}
|
|
485
|
+
} catch (err) {
|
|
486
|
+
shareError = String(err);
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
shareError = "Session file not found.";
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const runEndedAt = Date.now();
|
|
494
|
+
statusPayload.state = results.every((r) => r.success) ? "complete" : "failed";
|
|
495
|
+
statusPayload.endedAt = runEndedAt;
|
|
496
|
+
statusPayload.lastUpdate = runEndedAt;
|
|
497
|
+
statusPayload.sessionFile = sessionFile;
|
|
498
|
+
statusPayload.shareUrl = shareUrl;
|
|
499
|
+
statusPayload.gistUrl = gistUrl;
|
|
500
|
+
statusPayload.shareError = shareError;
|
|
501
|
+
if (statusPayload.state === "failed") {
|
|
502
|
+
const failedStep = statusPayload.steps.find((s) => s.status === "failed");
|
|
503
|
+
if (failedStep?.agent) {
|
|
504
|
+
statusPayload.error = `Step failed: ${failedStep.agent}`;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
writeJson(statusPath, statusPayload);
|
|
508
|
+
appendJsonl(
|
|
509
|
+
eventsPath,
|
|
510
|
+
JSON.stringify({
|
|
511
|
+
type: "subagent.run.completed",
|
|
512
|
+
ts: runEndedAt,
|
|
513
|
+
runId: id,
|
|
514
|
+
status: statusPayload.state,
|
|
515
|
+
durationMs: runEndedAt - overallStartTime,
|
|
516
|
+
}),
|
|
517
|
+
);
|
|
518
|
+
writeRunLog(logPath, {
|
|
519
|
+
id,
|
|
520
|
+
mode: statusPayload.mode,
|
|
521
|
+
cwd,
|
|
522
|
+
startedAt: overallStartTime,
|
|
523
|
+
endedAt: runEndedAt,
|
|
524
|
+
steps: statusPayload.steps.map((step) => ({
|
|
525
|
+
agent: step.agent,
|
|
526
|
+
status: step.status,
|
|
527
|
+
durationMs: step.durationMs,
|
|
528
|
+
})),
|
|
529
|
+
summary,
|
|
530
|
+
truncated,
|
|
531
|
+
artifactsDir,
|
|
532
|
+
sessionFile,
|
|
533
|
+
shareUrl,
|
|
534
|
+
shareError,
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
fs.mkdirSync(path.dirname(resultPath), { recursive: true });
|
|
539
|
+
fs.writeFileSync(
|
|
540
|
+
resultPath,
|
|
541
|
+
JSON.stringify({
|
|
542
|
+
id,
|
|
543
|
+
agent: agentName,
|
|
544
|
+
success: results.every((r) => r.success),
|
|
545
|
+
summary,
|
|
546
|
+
results: results.map((r) => ({
|
|
547
|
+
agent: r.agent,
|
|
548
|
+
output: r.output,
|
|
549
|
+
success: r.success,
|
|
550
|
+
artifactPaths: r.artifactPaths,
|
|
551
|
+
truncated: r.truncated,
|
|
552
|
+
})),
|
|
553
|
+
exitCode: results.every((r) => r.success) ? 0 : 1,
|
|
554
|
+
timestamp: runEndedAt,
|
|
555
|
+
durationMs: runEndedAt - overallStartTime,
|
|
556
|
+
truncated,
|
|
557
|
+
artifactsDir,
|
|
558
|
+
cwd,
|
|
559
|
+
asyncDir,
|
|
560
|
+
sessionId: config.sessionId,
|
|
561
|
+
sessionFile,
|
|
562
|
+
shareUrl,
|
|
563
|
+
gistUrl,
|
|
564
|
+
shareError,
|
|
565
|
+
...(taskIndex !== undefined && { taskIndex }),
|
|
566
|
+
...(totalTasks !== undefined && { totalTasks }),
|
|
567
|
+
}),
|
|
568
|
+
);
|
|
569
|
+
} catch (err) {
|
|
570
|
+
console.error(`Failed to write result file ${resultPath}:`, err);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const configArg = process.argv[2];
|
|
575
|
+
if (configArg) {
|
|
576
|
+
try {
|
|
577
|
+
const configJson = fs.readFileSync(configArg, "utf-8");
|
|
578
|
+
const config = JSON.parse(configJson) as SubagentRunConfig;
|
|
579
|
+
try {
|
|
580
|
+
fs.unlinkSync(configArg);
|
|
581
|
+
} catch {}
|
|
582
|
+
runSubagent(config).catch((runErr) => {
|
|
583
|
+
console.error("Subagent runner error:", runErr);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
});
|
|
586
|
+
} catch (err) {
|
|
587
|
+
console.error("Subagent runner error:", err);
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
let input = "";
|
|
592
|
+
process.stdin.setEncoding("utf-8");
|
|
593
|
+
process.stdin.on("data", (chunk) => {
|
|
594
|
+
input += chunk;
|
|
595
|
+
});
|
|
596
|
+
process.stdin.on("end", () => {
|
|
597
|
+
try {
|
|
598
|
+
const config = JSON.parse(input) as SubagentRunConfig;
|
|
599
|
+
runSubagent(config).catch((runErr) => {
|
|
600
|
+
console.error("Subagent runner error:", runErr);
|
|
601
|
+
process.exit(1);
|
|
602
|
+
});
|
|
603
|
+
} catch (err) {
|
|
604
|
+
console.error("Subagent runner error:", err);
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export interface MaxOutputConfig {
|
|
2
|
+
bytes?: number;
|
|
3
|
+
lines?: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface TruncationResult {
|
|
7
|
+
text: string;
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
originalBytes?: number;
|
|
10
|
+
originalLines?: number;
|
|
11
|
+
artifactPath?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AgentProgress {
|
|
15
|
+
index: number;
|
|
16
|
+
agent: string;
|
|
17
|
+
status: "pending" | "running" | "completed" | "failed";
|
|
18
|
+
task: string;
|
|
19
|
+
currentTool?: string;
|
|
20
|
+
currentToolArgs?: string;
|
|
21
|
+
recentTools: Array<{ tool: string; args: string; endMs: number }>;
|
|
22
|
+
recentOutput: string[];
|
|
23
|
+
toolCount: number;
|
|
24
|
+
tokens: number;
|
|
25
|
+
durationMs: number;
|
|
26
|
+
error?: string;
|
|
27
|
+
failedTool?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ProgressSummary {
|
|
31
|
+
toolCount: number;
|
|
32
|
+
tokens: number;
|
|
33
|
+
durationMs: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ArtifactPaths {
|
|
37
|
+
inputPath: string;
|
|
38
|
+
outputPath: string;
|
|
39
|
+
jsonlPath: string;
|
|
40
|
+
metadataPath: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ArtifactConfig {
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
includeInput: boolean;
|
|
46
|
+
includeOutput: boolean;
|
|
47
|
+
includeJsonl: boolean;
|
|
48
|
+
includeMetadata: boolean;
|
|
49
|
+
cleanupDays: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const DEFAULT_MAX_OUTPUT: Required<MaxOutputConfig> = {
|
|
53
|
+
bytes: 200 * 1024,
|
|
54
|
+
lines: 5000,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
|
|
58
|
+
enabled: true,
|
|
59
|
+
includeInput: true,
|
|
60
|
+
includeOutput: true,
|
|
61
|
+
includeJsonl: true,
|
|
62
|
+
includeMetadata: true,
|
|
63
|
+
cleanupDays: 7,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export function formatBytes(bytes: number): string {
|
|
67
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
68
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
69
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function truncateOutput(
|
|
73
|
+
output: string,
|
|
74
|
+
config: Required<MaxOutputConfig>,
|
|
75
|
+
artifactPath?: string,
|
|
76
|
+
): TruncationResult {
|
|
77
|
+
const lines = output.split("\n");
|
|
78
|
+
const bytes = Buffer.byteLength(output, "utf-8");
|
|
79
|
+
|
|
80
|
+
if (bytes <= config.bytes && lines.length <= config.lines) {
|
|
81
|
+
return { text: output, truncated: false };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let truncatedLines = lines;
|
|
85
|
+
if (lines.length > config.lines) {
|
|
86
|
+
truncatedLines = lines.slice(0, config.lines);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let result = truncatedLines.join("\n");
|
|
90
|
+
if (Buffer.byteLength(result, "utf-8") > config.bytes) {
|
|
91
|
+
let low = 0;
|
|
92
|
+
let high = result.length;
|
|
93
|
+
while (low < high) {
|
|
94
|
+
const mid = Math.floor((low + high + 1) / 2);
|
|
95
|
+
if (Buffer.byteLength(result.slice(0, mid), "utf-8") <= config.bytes) {
|
|
96
|
+
low = mid;
|
|
97
|
+
} else {
|
|
98
|
+
high = mid - 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
result = result.slice(0, low);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const keptLines = result.split("\n").length;
|
|
105
|
+
const marker = `[TRUNCATED: showing first ${keptLines} of ${lines.length} lines, ${formatBytes(Buffer.byteLength(result))} of ${formatBytes(bytes)}${artifactPath ? ` - full output at ${artifactPath}` : ""}]\n`;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
text: marker + result,
|
|
109
|
+
truncated: true,
|
|
110
|
+
originalBytes: bytes,
|
|
111
|
+
originalLines: lines.length,
|
|
112
|
+
artifactPath,
|
|
113
|
+
};
|
|
114
|
+
}
|