@bastani/atomic 0.8.24-alpha.2 → 0.8.24-alpha.3

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -1
  3. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  4. package/dist/builtin/intercom/package.json +1 -1
  5. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  6. package/dist/builtin/mcp/package.json +1 -1
  7. package/dist/builtin/subagents/CHANGELOG.md +10 -0
  8. package/dist/builtin/subagents/README.md +132 -21
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/subagents/prompts/parallel-context-build.md +4 -2
  11. package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +3 -1
  12. package/dist/builtin/subagents/skills/subagent/SKILL.md +49 -11
  13. package/dist/builtin/subagents/src/agents/agent-management.ts +79 -16
  14. package/dist/builtin/subagents/src/agents/agents.ts +47 -16
  15. package/dist/builtin/subagents/src/agents/chain-serializer.ts +114 -0
  16. package/dist/builtin/subagents/src/extension/schemas.ts +139 -3
  17. package/dist/builtin/subagents/src/runs/background/async-execution.ts +92 -6
  18. package/dist/builtin/subagents/src/runs/background/async-status.ts +11 -1
  19. package/dist/builtin/subagents/src/runs/background/run-status.ts +4 -1
  20. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +529 -32
  21. package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +361 -118
  22. package/dist/builtin/subagents/src/runs/foreground/execution.ts +75 -7
  23. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +33 -0
  24. package/dist/builtin/subagents/src/runs/shared/acceptance.ts +611 -0
  25. package/dist/builtin/subagents/src/runs/shared/chain-outputs.ts +101 -0
  26. package/dist/builtin/subagents/src/runs/shared/dynamic-fanout.ts +293 -0
  27. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +29 -1
  28. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +11 -0
  29. package/dist/builtin/subagents/src/runs/shared/structured-output.ts +79 -0
  30. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +52 -2
  31. package/dist/builtin/subagents/src/runs/shared/workflow-graph.ts +206 -0
  32. package/dist/builtin/subagents/src/shared/formatters.ts +2 -2
  33. package/dist/builtin/subagents/src/shared/settings.ts +53 -4
  34. package/dist/builtin/subagents/src/shared/types.ts +226 -0
  35. package/dist/builtin/subagents/src/shared/utils.ts +2 -1
  36. package/dist/builtin/subagents/src/slash/slash-commands.ts +41 -3
  37. package/dist/builtin/subagents/src/tui/render.ts +152 -34
  38. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  39. package/dist/builtin/web-access/package.json +1 -1
  40. package/dist/builtin/workflows/CHANGELOG.md +6 -0
  41. package/dist/builtin/workflows/package.json +1 -1
  42. package/dist/builtin/workflows/skills/create-spec/SKILL.md +1 -1
  43. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +0 -1
  44. package/dist/core/slash-commands.d.ts.map +1 -1
  45. package/dist/core/slash-commands.js +1 -0
  46. package/dist/core/slash-commands.js.map +1 -1
  47. package/dist/core/system-prompt.d.ts.map +1 -1
  48. package/dist/core/system-prompt.js +4 -3
  49. package/dist/core/system-prompt.js.map +1 -1
  50. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  51. package/dist/modes/interactive/interactive-mode.js +1 -1
  52. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  53. package/docs/usage.md +1 -0
  54. package/docs/workflows.md +173 -0
  55. package/package.json +1 -1
