pi-subagents 0.9.2 → 0.10.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 +24 -0
- package/README.md +41 -4
- package/async-execution.ts +64 -30
- package/chain-execution.ts +1 -1
- package/execution.ts +16 -1
- package/index.ts +90 -13
- package/package.json +11 -2
- package/parallel-utils.ts +93 -0
- package/render.ts +78 -8
- package/schemas.ts +1 -1
- package/settings.ts +16 -14
- package/skills.ts +25 -1
- package/subagent-runner.ts +360 -176
- package/utils.ts +23 -7
package/subagent-runner.ts
CHANGED
|
@@ -15,23 +15,19 @@ import {
|
|
|
15
15
|
truncateOutput,
|
|
16
16
|
getSubagentDepthEnv,
|
|
17
17
|
} from "./types.js";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
systemPrompt?: string | null;
|
|
28
|
-
skills?: string[];
|
|
29
|
-
outputPath?: string;
|
|
30
|
-
}
|
|
18
|
+
import {
|
|
19
|
+
type RunnerSubagentStep as SubagentStep,
|
|
20
|
+
type RunnerStep,
|
|
21
|
+
isParallelGroup,
|
|
22
|
+
flattenSteps,
|
|
23
|
+
mapConcurrent,
|
|
24
|
+
aggregateParallelOutputs,
|
|
25
|
+
MAX_PARALLEL_CONCURRENCY,
|
|
26
|
+
} from "./parallel-utils.js";
|
|
31
27
|
|
|
32
28
|
interface SubagentRunConfig {
|
|
33
29
|
id: string;
|
|
34
|
-
steps:
|
|
30
|
+
steps: RunnerStep[];
|
|
35
31
|
resultPath: string;
|
|
36
32
|
cwd: string;
|
|
37
33
|
placeholder: string;
|
|
@@ -51,6 +47,7 @@ interface StepResult {
|
|
|
51
47
|
agent: string;
|
|
52
48
|
output: string;
|
|
53
49
|
success: boolean;
|
|
50
|
+
skipped?: boolean;
|
|
54
51
|
artifactPaths?: ArtifactPaths;
|
|
55
52
|
truncated?: boolean;
|
|
56
53
|
}
|
|
@@ -105,11 +102,12 @@ function runPiStreaming(
|
|
|
105
102
|
cwd: string,
|
|
106
103
|
outputFile: string,
|
|
107
104
|
env?: Record<string, string | undefined>,
|
|
105
|
+
piPackageRoot?: string,
|
|
108
106
|
): Promise<{ stdout: string; exitCode: number | null }> {
|
|
109
107
|
return new Promise((resolve) => {
|
|
110
108
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
111
109
|
const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv() };
|
|
112
|
-
const spawnSpec = getPiSpawnCommand(args);
|
|
110
|
+
const spawnSpec = getPiSpawnCommand(args, piPackageRoot ? { piPackageRoot } : undefined);
|
|
113
111
|
const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
|
|
114
112
|
let stdout = "";
|
|
115
113
|
|
|
@@ -135,8 +133,24 @@ function runPiStreaming(
|
|
|
135
133
|
});
|
|
136
134
|
}
|
|
137
135
|
|
|
136
|
+
function resolvePiPackageRootFallback(): string {
|
|
137
|
+
// Try to resolve the main entry point and walk up to find the package root
|
|
138
|
+
const entryPoint = require.resolve("@mariozechner/pi-coding-agent");
|
|
139
|
+
// Entry point is typically /path/to/dist/index.js, so go up to find package root
|
|
140
|
+
let dir = path.dirname(entryPoint);
|
|
141
|
+
while (dir !== path.dirname(dir)) {
|
|
142
|
+
const pkgJsonPath = path.join(dir, "package.json");
|
|
143
|
+
try {
|
|
144
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
145
|
+
if (pkg.name === "@mariozechner/pi-coding-agent") return dir;
|
|
146
|
+
} catch {}
|
|
147
|
+
dir = path.dirname(dir);
|
|
148
|
+
}
|
|
149
|
+
throw new Error("Could not resolve @mariozechner/pi-coding-agent package root");
|
|
150
|
+
}
|
|
151
|
+
|
|
138
152
|
async function exportSessionHtml(sessionFile: string, outputDir: string, piPackageRoot?: string): Promise<string> {
|
|
139
|
-
const pkgRoot = piPackageRoot ??
|
|
153
|
+
const pkgRoot = piPackageRoot ?? resolvePiPackageRootFallback();
|
|
140
154
|
const exportModulePath = path.join(pkgRoot, "dist", "core", "export-html", "index.js");
|
|
141
155
|
const moduleUrl = pathToFileURL(exportModulePath).href;
|
|
142
156
|
const mod = await import(moduleUrl);
|
|
@@ -240,6 +254,138 @@ function writeRunLog(
|
|
|
240
254
|
fs.writeFileSync(logPath, lines.join("\n"), "utf-8");
|
|
241
255
|
}
|
|
242
256
|
|
|
257
|
+
/** Context for running a single step */
|
|
258
|
+
interface SingleStepContext {
|
|
259
|
+
previousOutput: string;
|
|
260
|
+
placeholder: string;
|
|
261
|
+
cwd: string;
|
|
262
|
+
sessionEnabled: boolean;
|
|
263
|
+
sessionDir?: string;
|
|
264
|
+
artifactsDir?: string;
|
|
265
|
+
artifactConfig?: Partial<ArtifactConfig>;
|
|
266
|
+
id: string;
|
|
267
|
+
flatIndex: number;
|
|
268
|
+
flatStepCount: number;
|
|
269
|
+
outputFile: string;
|
|
270
|
+
piPackageRoot?: string;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Run a single pi agent step, returning output and metadata */
|
|
274
|
+
async function runSingleStep(
|
|
275
|
+
step: SubagentStep,
|
|
276
|
+
ctx: SingleStepContext,
|
|
277
|
+
): Promise<{ agent: string; output: string; exitCode: number | null; artifactPaths?: ArtifactPaths }> {
|
|
278
|
+
const args = ["-p"];
|
|
279
|
+
if (!ctx.sessionEnabled) {
|
|
280
|
+
args.push("--no-session");
|
|
281
|
+
}
|
|
282
|
+
if (ctx.sessionDir) {
|
|
283
|
+
try { fs.mkdirSync(ctx.sessionDir, { recursive: true }); } catch {}
|
|
284
|
+
args.push("--session-dir", ctx.sessionDir);
|
|
285
|
+
}
|
|
286
|
+
if (step.model) args.push("--models", step.model);
|
|
287
|
+
|
|
288
|
+
const toolExtensionPaths: string[] = [];
|
|
289
|
+
if (step.tools?.length) {
|
|
290
|
+
const builtinTools: string[] = [];
|
|
291
|
+
for (const tool of step.tools) {
|
|
292
|
+
if (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")) {
|
|
293
|
+
toolExtensionPaths.push(tool);
|
|
294
|
+
} else {
|
|
295
|
+
builtinTools.push(tool);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (builtinTools.length > 0) args.push("--tools", builtinTools.join(","));
|
|
299
|
+
}
|
|
300
|
+
if (step.extensions !== undefined) {
|
|
301
|
+
args.push("--no-extensions");
|
|
302
|
+
for (const extPath of step.extensions) args.push("--extension", extPath);
|
|
303
|
+
} else {
|
|
304
|
+
for (const extPath of toolExtensionPaths) args.push("--extension", extPath);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let tmpDir: string | null = null;
|
|
308
|
+
if (step.systemPrompt) {
|
|
309
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
310
|
+
const promptPath = path.join(tmpDir, "prompt.md");
|
|
311
|
+
fs.writeFileSync(promptPath, step.systemPrompt);
|
|
312
|
+
args.push("--append-system-prompt", promptPath);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
|
|
316
|
+
const task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
|
|
317
|
+
|
|
318
|
+
const TASK_ARG_LIMIT = 8000;
|
|
319
|
+
if (task.length > TASK_ARG_LIMIT) {
|
|
320
|
+
if (!tmpDir) tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
321
|
+
const taskFilePath = path.join(tmpDir, "task.md");
|
|
322
|
+
fs.writeFileSync(taskFilePath, `Task: ${task}`, { mode: 0o600 });
|
|
323
|
+
args.push(`@${taskFilePath}`);
|
|
324
|
+
} else {
|
|
325
|
+
args.push(`Task: ${task}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let artifactPaths: ArtifactPaths | undefined;
|
|
329
|
+
if (ctx.artifactsDir && ctx.artifactConfig?.enabled !== false) {
|
|
330
|
+
const index = ctx.flatStepCount > 1 ? ctx.flatIndex : undefined;
|
|
331
|
+
artifactPaths = getArtifactPaths(ctx.artifactsDir, ctx.id, step.agent, index);
|
|
332
|
+
fs.mkdirSync(ctx.artifactsDir, { recursive: true });
|
|
333
|
+
if (ctx.artifactConfig?.includeInput !== false) {
|
|
334
|
+
fs.writeFileSync(artifactPaths.inputPath, `# Task for ${step.agent}\n\n${task}`, "utf-8");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const mcpEnv: Record<string, string | undefined> = {};
|
|
339
|
+
if (step.mcpDirectTools?.length) {
|
|
340
|
+
mcpEnv.MCP_DIRECT_TOOLS = step.mcpDirectTools.join(",");
|
|
341
|
+
} else {
|
|
342
|
+
mcpEnv.MCP_DIRECT_TOOLS = "__none__";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const result = await runPiStreaming(args, step.cwd ?? ctx.cwd, ctx.outputFile, mcpEnv, ctx.piPackageRoot);
|
|
346
|
+
|
|
347
|
+
if (tmpDir) {
|
|
348
|
+
try { fs.rmSync(tmpDir, { recursive: true }); } catch {}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const output = (result.stdout || "").trim();
|
|
352
|
+
let outputForSummary = output;
|
|
353
|
+
if (step.outputPath && result.exitCode === 0) {
|
|
354
|
+
const persisted = persistSingleOutput(step.outputPath, output);
|
|
355
|
+
if (persisted.savedPath) {
|
|
356
|
+
outputForSummary = output
|
|
357
|
+
? `${output}\n\n📄 Output saved to: ${persisted.savedPath}`
|
|
358
|
+
: `📄 Output saved to: ${persisted.savedPath}`;
|
|
359
|
+
} else if (persisted.error) {
|
|
360
|
+
outputForSummary = output
|
|
361
|
+
? `${output}\n\n⚠️ Failed to save output to: ${step.outputPath}\n${persisted.error}`
|
|
362
|
+
: `⚠️ Failed to save output to: ${step.outputPath}\n${persisted.error}`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
|
|
367
|
+
if (ctx.artifactConfig?.includeOutput !== false) {
|
|
368
|
+
fs.writeFileSync(artifactPaths.outputPath, output, "utf-8");
|
|
369
|
+
}
|
|
370
|
+
if (ctx.artifactConfig?.includeMetadata !== false) {
|
|
371
|
+
fs.writeFileSync(
|
|
372
|
+
artifactPaths.metadataPath,
|
|
373
|
+
JSON.stringify({
|
|
374
|
+
runId: ctx.id,
|
|
375
|
+
agent: step.agent,
|
|
376
|
+
task,
|
|
377
|
+
exitCode: result.exitCode,
|
|
378
|
+
skills: step.skills,
|
|
379
|
+
timestamp: Date.now(),
|
|
380
|
+
}, null, 2),
|
|
381
|
+
"utf-8",
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return { agent: step.agent, output: outputForSummary, exitCode: result.exitCode, artifactPaths };
|
|
387
|
+
}
|
|
388
|
+
|
|
243
389
|
async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
244
390
|
const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
|
|
245
391
|
config;
|
|
@@ -254,7 +400,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
254
400
|
const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
|
|
255
401
|
let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
|
|
256
402
|
|
|
257
|
-
|
|
403
|
+
// Flatten steps for status tracking (parallel groups expand to individual entries)
|
|
404
|
+
const flatSteps = flattenSteps(steps);
|
|
258
405
|
const statusPayload: {
|
|
259
406
|
runId: string;
|
|
260
407
|
mode: "single" | "chain";
|
|
@@ -287,17 +434,17 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
287
434
|
error?: string;
|
|
288
435
|
} = {
|
|
289
436
|
runId: id,
|
|
290
|
-
mode:
|
|
437
|
+
mode: flatSteps.length > 1 ? "chain" : "single",
|
|
291
438
|
state: "running",
|
|
292
439
|
startedAt: overallStartTime,
|
|
293
440
|
lastUpdate: overallStartTime,
|
|
294
441
|
pid: process.pid,
|
|
295
442
|
cwd,
|
|
296
443
|
currentStep: 0,
|
|
297
|
-
steps:
|
|
444
|
+
steps: flatSteps.map((step) => ({ agent: step.agent, status: "pending", skills: step.skills })),
|
|
298
445
|
artifactsDir,
|
|
299
446
|
sessionDir: config.sessionDir,
|
|
300
|
-
outputFile,
|
|
447
|
+
outputFile: path.join(asyncDir, "output-0.log"),
|
|
301
448
|
};
|
|
302
449
|
|
|
303
450
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
@@ -314,184 +461,218 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
314
461
|
}),
|
|
315
462
|
);
|
|
316
463
|
|
|
464
|
+
// Track the flat index into statusPayload.steps across sequential + parallel steps
|
|
465
|
+
let flatIndex = 0;
|
|
466
|
+
|
|
317
467
|
for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
|
|
318
468
|
const step = steps[stepIndex];
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
469
|
+
|
|
470
|
+
if (isParallelGroup(step)) {
|
|
471
|
+
// === PARALLEL STEP GROUP ===
|
|
472
|
+
const group = step;
|
|
473
|
+
const concurrency = group.concurrency ?? MAX_PARALLEL_CONCURRENCY;
|
|
474
|
+
const failFast = group.failFast ?? false;
|
|
475
|
+
const groupStartFlatIndex = flatIndex;
|
|
476
|
+
let aborted = false;
|
|
477
|
+
|
|
478
|
+
// Mark all tasks in the group as running
|
|
479
|
+
const groupStartTime = Date.now();
|
|
480
|
+
for (let t = 0; t < group.parallel.length; t++) {
|
|
481
|
+
const fi = groupStartFlatIndex + t;
|
|
482
|
+
statusPayload.steps[fi].status = "running";
|
|
483
|
+
statusPayload.steps[fi].startedAt = groupStartTime;
|
|
484
|
+
}
|
|
485
|
+
statusPayload.currentStep = groupStartFlatIndex;
|
|
486
|
+
statusPayload.lastUpdate = groupStartTime;
|
|
487
|
+
statusPayload.outputFile = path.join(asyncDir, `output-${groupStartFlatIndex}.log`);
|
|
488
|
+
writeJson(statusPath, statusPayload);
|
|
489
|
+
|
|
490
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
491
|
+
type: "subagent.parallel.started",
|
|
492
|
+
ts: groupStartTime,
|
|
331
493
|
runId: id,
|
|
332
494
|
stepIndex,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
495
|
+
agents: group.parallel.map((t) => t.agent),
|
|
496
|
+
count: group.parallel.length,
|
|
497
|
+
}));
|
|
498
|
+
|
|
499
|
+
const parallelResults = await mapConcurrent(
|
|
500
|
+
group.parallel,
|
|
501
|
+
concurrency,
|
|
502
|
+
async (task, taskIdx) => {
|
|
503
|
+
if (aborted && failFast) {
|
|
504
|
+
return { agent: task.agent, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const fi = groupStartFlatIndex + taskIdx;
|
|
508
|
+
const taskStartTime = Date.now();
|
|
509
|
+
|
|
510
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
511
|
+
type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent,
|
|
512
|
+
}));
|
|
513
|
+
|
|
514
|
+
// Each parallel task gets its own session subdirectory to avoid conflicts
|
|
515
|
+
const taskSessionDir = config.sessionDir
|
|
516
|
+
? path.join(config.sessionDir, `parallel-${taskIdx}`)
|
|
517
|
+
: undefined;
|
|
518
|
+
|
|
519
|
+
const singleResult = await runSingleStep(task, {
|
|
520
|
+
previousOutput, placeholder, cwd, sessionEnabled,
|
|
521
|
+
sessionDir: taskSessionDir,
|
|
522
|
+
artifactsDir, artifactConfig, id,
|
|
523
|
+
flatIndex: fi, flatStepCount: flatSteps.length,
|
|
524
|
+
outputFile: path.join(asyncDir, `output-${fi}.log`),
|
|
525
|
+
piPackageRoot: config.piPackageRoot,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
const taskEndTime = Date.now();
|
|
529
|
+
const taskDuration = taskEndTime - taskStartTime;
|
|
530
|
+
|
|
531
|
+
statusPayload.steps[fi].status = singleResult.exitCode === 0 ? "complete" : "failed";
|
|
532
|
+
statusPayload.steps[fi].endedAt = taskEndTime;
|
|
533
|
+
statusPayload.steps[fi].durationMs = taskDuration;
|
|
534
|
+
statusPayload.steps[fi].exitCode = singleResult.exitCode;
|
|
535
|
+
statusPayload.lastUpdate = taskEndTime;
|
|
536
|
+
writeJson(statusPath, statusPayload);
|
|
537
|
+
|
|
538
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
539
|
+
type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
540
|
+
ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
|
|
541
|
+
exitCode: singleResult.exitCode, durationMs: taskDuration,
|
|
542
|
+
}));
|
|
543
|
+
|
|
544
|
+
if (singleResult.exitCode !== 0 && failFast) aborted = true;
|
|
545
|
+
return { ...singleResult, skipped: false };
|
|
546
|
+
},
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
flatIndex += group.parallel.length;
|
|
550
|
+
|
|
551
|
+
// Aggregate token usage from parallel task session dirs
|
|
552
|
+
if (config.sessionDir) {
|
|
553
|
+
for (let t = 0; t < group.parallel.length; t++) {
|
|
554
|
+
const taskSessionDir = path.join(config.sessionDir, `parallel-${t}`);
|
|
555
|
+
const taskTokens = parseSessionTokens(taskSessionDir);
|
|
556
|
+
if (taskTokens) {
|
|
557
|
+
const fi = groupStartFlatIndex + t;
|
|
558
|
+
statusPayload.steps[fi].tokens = taskTokens;
|
|
559
|
+
previousCumulativeTokens = {
|
|
560
|
+
input: previousCumulativeTokens.input + taskTokens.input,
|
|
561
|
+
output: previousCumulativeTokens.output + taskTokens.output,
|
|
562
|
+
total: previousCumulativeTokens.total + taskTokens.total,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
358
565
|
}
|
|
566
|
+
statusPayload.totalTokens = { ...previousCumulativeTokens };
|
|
567
|
+
statusPayload.lastUpdate = Date.now();
|
|
568
|
+
writeJson(statusPath, statusPayload);
|
|
359
569
|
}
|
|
360
|
-
if (builtinTools.length > 0) args.push("--tools", builtinTools.join(","));
|
|
361
|
-
}
|
|
362
|
-
if (step.extensions !== undefined) {
|
|
363
|
-
args.push("--no-extensions");
|
|
364
|
-
for (const extPath of step.extensions) args.push("--extension", extPath);
|
|
365
|
-
} else {
|
|
366
|
-
for (const extPath of toolExtensionPaths) args.push("--extension", extPath);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
let tmpDir: string | null = null;
|
|
370
|
-
if (step.systemPrompt) {
|
|
371
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
372
|
-
const promptPath = path.join(tmpDir, "prompt.md");
|
|
373
|
-
fs.writeFileSync(promptPath, step.systemPrompt);
|
|
374
|
-
args.push("--append-system-prompt", promptPath);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const placeholderRegex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
|
|
378
|
-
const task = step.task.replace(placeholderRegex, () => previousOutput);
|
|
379
|
-
args.push(`Task: ${task}`);
|
|
380
|
-
|
|
381
|
-
let artifactPaths: ArtifactPaths | undefined;
|
|
382
|
-
if (artifactsDir && artifactConfig?.enabled !== false) {
|
|
383
|
-
const index = taskIndex !== undefined ? taskIndex : steps.length > 1 ? stepIndex : undefined;
|
|
384
|
-
artifactPaths = getArtifactPaths(artifactsDir, id, step.agent, index);
|
|
385
|
-
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
386
570
|
|
|
387
|
-
|
|
388
|
-
|
|
571
|
+
// Collect results
|
|
572
|
+
for (const pr of parallelResults) {
|
|
573
|
+
results.push({
|
|
574
|
+
agent: pr.agent,
|
|
575
|
+
output: pr.output,
|
|
576
|
+
success: pr.exitCode === 0,
|
|
577
|
+
skipped: pr.skipped,
|
|
578
|
+
artifactPaths: pr.artifactPaths,
|
|
579
|
+
});
|
|
389
580
|
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const mcpEnv: Record<string, string | undefined> = {};
|
|
393
|
-
if (step.mcpDirectTools?.length) {
|
|
394
|
-
mcpEnv.MCP_DIRECT_TOOLS = step.mcpDirectTools.join(",");
|
|
395
|
-
} else {
|
|
396
|
-
mcpEnv.MCP_DIRECT_TOOLS = "__none__";
|
|
397
|
-
}
|
|
398
581
|
|
|
399
|
-
|
|
582
|
+
// Aggregate parallel outputs for {previous}
|
|
583
|
+
previousOutput = aggregateParallelOutputs(
|
|
584
|
+
parallelResults.map((r) => ({ agent: r.agent, output: r.output, exitCode: r.exitCode })),
|
|
585
|
+
);
|
|
400
586
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
587
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
588
|
+
type: "subagent.parallel.completed",
|
|
589
|
+
ts: Date.now(),
|
|
590
|
+
runId: id,
|
|
591
|
+
stepIndex,
|
|
592
|
+
success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1),
|
|
593
|
+
}));
|
|
406
594
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (step.outputPath && result.exitCode === 0) {
|
|
411
|
-
const persisted = persistSingleOutput(step.outputPath, output);
|
|
412
|
-
if (persisted.savedPath) {
|
|
413
|
-
outputForSummary = output
|
|
414
|
-
? `${output}\n\n📄 Output saved to: ${persisted.savedPath}`
|
|
415
|
-
: `📄 Output saved to: ${persisted.savedPath}`;
|
|
416
|
-
} else if (persisted.error) {
|
|
417
|
-
outputForSummary = output
|
|
418
|
-
? `${output}\n\n⚠️ Failed to save output to: ${step.outputPath}\n${persisted.error}`
|
|
419
|
-
: `⚠️ Failed to save output to: ${step.outputPath}\n${persisted.error}`;
|
|
595
|
+
// If any parallel task failed (not skipped), stop the chain
|
|
596
|
+
if (parallelResults.some((r) => r.exitCode !== 0 && r.exitCode !== -1)) {
|
|
597
|
+
break;
|
|
420
598
|
}
|
|
421
|
-
}
|
|
599
|
+
} else {
|
|
600
|
+
// === SEQUENTIAL STEP ===
|
|
601
|
+
const seqStep = step as SubagentStep;
|
|
602
|
+
const stepStartTime = Date.now();
|
|
603
|
+
statusPayload.currentStep = flatIndex;
|
|
604
|
+
statusPayload.steps[flatIndex].status = "running";
|
|
605
|
+
statusPayload.steps[flatIndex].skills = seqStep.skills;
|
|
606
|
+
statusPayload.steps[flatIndex].startedAt = stepStartTime;
|
|
607
|
+
statusPayload.lastUpdate = stepStartTime;
|
|
608
|
+
statusPayload.outputFile = path.join(asyncDir, `output-${flatIndex}.log`);
|
|
609
|
+
writeJson(statusPath, statusPayload);
|
|
610
|
+
|
|
611
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
612
|
+
type: "subagent.step.started",
|
|
613
|
+
ts: stepStartTime,
|
|
614
|
+
runId: id,
|
|
615
|
+
stepIndex: flatIndex,
|
|
616
|
+
agent: seqStep.agent,
|
|
617
|
+
}));
|
|
618
|
+
|
|
619
|
+
const singleResult = await runSingleStep(seqStep, {
|
|
620
|
+
previousOutput, placeholder, cwd, sessionEnabled,
|
|
621
|
+
sessionDir: config.sessionDir,
|
|
622
|
+
artifactsDir, artifactConfig, id,
|
|
623
|
+
flatIndex, flatStepCount: flatSteps.length,
|
|
624
|
+
outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
|
|
625
|
+
piPackageRoot: config.piPackageRoot,
|
|
626
|
+
});
|
|
422
627
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
: null;
|
|
431
|
-
if (cumulativeTokens) {
|
|
432
|
-
previousCumulativeTokens = cumulativeTokens;
|
|
433
|
-
}
|
|
628
|
+
previousOutput = singleResult.output;
|
|
629
|
+
results.push({
|
|
630
|
+
agent: singleResult.agent,
|
|
631
|
+
output: singleResult.output,
|
|
632
|
+
success: singleResult.exitCode === 0,
|
|
633
|
+
artifactPaths: singleResult.artifactPaths,
|
|
634
|
+
});
|
|
434
635
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (
|
|
444
|
-
|
|
636
|
+
const cumulativeTokens = config.sessionDir ? parseSessionTokens(config.sessionDir) : null;
|
|
637
|
+
const stepTokens: TokenUsage | null = cumulativeTokens
|
|
638
|
+
? {
|
|
639
|
+
input: cumulativeTokens.input - previousCumulativeTokens.input,
|
|
640
|
+
output: cumulativeTokens.output - previousCumulativeTokens.output,
|
|
641
|
+
total: cumulativeTokens.total - previousCumulativeTokens.total,
|
|
642
|
+
}
|
|
643
|
+
: null;
|
|
644
|
+
if (cumulativeTokens) {
|
|
645
|
+
previousCumulativeTokens = cumulativeTokens;
|
|
445
646
|
}
|
|
446
647
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
exitCode: result.exitCode,
|
|
456
|
-
durationMs: Date.now() - stepStartTime,
|
|
457
|
-
skills: step.skills,
|
|
458
|
-
timestamp: Date.now(),
|
|
459
|
-
},
|
|
460
|
-
null,
|
|
461
|
-
2,
|
|
462
|
-
),
|
|
463
|
-
"utf-8",
|
|
464
|
-
);
|
|
648
|
+
const stepEndTime = Date.now();
|
|
649
|
+
statusPayload.steps[flatIndex].status = singleResult.exitCode === 0 ? "complete" : "failed";
|
|
650
|
+
statusPayload.steps[flatIndex].endedAt = stepEndTime;
|
|
651
|
+
statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
|
|
652
|
+
statusPayload.steps[flatIndex].exitCode = singleResult.exitCode;
|
|
653
|
+
if (stepTokens) {
|
|
654
|
+
statusPayload.steps[flatIndex].tokens = stepTokens;
|
|
655
|
+
statusPayload.totalTokens = { ...previousCumulativeTokens };
|
|
465
656
|
}
|
|
466
|
-
|
|
657
|
+
statusPayload.lastUpdate = stepEndTime;
|
|
658
|
+
writeJson(statusPath, statusPayload);
|
|
467
659
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
statusPayload.steps[stepIndex].status = result.exitCode === 0 ? "complete" : "failed";
|
|
471
|
-
statusPayload.steps[stepIndex].endedAt = stepEndTime;
|
|
472
|
-
statusPayload.steps[stepIndex].durationMs = stepEndTime - stepStartTime;
|
|
473
|
-
statusPayload.steps[stepIndex].exitCode = result.exitCode;
|
|
474
|
-
if (stepTokens) {
|
|
475
|
-
statusPayload.steps[stepIndex].tokens = stepTokens;
|
|
476
|
-
statusPayload.totalTokens = { ...previousCumulativeTokens };
|
|
477
|
-
}
|
|
478
|
-
statusPayload.lastUpdate = stepEndTime;
|
|
479
|
-
writeJson(statusPath, statusPayload);
|
|
480
|
-
appendJsonl(
|
|
481
|
-
eventsPath,
|
|
482
|
-
JSON.stringify({
|
|
483
|
-
type: result.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
660
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
661
|
+
type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
484
662
|
ts: stepEndTime,
|
|
485
663
|
runId: id,
|
|
486
|
-
stepIndex,
|
|
487
|
-
agent:
|
|
488
|
-
exitCode:
|
|
664
|
+
stepIndex: flatIndex,
|
|
665
|
+
agent: seqStep.agent,
|
|
666
|
+
exitCode: singleResult.exitCode,
|
|
489
667
|
durationMs: stepEndTime - stepStartTime,
|
|
490
668
|
tokens: stepTokens,
|
|
491
|
-
})
|
|
492
|
-
);
|
|
669
|
+
}));
|
|
493
670
|
|
|
494
|
-
|
|
671
|
+
flatIndex++;
|
|
672
|
+
if (singleResult.exitCode !== 0) {
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
495
676
|
}
|
|
496
677
|
|
|
497
678
|
let summary = results.map((r) => `${r.agent}:\n${r.output}`).join("\n\n");
|
|
@@ -507,7 +688,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
507
688
|
}
|
|
508
689
|
}
|
|
509
690
|
|
|
510
|
-
const agentName =
|
|
691
|
+
const agentName = flatSteps.length === 1
|
|
692
|
+
? flatSteps[0].agent
|
|
693
|
+
: `chain:${flatSteps.map((s) => s.agent).join("->")}`;
|
|
511
694
|
let sessionFile: string | undefined;
|
|
512
695
|
let shareUrl: string | undefined;
|
|
513
696
|
let gistUrl: string | undefined;
|
|
@@ -589,6 +772,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
589
772
|
agent: r.agent,
|
|
590
773
|
output: r.output,
|
|
591
774
|
success: r.success,
|
|
775
|
+
skipped: r.skipped || undefined,
|
|
592
776
|
artifactPaths: r.artifactPaths,
|
|
593
777
|
truncated: r.truncated,
|
|
594
778
|
})),
|