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.
@@ -15,23 +15,19 @@ import {
15
15
  truncateOutput,
16
16
  getSubagentDepthEnv,
17
17
  } from "./types.js";
18
-
19
- interface SubagentStep {
20
- agent: string;
21
- task: string;
22
- cwd?: string;
23
- model?: string;
24
- tools?: string[];
25
- extensions?: string[];
26
- mcpDirectTools?: string[];
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: SubagentStep[];
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 ?? path.dirname(require.resolve("@mariozechner/pi-coding-agent/package.json"));
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
- const outputFile = path.join(asyncDir, "output.log");
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: steps.length > 1 ? "chain" : "single",
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: steps.map((step) => ({ agent: step.agent, status: "pending", skills: step.skills })),
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
- const stepStartTime = Date.now();
320
- statusPayload.currentStep = stepIndex;
321
- statusPayload.steps[stepIndex].status = "running";
322
- statusPayload.steps[stepIndex].skills = step.skills;
323
- statusPayload.steps[stepIndex].startedAt = stepStartTime;
324
- statusPayload.lastUpdate = stepStartTime;
325
- writeJson(statusPath, statusPayload);
326
- appendJsonl(
327
- eventsPath,
328
- JSON.stringify({
329
- type: "subagent.step.started",
330
- ts: stepStartTime,
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
- agent: step.agent,
334
- }),
335
- );
336
- const args = ["-p"];
337
- if (!sessionEnabled) {
338
- args.push("--no-session");
339
- }
340
- if (config.sessionDir) {
341
- try {
342
- fs.mkdirSync(config.sessionDir, { recursive: true });
343
- } catch {}
344
- args.push("--session-dir", config.sessionDir);
345
- }
346
- // Use --models (not --model) because pi CLI silently ignores --model
347
- // without a companion --provider flag. --models resolves the provider
348
- // automatically via resolveModelScope. See: #8
349
- if (step.model) args.push("--models", step.model);
350
- const toolExtensionPaths: string[] = [];
351
- if (step.tools?.length) {
352
- const builtinTools: string[] = [];
353
- for (const tool of step.tools) {
354
- if (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")) {
355
- toolExtensionPaths.push(tool);
356
- } else {
357
- builtinTools.push(tool);
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
- if (artifactConfig?.includeInput !== false) {
388
- fs.writeFileSync(artifactPaths.inputPath, `# Task for ${step.agent}\n\n${task}`, "utf-8");
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
- const result = await runPiStreaming(args, step.cwd ?? cwd, outputFile, mcpEnv);
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
- if (tmpDir) {
402
- try {
403
- fs.rmSync(tmpDir, { recursive: true });
404
- } catch {}
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
- const output = (result.stdout || "").trim();
408
- previousOutput = output;
409
- let outputForSummary = output;
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
- const cumulativeTokens = config.sessionDir ? parseSessionTokens(config.sessionDir) : null;
424
- const stepTokens: TokenUsage | null = cumulativeTokens
425
- ? {
426
- input: cumulativeTokens.input - previousCumulativeTokens.input,
427
- output: cumulativeTokens.output - previousCumulativeTokens.output,
428
- total: cumulativeTokens.total - previousCumulativeTokens.total,
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
- const stepResult: StepResult = {
436
- agent: step.agent,
437
- output: outputForSummary,
438
- success: result.exitCode === 0,
439
- artifactPaths,
440
- };
441
-
442
- if (artifactPaths && artifactConfig?.enabled !== false) {
443
- if (artifactConfig?.includeOutput !== false) {
444
- fs.writeFileSync(artifactPaths.outputPath, output, "utf-8");
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
- if (artifactConfig?.includeMetadata !== false) {
448
- fs.writeFileSync(
449
- artifactPaths.metadataPath,
450
- JSON.stringify(
451
- {
452
- runId: id,
453
- agent: step.agent,
454
- task,
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
- results.push(stepResult);
469
- const stepEndTime = Date.now();
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: step.agent,
488
- exitCode: result.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
- if (result.exitCode !== 0) break;
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 = steps.length === 1 ? steps[0].agent : `chain:${steps.map((s) => s.agent).join("->")}`;
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
  })),