@@ -13,11 +13,13 @@ import {
13
13
  type ArtifactPaths,
14
14
  type AsyncParallelGroupStatus,
15
15
  type AsyncStatus,
16
+ type ChainOutputMap,
16
17
  type ModelAttempt,
17
18
  type NestedRouteInfo,
18
19
  type ResolvedControlConfig,
19
20
  type SubagentRunMode,
20
21
  type Usage,
22
+ type WorkflowGraphSnapshot,
21
23
  DEFAULT_MAX_OUTPUT,
22
24
  type MaxOutputConfig,
23
25
  truncateOutput,
@@ -34,6 +36,7 @@ import {
34
36
  import {
35
37
  type RunnerSubagentStep as SubagentStep,
36
38
  type RunnerStep,
39
+ isDynamicRunnerGroup,
37
40
  isParallelGroup,
38
41
  flattenSteps,
39
42
  mapConcurrent,
@@ -41,6 +44,9 @@ import {
41
44
  MAX_PARALLEL_CONCURRENCY,
42
45
  } from "../shared/parallel-utils.ts";
43
46
  import { buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
47
+ import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
48
+ import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
49
+ import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
44
50
  import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
45
51
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
46
52
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
@@ -70,6 +76,8 @@ import {
70
76
  } from "../shared/worktree.ts";
71
77
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
72
78
  import { writeInitialProgressFile } from "../../shared/settings.ts";
79
+ import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
80
+ import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, formatAcceptancePrompt, stripAcceptanceReport } from "../shared/acceptance.ts";
73
81
 
74
82
  interface SubagentRunConfig {
75
83
  id: string;
@@ -94,6 +102,8 @@ interface SubagentRunConfig {
94
102
  controlIntercomTarget?: string;
95
103
  childIntercomTargets?: Array<string | undefined>;
96
104
  resultMode?: SubagentRunMode;
105
+ dynamicFanoutMaxItems?: number;
106
+ workflowGraph?: WorkflowGraphSnapshot;
97
107
  nestedRoute?: NestedRouteInfo;
98
108
  workflowStageSubagentGuard?: boolean;
99
109
  nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> };
@@ -104,6 +114,7 @@ interface StepResult {
104
114
  output: string;
105
115
  error?: string;
106
116
  success: boolean;
117
+ exitCode?: number | null;
107
118
  skipped?: boolean;
108
119
  sessionFile?: string;
109
120
  intercomTarget?: string;
@@ -113,6 +124,10 @@ interface StepResult {
113
124
  modelAttempts?: ModelAttempt[];
114
125
  artifactPaths?: ArtifactPaths;
115
126
  truncated?: boolean;
127
+ structuredOutput?: unknown;
128
+ structuredOutputPath?: string;
129
+ structuredOutputSchemaPath?: string;
130
+ acceptance?: import("../../shared/types.ts").AcceptanceLedger;
116
131
  }
117
132
 
118
133
  const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
@@ -558,6 +573,7 @@ function writeRunLog(
558
573
  /** Context for running a single step */
559
574
  interface SingleStepContext {
560
575
  previousOutput: string;
576
+ outputs?: ChainOutputMap;
561
577
  placeholder: string;
562
578
  cwd: string;
563
579
  sessionEnabled: boolean;
@@ -597,9 +613,22 @@ async function runSingleStep(
597
613
  sessionFile?: string;
598
614
  intercomTarget?: string;
599
615
  completionGuardTriggered?: boolean;
616
+ structuredOutput?: unknown;
617
+ structuredOutputPath?: string;
618
+ structuredOutputSchemaPath?: string;
619
+ acceptance?: import("../../shared/types.ts").AcceptanceLedger;
600
620
  }> {
621
+ const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
622
+ ? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"))
623
+ : undefined);
601
624
  const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
602
- const task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
625
+ let task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
626
+ task = resolveOutputReferences(task, ctx.outputs ?? {});
627
+ const taskForCompletionGuard = task;
628
+ if (step.effectiveAcceptance) {
629
+ const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance);
630
+ if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
631
+ }
603
632
  const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
604
633
  const sessionDir = step.sessionFile ? undefined : ctx.sessionDir;
605
634
 
@@ -632,6 +661,13 @@ async function runSingleStep(
632
661
  const attemptFastMode = fastModeForStepAttempt(step, candidate);
633
662
  ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking), fastMode: attemptFastMode ? true : undefined });
634
663
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
664
+ if (effectiveStructuredOutput) {
665
+ try {
666
+ if (fs.existsSync(effectiveStructuredOutput.outputPath)) fs.unlinkSync(effectiveStructuredOutput.outputPath);
667
+ } catch {
668
+ // Missing/stale structured-output files are handled after the child exits.
669
+ }
670
+ }
635
671
  const { args, env, tempDir } = buildPiArgs({
636
672
  baseArgs: ["--mode", "json", "-p"],
637
673
  task,
@@ -659,6 +695,7 @@ async function runSingleStep(
659
695
  parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
660
696
  codexFastModeSettings: step.codexFastModeSettings,
661
697
  codexFastModeScope: step.codexFastModeScope,
698
+ structuredOutput: effectiveStructuredOutput,
662
699
  });
663
700
  const run = await runPiStreaming(
664
701
  args,
@@ -676,10 +713,21 @@ async function runSingleStep(
676
713
  cleanupTempDir(tempDir);
677
714
 
678
715
  const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
716
+ let structuredOutput: unknown;
717
+ let structuredError: string | undefined;
718
+ if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError) {
719
+ const structured = readStructuredOutput({
720
+ schema: effectiveStructuredOutput.schema,
721
+ schemaPath: effectiveStructuredOutput.schemaPath,
722
+ outputPath: effectiveStructuredOutput.outputPath,
723
+ });
724
+ if (structured.error) structuredError = structured.error;
725
+ else structuredOutput = structured.value;
726
+ }
679
727
  const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && step.completionGuard !== false
680
728
  ? evaluateCompletionMutationGuard({
681
729
  agent: step.agent,
682
- task,
730
+ task: taskForCompletionGuard,
683
731
  messages: run.messages,
684
732
  tools: step.tools,
685
733
  mcpDirectTools: step.mcpDirectTools,
@@ -691,12 +739,15 @@ async function runSingleStep(
691
739
  : undefined;
692
740
  const effectiveExitCode = completionGuardTriggered
693
741
  ? 1
694
- : hiddenError?.hasError
742
+ : structuredError
743
+ ? 1
744
+ : hiddenError?.hasError
695
745
  ? (hiddenError.exitCode ?? 1)
696
746
  : run.error && run.exitCode === 0
697
747
  ? 1
698
748
  : run.exitCode;
699
749
  const error = completionGuardError
750
+ ?? structuredError
700
751
  ?? (hiddenError?.hasError
701
752
  ? hiddenError.details
702
753
  ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
@@ -716,24 +767,26 @@ async function runSingleStep(
716
767
  completionGuardTriggeredFinal = completionGuardTriggered;
717
768
  finalFastMode = attemptFastMode;
718
769
  finalOutputSnapshot = outputSnapshot;
719
- finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error };
770
+ finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
720
771
  if (attempt.success || completionGuardTriggered) break;
721
772
  if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
722
773
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
723
774
  }
724
775
 
725
776
  const rawOutput = finalResult?.finalOutput ?? "";
777
+ const outputForPersistence = stripAcceptanceReport(rawOutput);
726
778
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
727
- ? resolveSingleOutput(step.outputPath, rawOutput, finalOutputSnapshot)
728
- : { fullOutput: rawOutput };
779
+ ? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot)
780
+ : { fullOutput: outputForPersistence };
729
781
  const output = resolvedOutput.fullOutput;
730
782
  const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
731
783
  let outputForSummary = output;
732
- if (attemptNotes.length > 0) {
733
- outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
734
- }
735
- const finalizedOutput = finalizeSingleOutput({
736
- fullOutput: outputForSummary,
784
+ if (attemptNotes.length > 0) {
785
+ outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
786
+ }
787
+ const outputForAcceptance = rawOutput;
788
+ const finalizedOutput = finalizeSingleOutput({
789
+ fullOutput: outputForSummary,
737
790
  outputPath: step.outputPath,
738
791
  outputMode: step.outputMode,
739
792
  exitCode: finalResult?.exitCode ?? 1,
@@ -742,6 +795,19 @@ async function runSingleStep(
742
795
  saveError: resolvedOutput.saveError,
743
796
  });
744
797
  outputForSummary = finalizedOutput.displayOutput;
798
+ const acceptance = step.effectiveAcceptance
799
+ ? await evaluateAcceptance({
800
+ acceptance: step.effectiveAcceptance,
801
+ output: outputForAcceptance,
802
+ cwd: step.cwd ?? ctx.cwd,
803
+ })
804
+ : undefined;
805
+ const acceptanceFailure = acceptance ? acceptanceFailureMessage(acceptance) : undefined;
806
+ const acceptanceCanFailRun = acceptanceFailure && acceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted;
807
+ const effectiveFinalExitCode = acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
808
+ const effectiveFinalError = acceptanceCanFailRun
809
+ ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
810
+ : finalResult?.error;
745
811
 
746
812
  if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
747
813
  if (ctx.artifactConfig?.includeOutput !== false) {
@@ -754,7 +820,7 @@ async function runSingleStep(
754
820
  runId: ctx.id,
755
821
  agent: step.agent,
756
822
  task,
757
- exitCode: finalResult?.exitCode,
823
+ exitCode: effectiveFinalExitCode,
758
824
  model: finalResult?.model,
759
825
  ...(finalFastMode ? { fastMode: true } : {}),
760
826
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
@@ -770,8 +836,8 @@ async function runSingleStep(
770
836
  return {
771
837
  agent: step.agent,
772
838
  output: outputForSummary,
773
- exitCode: finalResult?.exitCode ?? 1,
774
- error: finalResult?.error,
839
+ exitCode: effectiveFinalExitCode,
840
+ error: effectiveFinalError,
775
841
  sessionFile: step.sessionFile,
776
842
  intercomTarget: ctx.childIntercomTarget,
777
843
  model: finalResult?.model,
@@ -781,6 +847,10 @@ async function runSingleStep(
781
847
  artifactPaths,
782
848
  interrupted: finalResult?.interrupted,
783
849
  completionGuardTriggered: completionGuardTriggeredFinal,
850
+ structuredOutput: (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
851
+ structuredOutputPath: effectiveStructuredOutput?.outputPath,
852
+ structuredOutputSchemaPath: effectiveStructuredOutput?.schemaPath,
853
+ acceptance,
784
854
  };
785
855
  }
786
856
 
@@ -823,7 +893,7 @@ function markParallelGroupSetupFailure(input: {
823
893
  input.statusPayload.steps[flatTaskIndex].endedAt = input.failedAt;
824
894
  input.statusPayload.steps[flatTaskIndex].durationMs = 0;
825
895
  input.statusPayload.steps[flatTaskIndex].exitCode = 1;
826
- input.results.push({ agent: input.group.parallel[taskIndex].agent, output: input.setupError, success: false, sessionFile: input.group.parallel[taskIndex].sessionFile });
896
+ input.results.push({ agent: input.group.parallel[taskIndex].agent, output: input.setupError, success: false, exitCode: 1, sessionFile: input.group.parallel[taskIndex].sessionFile });
827
897
  }
828
898
  input.statusPayload.currentStep = input.groupStartFlatIndex;
829
899
  input.statusPayload.lastUpdate = input.failedAt;
@@ -913,6 +983,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
913
983
  const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
914
984
  config;
915
985
  let previousOutput = "";
986
+ const outputs: ChainOutputMap = {};
916
987
  const results: StepResult[] = [];
917
988
  const overallStartTime = Date.now();
918
989
  const shareEnabled = config.share === true;
@@ -929,13 +1000,61 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
929
1000
  let latestSessionFile: string | undefined;
930
1001
 
931
1002
  const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
1003
+ const initialStatusSteps: RunnerStatusStep[] = [];
932
1004
  let flatStepCount = 0;
933
1005
  for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
934
1006
  const step = steps[stepIndex]!;
935
1007
  if (isParallelGroup(step)) {
936
1008
  parallelGroups.push({ start: flatStepCount, count: step.parallel.length, stepIndex });
1009
+ for (const task of step.parallel) {
1010
+ initialStatusSteps.push({
1011
+ agent: task.agent,
1012
+ phase: task.phase,
1013
+ label: task.label,
1014
+ outputName: task.outputName,
1015
+ structured: task.structured,
1016
+ status: "pending",
1017
+ ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
1018
+ skills: task.skills,
1019
+ model: task.model,
1020
+ thinking: task.thinking,
1021
+ ...(task.fastMode ? { fastMode: true } : {}),
1022
+ attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
1023
+ recentTools: [],
1024
+ recentOutput: [],
1025
+ });
1026
+ }
937
1027
  flatStepCount += step.parallel.length;
1028
+ } else if (isDynamicRunnerGroup(step)) {
1029
+ parallelGroups.push({ start: flatStepCount, count: 1, stepIndex });
1030
+ initialStatusSteps.push({
1031
+ agent: `expand:${step.parallel.agent}`,
1032
+ phase: step.phase ?? step.parallel.phase,
1033
+ label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
1034
+ outputName: step.collect.as,
1035
+ structured: Boolean(step.collect.outputSchema),
1036
+ status: "pending",
1037
+ recentTools: [],
1038
+ recentOutput: [],
1039
+ });
1040
+ flatStepCount++;
938
1041
  } else {
1042
+ initialStatusSteps.push({
1043
+ agent: step.agent,
1044
+ phase: step.phase,
1045
+ label: step.label,
1046
+ outputName: step.outputName,
1047
+ structured: step.structured,
1048
+ status: "pending",
1049
+ ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
1050
+ skills: step.skills,
1051
+ model: step.model,
1052
+ thinking: step.thinking,
1053
+ ...(step.fastMode ? { fastMode: true } : {}),
1054
+ attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
1055
+ recentTools: [],
1056
+ recentOutput: [],
1057
+ });
939
1058
  flatStepCount++;
940
1059
  }
941
1060
  }
@@ -956,18 +1075,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
956
1075
  currentStep: 0,
957
1076
  chainStepCount: steps.length,
958
1077
  parallelGroups,
959
- steps: flatSteps.map((step) => ({
960
- agent: step.agent,
961
- status: "pending",
962
- ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
963
- skills: step.skills,
964
- model: step.model,
965
- thinking: step.thinking,
966
- ...(step.fastMode ? { fastMode: true } : {}),
967
- attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
968
- recentTools: [],
969
- recentOutput: [],
970
- })),
1078
+ workflowGraph: config.workflowGraph,
1079
+ steps: initialStatusSteps,
971
1080
  artifactsDir,
972
1081
  sessionDir: config.sessionDir,
973
1082
  outputFile: path.join(asyncDir, "output-0.log"),
@@ -997,10 +1106,48 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
997
1106
  console.error("Failed to emit nested async status event:", error);
998
1107
  }
999
1108
  };
1109
+ const refreshWorkflowGraph = (): void => {
1110
+ if (!config.workflowGraph) return;
1111
+ const graph = structuredClone(statusPayload.workflowGraph ?? config.workflowGraph);
1112
+ const normalize = (status: RunnerStatusStep["status"]): "pending" | "running" | "completed" | "failed" | "paused" | "detached" => {
1113
+ if (status === "complete" || status === "completed") return "completed";
1114
+ if (status === "running" || status === "failed" || status === "paused" || status === "pending") return status;
1115
+ return "pending";
1116
+ };
1117
+ const updateNode = (node: NonNullable<typeof graph.nodes>[number]): void => {
1118
+ if (node.flatIndex !== undefined) {
1119
+ const step = statusPayload.steps[node.flatIndex];
1120
+ if (step) {
1121
+ node.status = normalize(step.status);
1122
+ node.error = step.error;
1123
+ node.acceptanceStatus = step.acceptance?.status;
1124
+ }
1125
+ if (statusPayload.currentStep === node.flatIndex) graph.currentNodeId = node.id;
1126
+ }
1127
+ for (const child of node.children ?? []) updateNode(child);
1128
+ if (node.children?.length) {
1129
+ if (node.children.every((child) => child.status === "completed")) node.status = "completed";
1130
+ else if (node.children.some((child) => child.status === "running")) node.status = "running";
1131
+ else if (node.children.some((child) => child.status === "failed")) node.status = "failed";
1132
+ else if (node.children.some((child) => child.status === "paused")) node.status = "paused";
1133
+ }
1134
+ if (node.error) node.status = "failed";
1135
+ };
1136
+ for (const node of graph.nodes) updateNode(node);
1137
+ statusPayload.workflowGraph = graph;
1138
+ };
1000
1139
  const writeStatusPayload = (): void => {
1140
+ refreshWorkflowGraph();
1001
1141
  writeAtomicJson(statusPath, statusPayload);
1002
1142
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
1003
1143
  };
1144
+ const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running", error?: string, acceptance?: import("../../shared/types.ts").AcceptanceLedger): void => {
1145
+ const groupNode = statusPayload.workflowGraph?.nodes.find((node) => node.id === `step-${stepIndex}`);
1146
+ if (!groupNode) return;
1147
+ groupNode.status = status;
1148
+ groupNode.error = error;
1149
+ groupNode.acceptanceStatus = acceptance?.status ?? groupNode.acceptanceStatus;
1150
+ };
1004
1151
 
1005
1152
  const stepOutputActivityAt = (index: number): number => {
1006
1153
  const step = statusPayload.steps[index];
@@ -1017,8 +1164,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1017
1164
  };
1018
1165
  const emittedControlEventKeys = new Set<string>();
1019
1166
  const activeLongRunningSteps = new Set<number>();
1020
- const mutatingFailureStates = flatSteps.map(() => createMutatingFailureState());
1021
- const pendingToolResults: Array<{ tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined> = [];
1167
+ const mutatingFailureStates = initialStatusSteps.map(() => createMutatingFailureState());
1168
+ const pendingToolResults: Array<{ tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined> = initialStatusSteps.map(() => undefined);
1022
1169
  const mutatingFailureWindowMs = 5 * 60_000;
1023
1170
  const appendControlEvent = (event: ReturnType<typeof buildControlEvent>) => {
1024
1171
  if (!controlConfig.enabled) return;
@@ -1160,7 +1307,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1160
1307
  resetMutatingFailureState(mutatingFailureStates[flatIndex]!);
1161
1308
  }
1162
1309
  } else if (event.type === "message_end" && event.message?.role === "assistant") {
1163
- appendRecentStepOutput(step, extractTextFromContent(event.message.content).split("\n").slice(-10));
1310
+ appendRecentStepOutput(step, stripAcceptanceReport(extractTextFromContent(event.message.content)).split("\n").slice(-10));
1164
1311
  step.turnCount = (step.turnCount ?? 0) + 1;
1165
1312
  const usage = event.message.usage;
1166
1313
  if (usage) {
@@ -1291,6 +1438,313 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1291
1438
  if (interrupted) break;
1292
1439
  const step = steps[stepIndex];
1293
1440
 
1441
+ if (isDynamicRunnerGroup(step)) {
1442
+ const groupStartFlatIndex = flatIndex;
1443
+ let materialized: ReturnType<typeof materializeDynamicParallelStep>;
1444
+ try {
1445
+ materialized = materializeDynamicParallelStep(step as Parameters<typeof materializeDynamicParallelStep>[0], outputs, stepIndex, { maxItems: config.dynamicFanoutMaxItems, allowRunnerFields: true });
1446
+ if (materialized.collectedOnEmpty) validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
1447
+ } catch (error) {
1448
+ const now = Date.now();
1449
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
1450
+ statusPayload.state = "failed";
1451
+ statusPayload.error = message;
1452
+ statusPayload.currentStep = flatIndex;
1453
+ const placeholder = statusPayload.steps[groupStartFlatIndex];
1454
+ if (placeholder) {
1455
+ placeholder.status = "failed";
1456
+ placeholder.error = message;
1457
+ placeholder.startedAt = now;
1458
+ placeholder.endedAt = now;
1459
+ placeholder.durationMs = 0;
1460
+ placeholder.exitCode = 1;
1461
+ }
1462
+ statusPayload.lastUpdate = now;
1463
+ markDynamicGraphGroup(stepIndex, "failed", message);
1464
+ writeStatusPayload();
1465
+ results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1 });
1466
+ break;
1467
+ }
1468
+
1469
+ if (materialized.parallel.length === 0) {
1470
+ const now = Date.now();
1471
+ const collection = materialized.collectedOnEmpty ?? [];
1472
+ outputs[step.collect.as] = {
1473
+ text: JSON.stringify(collection),
1474
+ structured: collection,
1475
+ agent: step.parallel.agent,
1476
+ stepIndex,
1477
+ };
1478
+ statusPayload.outputs = outputs;
1479
+ const placeholder = statusPayload.steps[groupStartFlatIndex];
1480
+ if (placeholder) {
1481
+ placeholder.status = "complete";
1482
+ placeholder.startedAt = now;
1483
+ placeholder.endedAt = now;
1484
+ placeholder.durationMs = 0;
1485
+ }
1486
+ previousOutput = "Dynamic fanout produced 0 results.";
1487
+ const groupAcceptance = step.effectiveAcceptance?.explicit
1488
+ ? await evaluateAcceptance({
1489
+ acceptance: step.effectiveAcceptance,
1490
+ output: "",
1491
+ report: aggregateAcceptanceReport({
1492
+ results: [],
1493
+ notes: "Dynamic fanout produced 0 results.",
1494
+ }),
1495
+ cwd,
1496
+ })
1497
+ : undefined;
1498
+ if (placeholder && groupAcceptance) placeholder.acceptance = groupAcceptance;
1499
+ const groupAcceptanceFailure = groupAcceptance ? acceptanceFailureMessage(groupAcceptance) : undefined;
1500
+ if (groupAcceptanceFailure) {
1501
+ statusPayload.state = "failed";
1502
+ statusPayload.error = groupAcceptanceFailure;
1503
+ if (placeholder) {
1504
+ placeholder.status = "failed";
1505
+ placeholder.error = groupAcceptanceFailure;
1506
+ placeholder.exitCode = 1;
1507
+ }
1508
+ markDynamicGraphGroup(stepIndex, "failed", groupAcceptanceFailure, groupAcceptance);
1509
+ statusPayload.lastUpdate = now;
1510
+ writeStatusPayload();
1511
+ results.push({ agent: step.parallel.agent, output: groupAcceptanceFailure, error: groupAcceptanceFailure, success: false, exitCode: 1, acceptance: groupAcceptance });
1512
+ break;
1513
+ }
1514
+ flatIndex++;
1515
+ statusPayload.lastUpdate = now;
1516
+ markDynamicGraphGroup(stepIndex, "completed", undefined, groupAcceptance);
1517
+ writeStatusPayload();
1518
+ continue;
1519
+ }
1520
+
1521
+ const dynamicSteps = materialized.parallel.map((task, itemIndex) => ({
1522
+ ...step.parallel,
1523
+ task: task.task ?? step.parallel.task,
1524
+ label: task.label ?? step.parallel.label,
1525
+ structuredOutput: undefined,
1526
+ structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
1527
+ }));
1528
+ const dynamicStatusSteps: RunnerStatusStep[] = dynamicSteps.map((task) => ({
1529
+ agent: task.agent,
1530
+ phase: task.phase ?? step.phase,
1531
+ label: task.label,
1532
+ outputName: undefined,
1533
+ structured: Boolean(task.structuredOutputSchema),
1534
+ status: "pending",
1535
+ ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
1536
+ skills: task.skills,
1537
+ model: task.model,
1538
+ thinking: task.thinking,
1539
+ attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
1540
+ recentTools: [],
1541
+ recentOutput: [],
1542
+ }));
1543
+ statusPayload.steps.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps);
1544
+ if (config.childIntercomTargets) {
1545
+ config.childIntercomTargets = statusPayload.steps.map((statusStep, index) => resolveSubagentIntercomTarget(id, statusStep.agent, index));
1546
+ }
1547
+ mutatingFailureStates.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps.map(() => createMutatingFailureState()));
1548
+ pendingToolResults.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps.map(() => undefined));
1549
+ const materializedDelta = dynamicStatusSteps.length - 1;
1550
+ for (const group of statusPayload.parallelGroups) {
1551
+ if (group.stepIndex === stepIndex) {
1552
+ group.start = groupStartFlatIndex;
1553
+ group.count = dynamicStatusSteps.length;
1554
+ } else if (group.start > groupStartFlatIndex) {
1555
+ group.start += materializedDelta;
1556
+ }
1557
+ }
1558
+ if (statusPayload.workflowGraph) {
1559
+ const shiftFlatIndexes = (nodes: NonNullable<typeof statusPayload.workflowGraph>["nodes"]): void => {
1560
+ for (const node of nodes) {
1561
+ if (node.stepIndex !== undefined && node.stepIndex > stepIndex && node.flatIndex !== undefined && node.flatIndex >= groupStartFlatIndex) {
1562
+ node.flatIndex += dynamicStatusSteps.length;
1563
+ }
1564
+ if (node.children) shiftFlatIndexes(node.children);
1565
+ }
1566
+ };
1567
+ shiftFlatIndexes(statusPayload.workflowGraph.nodes);
1568
+ const groupNode = statusPayload.workflowGraph.nodes.find((node) => node.id === `step-${stepIndex}`);
1569
+ if (groupNode) {
1570
+ groupNode.children = materialized.items.map((item, itemIndex) => ({
1571
+ id: `step-${stepIndex}-item-${item.idKey}`,
1572
+ kind: "agent",
1573
+ agent: step.parallel.agent,
1574
+ phase: dynamicSteps[itemIndex]?.phase ?? step.phase,
1575
+ label: dynamicSteps[itemIndex]?.label?.trim() || `${step.parallel.agent} ${item.key}`,
1576
+ status: "pending",
1577
+ flatIndex: groupStartFlatIndex + itemIndex,
1578
+ stepIndex,
1579
+ itemKey: item.key,
1580
+ structured: Boolean(dynamicSteps[itemIndex]?.structuredOutputSchema),
1581
+ }));
1582
+ }
1583
+ }
1584
+ writeStatusPayload();
1585
+
1586
+ const concurrency = step.concurrency ?? MAX_PARALLEL_CONCURRENCY;
1587
+ const failFast = step.failFast ?? false;
1588
+ let aborted = false;
1589
+ const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
1590
+ const fi = groupStartFlatIndex + taskIdx;
1591
+ if (aborted && failFast) {
1592
+ const skippedAt = Date.now();
1593
+ statusPayload.steps[fi].status = "failed";
1594
+ statusPayload.steps[fi].error = "Skipped due to fail-fast";
1595
+ statusPayload.steps[fi].startedAt = skippedAt;
1596
+ statusPayload.steps[fi].endedAt = skippedAt;
1597
+ statusPayload.steps[fi].durationMs = 0;
1598
+ statusPayload.steps[fi].exitCode = -1;
1599
+ statusPayload.lastUpdate = skippedAt;
1600
+ writeStatusPayload();
1601
+ return { agent: task.agent, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
1602
+ }
1603
+ const taskStartTime = Date.now();
1604
+ statusPayload.currentStep = fi;
1605
+ statusPayload.steps[fi].status = "running";
1606
+ statusPayload.steps[fi].error = undefined;
1607
+ statusPayload.steps[fi].activityState = undefined;
1608
+ resetStepLiveDetail(statusPayload.steps[fi]);
1609
+ statusPayload.steps[fi].startedAt = taskStartTime;
1610
+ statusPayload.steps[fi].lastActivityAt = taskStartTime;
1611
+ statusPayload.outputFile = path.join(asyncDir, `output-${fi}.log`);
1612
+ statusPayload.lastActivityAt = taskStartTime;
1613
+ statusPayload.lastUpdate = taskStartTime;
1614
+ writeStatusPayload();
1615
+ appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent }));
1616
+ const singleResult = await runSingleStep(task, {
1617
+ previousOutput, placeholder, cwd, sessionEnabled,
1618
+ outputs,
1619
+ sessionDir: config.sessionDir ? path.join(config.sessionDir, `dynamic-${stepIndex}-${taskIdx}`) : undefined,
1620
+ artifactsDir, artifactConfig, id,
1621
+ flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
1622
+ outputFile: path.join(asyncDir, `output-${fi}.log`),
1623
+ piPackageRoot: config.piPackageRoot,
1624
+ piArgv1: config.piArgv1,
1625
+ childIntercomTarget: config.childIntercomTargets?.[fi],
1626
+ orchestratorIntercomTarget: config.controlIntercomTarget,
1627
+ nestedRoute: config.nestedRoute,
1628
+ registerInterrupt: (interrupt) => {
1629
+ activeChildInterrupt = interrupt;
1630
+ },
1631
+ onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
1632
+ onChildEvent: (event) => updateStepFromChildEvent(fi, event),
1633
+ });
1634
+ const taskEndTime = Date.now();
1635
+ statusPayload.steps[fi].status = singleResult.exitCode === 0 ? "complete" : "failed";
1636
+ statusPayload.steps[fi].endedAt = taskEndTime;
1637
+ statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
1638
+ statusPayload.steps[fi].exitCode = singleResult.exitCode;
1639
+ statusPayload.steps[fi].model = singleResult.model;
1640
+ statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
1641
+ statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
1642
+ statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
1643
+ statusPayload.steps[fi].error = singleResult.error;
1644
+ statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
1645
+ statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
1646
+ statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
1647
+ statusPayload.steps[fi].acceptance = singleResult.acceptance;
1648
+ statusPayload.lastUpdate = taskEndTime;
1649
+ writeStatusPayload();
1650
+ appendJsonl(eventsPath, JSON.stringify({
1651
+ type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
1652
+ ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
1653
+ exitCode: singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
1654
+ }));
1655
+ if (singleResult.exitCode !== 0 && failFast) aborted = true;
1656
+ return { ...singleResult, skipped: false };
1657
+ });
1658
+
1659
+ flatIndex += dynamicSteps.length;
1660
+ for (const pr of parallelResults) {
1661
+ results.push({
1662
+ agent: pr.agent,
1663
+ output: pr.output,
1664
+ error: pr.error,
1665
+ success: pr.exitCode === 0,
1666
+ exitCode: pr.exitCode,
1667
+ skipped: pr.skipped,
1668
+ sessionFile: pr.sessionFile,
1669
+ intercomTarget: pr.intercomTarget,
1670
+ model: pr.model,
1671
+ attemptedModels: pr.attemptedModels,
1672
+ modelAttempts: pr.modelAttempts,
1673
+ artifactPaths: pr.artifactPaths,
1674
+ structuredOutput: pr.structuredOutput,
1675
+ structuredOutputPath: pr.structuredOutputPath,
1676
+ structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
1677
+ acceptance: pr.acceptance,
1678
+ });
1679
+ }
1680
+ const collection = collectDynamicResults(step as Parameters<typeof collectDynamicResults>[0], materialized.items, parallelResults);
1681
+ const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
1682
+ if (failures.length === 0) {
1683
+ try {
1684
+ validateDynamicCollection(step.collect.outputSchema, collection);
1685
+ outputs[step.collect.as] = {
1686
+ text: JSON.stringify(collection),
1687
+ structured: collection,
1688
+ agent: step.parallel.agent,
1689
+ stepIndex,
1690
+ };
1691
+ statusPayload.outputs = outputs;
1692
+ const groupAcceptance = step.effectiveAcceptance
1693
+ ? await evaluateAcceptance({
1694
+ acceptance: step.effectiveAcceptance,
1695
+ output: "",
1696
+ report: aggregateAcceptanceReport({
1697
+ results: parallelResults,
1698
+ notes: `Dynamic fanout collected ${collection.length} result(s) into ${step.collect.as}.`,
1699
+ }),
1700
+ cwd,
1701
+ })
1702
+ : undefined;
1703
+ const groupAcceptanceFailure = groupAcceptance ? acceptanceFailureMessage(groupAcceptance) : undefined;
1704
+ markDynamicGraphGroup(stepIndex, groupAcceptanceFailure ? "failed" : "completed", groupAcceptanceFailure, groupAcceptance);
1705
+ if (groupAcceptanceFailure) {
1706
+ results.push({
1707
+ agent: step.parallel.agent,
1708
+ output: groupAcceptanceFailure,
1709
+ error: groupAcceptanceFailure,
1710
+ success: false,
1711
+ exitCode: 1,
1712
+ structuredOutput: collection,
1713
+ acceptance: groupAcceptance,
1714
+ });
1715
+ statusPayload.error = groupAcceptanceFailure;
1716
+ }
1717
+ } catch (error) {
1718
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
1719
+ results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
1720
+ statusPayload.error = message;
1721
+ markDynamicGraphGroup(stepIndex, "failed", message);
1722
+ }
1723
+ }
1724
+ previousOutput = aggregateParallelOutputs(
1725
+ parallelResults.map((r, i) => ({
1726
+ agent: r.agent,
1727
+ taskIndex: i,
1728
+ output: r.output,
1729
+ exitCode: r.exitCode,
1730
+ error: r.error,
1731
+ })),
1732
+ (i, agent) => `=== Dynamic Item ${i + 1} (${agent}, key ${materialized.items[i]?.key ?? i}) ===`,
1733
+ );
1734
+ appendJsonl(eventsPath, JSON.stringify({
1735
+ type: "subagent.dynamic.completed",
1736
+ ts: Date.now(),
1737
+ runId: id,
1738
+ stepIndex,
1739
+ success: failures.length === 0,
1740
+ }));
1741
+ if (failures.length > 0) markDynamicGraphGroup(stepIndex, "failed", failures[0]?.error ?? "Dynamic fanout child failed.");
1742
+ statusPayload.lastUpdate = Date.now();
1743
+ writeStatusPayload();
1744
+ if (failures.length > 0 || statusPayload.error) break;
1745
+ continue;
1746
+ }
1747
+
1294
1748
  if (isParallelGroup(step)) {
1295
1749
  const group = step;
1296
1750
  const concurrency = group.concurrency ?? MAX_PARALLEL_CONCURRENCY;
@@ -1408,6 +1862,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1408
1862
 
1409
1863
  const singleResult = await runSingleStep(taskForRun, {
1410
1864
  previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
1865
+ outputs,
1411
1866
  sessionDir: taskSessionDir,
1412
1867
  artifactsDir, artifactConfig, id,
1413
1868
  flatIndex: fi, flatStepCount: flatSteps.length,
@@ -1441,6 +1896,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1441
1896
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
1442
1897
  statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
1443
1898
  statusPayload.steps[fi].error = singleResult.error;
1899
+ statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
1900
+ statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
1901
+ statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
1902
+ statusPayload.steps[fi].acceptance = singleResult.acceptance;
1444
1903
  statusPayload.lastUpdate = taskEndTime;
1445
1904
  writeStatusPayload();
1446
1905
 
@@ -1494,6 +1953,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1494
1953
  output: pr.output,
1495
1954
  error: pr.error,
1496
1955
  success: pr.exitCode === 0,
1956
+ exitCode: pr.exitCode,
1497
1957
  skipped: pr.skipped,
1498
1958
  sessionFile: pr.sessionFile,
1499
1959
  intercomTarget: pr.intercomTarget,
@@ -1502,8 +1962,21 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1502
1962
  attemptedModels: pr.attemptedModels,
1503
1963
  modelAttempts: pr.modelAttempts,
1504
1964
  artifactPaths: pr.artifactPaths,
1505
- });
1965
+ structuredOutput: pr.structuredOutput,
1966
+ structuredOutputPath: pr.structuredOutputPath,
1967
+ structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
1968
+ acceptance: pr.acceptance,
1969
+ });
1970
+ }
1971
+ for (let t = 0; t < group.parallel.length; t++) {
1972
+ const outputName = group.parallel[t]?.outputName;
1973
+ if (outputName) outputs[outputName] = outputEntryFromAsyncResult({
1974
+ agent: parallelResults[t]!.agent,
1975
+ output: parallelResults[t]!.output,
1976
+ structuredOutput: parallelResults[t]!.structuredOutput,
1977
+ }, stepIndex);
1506
1978
  }
1979
+ statusPayload.outputs = outputs;
1507
1980
 
1508
1981
  previousOutput = aggregateParallelOutputs(
1509
1982
  parallelResults.map((r) => ({
@@ -1558,6 +2031,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1558
2031
 
1559
2032
  const singleResult = await runSingleStep(seqStep, {
1560
2033
  previousOutput, placeholder, cwd, sessionEnabled,
2034
+ outputs,
1561
2035
  sessionDir: config.sessionDir,
1562
2036
  artifactsDir, artifactConfig, id,
1563
2037
  flatIndex, flatStepCount: flatSteps.length,
@@ -1584,6 +2058,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1584
2058
  output: singleResult.output,
1585
2059
  error: singleResult.error,
1586
2060
  success: singleResult.exitCode === 0,
2061
+ exitCode: singleResult.exitCode,
1587
2062
  sessionFile: singleResult.sessionFile,
1588
2063
  intercomTarget: singleResult.intercomTarget,
1589
2064
  model: singleResult.model,
@@ -1591,7 +2066,19 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1591
2066
  attemptedModels: singleResult.attemptedModels,
1592
2067
  modelAttempts: singleResult.modelAttempts,
1593
2068
  artifactPaths: singleResult.artifactPaths,
2069
+ structuredOutput: singleResult.structuredOutput,
2070
+ structuredOutputPath: singleResult.structuredOutputPath,
2071
+ structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
2072
+ acceptance: singleResult.acceptance,
1594
2073
  });
2074
+ if (seqStep.outputName) {
2075
+ outputs[seqStep.outputName] = outputEntryFromAsyncResult({
2076
+ agent: singleResult.agent,
2077
+ output: singleResult.output,
2078
+ structuredOutput: singleResult.structuredOutput,
2079
+ }, stepIndex);
2080
+ }
2081
+ statusPayload.outputs = outputs;
1595
2082
 
1596
2083
  const cumulativeTokens = config.sessionDir ? parseSessionTokens(config.sessionDir) : null;
1597
2084
  let stepTokens: TokenUsage | null = cumulativeTokens
@@ -1625,6 +2112,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1625
2112
  statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
1626
2113
  statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
1627
2114
  statusPayload.steps[flatIndex].error = singleResult.error;
2115
+ statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
2116
+ statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
2117
+ statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
2118
+ statusPayload.steps[flatIndex].acceptance = singleResult.acceptance;
1628
2119
  if (stepTokens) {
1629
2120
  statusPayload.steps[flatIndex].tokens = stepTokens;
1630
2121
  statusPayload.totalTokens = { ...previousCumulativeTokens };
@@ -1726,7 +2217,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1726
2217
  statusPayload.shareUrl = shareUrl;
1727
2218
  statusPayload.gistUrl = gistUrl;
1728
2219
  statusPayload.shareError = shareError;
1729
- if (statusPayload.state === "failed") {
2220
+ if (statusPayload.state === "failed" && !statusPayload.error) {
1730
2221
  const failedStep = statusPayload.steps.find((s) => s.status === "failed");
1731
2222
  if (failedStep?.agent) {
1732
2223
  statusPayload.error = `Step failed: ${failedStep.agent}`;
@@ -1784,7 +2275,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1784
2275
  modelAttempts: r.modelAttempts,
1785
2276
  artifactPaths: r.artifactPaths,
1786
2277
  truncated: r.truncated,
2278
+ structuredOutput: r.structuredOutput,
2279
+ structuredOutputPath: r.structuredOutputPath,
2280
+ structuredOutputSchemaPath: r.structuredOutputSchemaPath,
2281
+ acceptance: r.acceptance,
1787
2282
  })),
2283
+ outputs,
2284
+ workflowGraph: statusPayload.workflowGraph,
1788
2285
  exitCode: interrupted || results.every((r) => r.success) ? 0 : 1,
1789
2286
  timestamp: runEndedAt,
1790
2287
  durationMs: runEndedAt - overallStartTime,