pi-subagents 0.25.0 → 0.27.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +129 -17
  3. package/package.json +1 -1
  4. package/prompts/parallel-context-build.md +3 -1
  5. package/prompts/parallel-handoff-plan.md +3 -1
  6. package/skills/pi-subagents/SKILL.md +32 -17
  7. package/src/agents/agent-management.ts +57 -15
  8. package/src/agents/agent-serializer.ts +3 -2
  9. package/src/agents/agents.ts +47 -16
  10. package/src/agents/chain-serializer.ts +120 -0
  11. package/src/extension/fanout-child.ts +1 -0
  12. package/src/extension/index.ts +1 -0
  13. package/src/extension/schemas.ts +138 -5
  14. package/src/runs/background/async-execution.ts +84 -6
  15. package/src/runs/background/async-status.ts +11 -1
  16. package/src/runs/background/run-status.ts +10 -1
  17. package/src/runs/background/subagent-runner.ts +600 -31
  18. package/src/runs/foreground/chain-execution.ts +325 -118
  19. package/src/runs/foreground/execution.ts +222 -10
  20. package/src/runs/foreground/subagent-executor.ts +67 -0
  21. package/src/runs/shared/acceptance-contract.ts +291 -0
  22. package/src/runs/shared/acceptance-evaluation.ts +221 -0
  23. package/src/runs/shared/acceptance-finalization.ts +161 -0
  24. package/src/runs/shared/acceptance-reports.ts +127 -0
  25. package/src/runs/shared/acceptance.ts +22 -0
  26. package/src/runs/shared/chain-outputs.ts +101 -0
  27. package/src/runs/shared/completion-guard.ts +26 -3
  28. package/src/runs/shared/dynamic-fanout.ts +293 -0
  29. package/src/runs/shared/parallel-utils.ts +31 -1
  30. package/src/runs/shared/pi-args.ts +11 -0
  31. package/src/runs/shared/structured-output.ts +77 -0
  32. package/src/runs/shared/subagent-prompt-runtime.ts +53 -3
  33. package/src/runs/shared/workflow-graph.ts +206 -0
  34. package/src/shared/formatters.ts +2 -2
  35. package/src/shared/settings.ts +53 -4
  36. package/src/shared/types.ts +250 -0
  37. package/src/slash/slash-commands.ts +41 -3
  38. package/src/tui/render.ts +162 -34
@@ -8,16 +8,21 @@ import { appendJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
8
8
  import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
9
9
  import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
10
10
  import {
11
+ type AcceptanceFinalizationTurn,
12
+ type AcceptanceLedger,
11
13
  type ActivityState,
12
14
  type ArtifactConfig,
13
15
  type ArtifactPaths,
14
16
  type AsyncParallelGroupStatus,
15
17
  type AsyncStatus,
18
+ type ChainOutputMap,
16
19
  type ModelAttempt,
17
20
  type NestedRouteInfo,
18
21
  type ResolvedControlConfig,
19
22
  type SubagentRunMode,
23
+ type TokenUsage,
20
24
  type Usage,
25
+ type WorkflowGraphSnapshot,
21
26
  DEFAULT_MAX_OUTPUT,
22
27
  type MaxOutputConfig,
23
28
  truncateOutput,
@@ -34,6 +39,7 @@ import {
34
39
  import {
35
40
  type RunnerSubagentStep as SubagentStep,
36
41
  type RunnerStep,
42
+ isDynamicRunnerGroup,
37
43
  isParallelGroup,
38
44
  flattenSteps,
39
45
  mapConcurrent,
@@ -41,11 +47,14 @@ import {
41
47
  MAX_PARALLEL_CONCURRENCY,
42
48
  } from "../shared/parallel-utils.ts";
43
49
  import { buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
50
+ import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
51
+ import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
52
+ import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
44
53
  import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
45
54
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
46
55
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
47
56
  import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
48
- import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
57
+ import { evaluateCompletionMutationGuard, resolveCompletionPolicy } from "../shared/completion-guard.ts";
49
58
  import {
50
59
  createMutatingFailureState,
51
60
  didMutatingToolFail,
@@ -58,7 +67,6 @@ import {
58
67
  summarizeRecentMutatingFailures,
59
68
  } from "../shared/long-running-guard.ts";
60
69
  import { parseSessionTokens } from "../../shared/session-tokens.ts";
61
- import type { TokenUsage } from "../../shared/types.ts";
62
70
  import {
63
71
  cleanupWorktrees,
64
72
  createWorktrees,
@@ -70,6 +78,20 @@ import {
70
78
  } from "../shared/worktree.ts";
71
79
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
72
80
  import { writeInitialProgressFile } from "../../shared/settings.ts";
81
+ import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
82
+ import {
83
+ acceptanceFailureMessage,
84
+ acceptanceSelfReviewConfig,
85
+ attachFinalizationToLedger,
86
+ buildFinalizationProcessFailureLedger,
87
+ createFinalizationProcessFailureTurn,
88
+ createFinalizationTurn,
89
+ evaluateAcceptance,
90
+ formatAcceptanceFinalizationPrompt,
91
+ formatAcceptancePrompt,
92
+ shouldRunAcceptanceFinalization,
93
+ stripAcceptanceReport,
94
+ } from "../shared/acceptance.ts";
73
95
 
74
96
  interface SubagentRunConfig {
75
97
  id: string;
@@ -94,6 +116,8 @@ interface SubagentRunConfig {
94
116
  controlIntercomTarget?: string;
95
117
  childIntercomTargets?: Array<string | undefined>;
96
118
  resultMode?: SubagentRunMode;
119
+ dynamicFanoutMaxItems?: number;
120
+ workflowGraph?: WorkflowGraphSnapshot;
97
121
  nestedRoute?: NestedRouteInfo;
98
122
  nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> };
99
123
  }
@@ -103,6 +127,7 @@ interface StepResult {
103
127
  output: string;
104
128
  error?: string;
105
129
  success: boolean;
130
+ exitCode?: number | null;
106
131
  skipped?: boolean;
107
132
  sessionFile?: string;
108
133
  intercomTarget?: string;
@@ -111,6 +136,10 @@ interface StepResult {
111
136
  modelAttempts?: ModelAttempt[];
112
137
  artifactPaths?: ArtifactPaths;
113
138
  truncated?: boolean;
139
+ structuredOutput?: unknown;
140
+ structuredOutputPath?: string;
141
+ structuredOutputSchemaPath?: string;
142
+ acceptance?: AcceptanceLedger;
114
143
  }
115
144
 
116
145
  const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
@@ -543,6 +572,7 @@ function writeRunLog(
543
572
  /** Context for running a single step */
544
573
  interface SingleStepContext {
545
574
  previousOutput: string;
575
+ outputs?: ChainOutputMap;
546
576
  placeholder: string;
547
577
  cwd: string;
548
578
  sessionEnabled: boolean;
@@ -580,9 +610,22 @@ async function runSingleStep(
580
610
  sessionFile?: string;
581
611
  intercomTarget?: string;
582
612
  completionGuardTriggered?: boolean;
613
+ structuredOutput?: unknown;
614
+ structuredOutputPath?: string;
615
+ structuredOutputSchemaPath?: string;
616
+ acceptance?: AcceptanceLedger;
583
617
  }> {
618
+ const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
619
+ ? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"))
620
+ : undefined);
584
621
  const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
585
- const task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
622
+ let task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
623
+ task = resolveOutputReferences(task, ctx.outputs ?? {});
624
+ const taskForCompletionGuard = task;
625
+ if (step.effectiveAcceptance) {
626
+ const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance);
627
+ if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
628
+ }
586
629
  const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
587
630
  const sessionDir = step.sessionFile ? undefined : ctx.sessionDir;
588
631
 
@@ -613,6 +656,13 @@ async function runSingleStep(
613
656
  const candidate = candidates[index];
614
657
  ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) });
615
658
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
659
+ if (effectiveStructuredOutput) {
660
+ try {
661
+ if (fs.existsSync(effectiveStructuredOutput.outputPath)) fs.unlinkSync(effectiveStructuredOutput.outputPath);
662
+ } catch {
663
+ // Missing/stale structured-output files are handled after the child exits.
664
+ }
665
+ }
616
666
  const { args, env, tempDir } = buildPiArgs({
617
667
  baseArgs: ["--mode", "json", "-p"],
618
668
  task,
@@ -638,6 +688,7 @@ async function runSingleStep(
638
688
  parentControlInbox: ctx.nestedRoute?.controlInbox,
639
689
  parentRootRunId: ctx.nestedRoute?.rootRunId,
640
690
  parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
691
+ structuredOutput: effectiveStructuredOutput,
641
692
  });
642
693
  const run = await runPiStreaming(
643
694
  args,
@@ -654,10 +705,29 @@ async function runSingleStep(
654
705
  cleanupTempDir(tempDir);
655
706
 
656
707
  const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
657
- const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && step.completionGuard !== false
708
+ let structuredOutput: unknown;
709
+ let structuredError: string | undefined;
710
+ if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError) {
711
+ const structured = readStructuredOutput({
712
+ schema: effectiveStructuredOutput.schema,
713
+ schemaPath: effectiveStructuredOutput.schemaPath,
714
+ outputPath: effectiveStructuredOutput.outputPath,
715
+ });
716
+ if (structured.error) structuredError = structured.error;
717
+ else structuredOutput = structured.value;
718
+ }
719
+ const completionPolicy = resolveCompletionPolicy({
720
+ agent: step.agent,
721
+ task: taskForCompletionGuard,
722
+ completionGuardEnabled: step.completionGuard !== false,
723
+ usesAcceptanceContract: step.effectiveAcceptance?.explicit === true,
724
+ tools: step.tools,
725
+ mcpDirectTools: step.mcpDirectTools,
726
+ });
727
+ const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && completionPolicy === "mutation-guard"
658
728
  ? evaluateCompletionMutationGuard({
659
729
  agent: step.agent,
660
- task,
730
+ task: taskForCompletionGuard,
661
731
  messages: run.messages,
662
732
  tools: step.tools,
663
733
  mcpDirectTools: step.mcpDirectTools,
@@ -667,14 +737,17 @@ async function runSingleStep(
667
737
  const completionGuardError = completionGuardTriggered
668
738
  ? "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes."
669
739
  : undefined;
670
- const effectiveExitCode = completionGuardTriggered
740
+ const effectiveExitCode = completionGuardError
671
741
  ? 1
672
- : hiddenError?.hasError
742
+ : structuredError
743
+ ? 1
744
+ : hiddenError?.hasError
673
745
  ? (hiddenError.exitCode ?? 1)
674
746
  : run.error && run.exitCode === 0
675
747
  ? 1
676
748
  : run.exitCode;
677
749
  const error = completionGuardError
750
+ ?? structuredError
678
751
  ?? (hiddenError?.hasError
679
752
  ? hiddenError.details
680
753
  ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
@@ -691,22 +764,24 @@ async function runSingleStep(
691
764
  if (candidate) attemptedModels.push(candidate);
692
765
  completionGuardTriggeredFinal = completionGuardTriggered;
693
766
  finalOutputSnapshot = outputSnapshot;
694
- finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error };
695
- if (attempt.success || completionGuardTriggered) break;
767
+ finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
768
+ if (attempt.success || completionGuardError) break;
696
769
  if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
697
770
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
698
771
  }
699
772
 
700
773
  const rawOutput = finalResult?.finalOutput ?? "";
774
+ const outputForPersistence = stripAcceptanceReport(rawOutput);
701
775
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
702
- ? resolveSingleOutput(step.outputPath, rawOutput, finalOutputSnapshot)
703
- : { fullOutput: rawOutput };
776
+ ? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot)
777
+ : { fullOutput: outputForPersistence };
704
778
  const output = resolvedOutput.fullOutput;
705
779
  const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
706
780
  let outputForSummary = output;
707
781
  if (attemptNotes.length > 0) {
708
782
  outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
709
783
  }
784
+ const outputForAcceptance = rawOutput;
710
785
  const finalizedOutput = finalizeSingleOutput({
711
786
  fullOutput: outputForSummary,
712
787
  outputPath: step.outputPath,
@@ -717,6 +792,121 @@ async function runSingleStep(
717
792
  saveError: resolvedOutput.saveError,
718
793
  });
719
794
  outputForSummary = finalizedOutput.displayOutput;
795
+ const acceptanceForInitialReport = step.effectiveAcceptance && shouldRunAcceptanceFinalization(step.effectiveAcceptance)
796
+ ? acceptanceSelfReviewConfig(step.effectiveAcceptance)
797
+ : step.effectiveAcceptance;
798
+ let acceptance = acceptanceForInitialReport
799
+ ? await evaluateAcceptance({
800
+ acceptance: acceptanceForInitialReport,
801
+ output: outputForAcceptance,
802
+ cwd: step.cwd ?? ctx.cwd,
803
+ })
804
+ : undefined;
805
+ if (acceptance && step.effectiveAcceptance && shouldRunAcceptanceFinalization(step.effectiveAcceptance) && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted) {
806
+ const sessionFile = step.sessionFile ?? (sessionDir ? findLatestSessionFile(sessionDir) ?? undefined : undefined);
807
+ const maxTurns = step.effectiveAcceptance.finalization.maxTurns;
808
+ const turns: AcceptanceFinalizationTurn[] = [];
809
+ if (!sessionFile) {
810
+ const message = "Acceptance finalization requires a session file for same-session continuation.";
811
+ turns.push(createFinalizationProcessFailureTurn({ turn: 1, prompt: "", message }));
812
+ acceptance = buildFinalizationProcessFailureLedger({ initialLedger: acceptance, turns, maxTurns, message });
813
+ } else {
814
+ const selfReviewAcceptance = acceptanceSelfReviewConfig(step.effectiveAcceptance);
815
+ let previousFailure = acceptanceFailureMessage(acceptance);
816
+ let authoritativeLedger = acceptance;
817
+ for (let turn = 1; turn <= maxTurns; turn++) {
818
+ const prompt = formatAcceptanceFinalizationPrompt({
819
+ acceptance: step.effectiveAcceptance,
820
+ initialOutput: outputForAcceptance,
821
+ initialLedger: acceptance,
822
+ turn,
823
+ maxTurns,
824
+ ...(previousFailure ? { previousFailure } : {}),
825
+ });
826
+ const { args, env, tempDir } = buildPiArgs({
827
+ baseArgs: ["--mode", "json", "-p"],
828
+ task: prompt,
829
+ sessionEnabled: true,
830
+ sessionFile,
831
+ model: finalResult?.model ?? step.model,
832
+ thinking: step.thinking,
833
+ inheritProjectContext: step.inheritProjectContext,
834
+ inheritSkills: step.inheritSkills,
835
+ tools: step.tools,
836
+ extensions: step.extensions,
837
+ systemPrompt: step.systemPrompt,
838
+ systemPromptMode: step.systemPromptMode,
839
+ mcpDirectTools: step.mcpDirectTools,
840
+ cwd: step.cwd ?? ctx.cwd,
841
+ promptFileStem: `${step.agent}-acceptance-finalization`,
842
+ intercomSessionName: ctx.childIntercomTarget,
843
+ orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
844
+ runId: ctx.id,
845
+ childAgentName: step.agent,
846
+ childIndex: ctx.flatIndex,
847
+ parentEventSink: ctx.nestedRoute?.eventSink,
848
+ parentControlInbox: ctx.nestedRoute?.controlInbox,
849
+ parentRootRunId: ctx.nestedRoute?.rootRunId,
850
+ parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
851
+ });
852
+ ctx.onAttemptStart?.({ model: finalResult?.model ?? step.model, thinking: resolveEffectiveThinking(finalResult?.model ?? step.model, step.thinking) });
853
+ const finalizationRun = await runPiStreaming(
854
+ args,
855
+ step.cwd ?? ctx.cwd,
856
+ `${ctx.outputFile}.finalization-${turn}.log`,
857
+ env,
858
+ ctx.piPackageRoot,
859
+ ctx.piArgv1,
860
+ step.maxSubagentDepth,
861
+ { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
862
+ ctx.registerInterrupt,
863
+ ctx.onChildEvent,
864
+ );
865
+ cleanupTempDir(tempDir);
866
+ modelAttempts.push({
867
+ model: finalResult?.model ?? finalizationRun.model ?? step.model ?? "default",
868
+ success: finalizationRun.exitCode === 0 && !finalizationRun.error,
869
+ exitCode: finalizationRun.exitCode,
870
+ error: finalizationRun.error,
871
+ usage: finalizationRun.usage,
872
+ });
873
+ const finalizationOutput = finalizationRun.finalOutput;
874
+ if (finalizationRun.exitCode !== 0 || finalizationRun.error || finalizationRun.interrupted) {
875
+ const message = finalizationRun.error ?? "Acceptance finalization turn did not complete successfully.";
876
+ turns.push(createFinalizationProcessFailureTurn({ turn, prompt, rawOutput: finalizationOutput, message }));
877
+ acceptance = buildFinalizationProcessFailureLedger({ initialLedger: acceptance, turns, maxTurns, message });
878
+ break;
879
+ }
880
+ const selfReviewLedger = await evaluateAcceptance({
881
+ acceptance: selfReviewAcceptance,
882
+ output: finalizationOutput,
883
+ cwd: step.cwd ?? ctx.cwd,
884
+ });
885
+ authoritativeLedger = selfReviewLedger;
886
+ turns.push(createFinalizationTurn({ turn, prompt, rawOutput: finalizationOutput, ledger: selfReviewLedger }));
887
+ const failure = acceptanceFailureMessage(selfReviewLedger);
888
+ if (!failure) {
889
+ authoritativeLedger = step.effectiveAcceptance === selfReviewAcceptance
890
+ ? selfReviewLedger
891
+ : await evaluateAcceptance({
892
+ acceptance: step.effectiveAcceptance,
893
+ output: finalizationOutput,
894
+ cwd: step.cwd ?? ctx.cwd,
895
+ });
896
+ acceptance = attachFinalizationToLedger({ initialLedger: acceptance, authoritativeLedger, turns, status: "completed", maxTurns });
897
+ break;
898
+ }
899
+ previousFailure = failure;
900
+ if (turn === maxTurns) acceptance = attachFinalizationToLedger({ initialLedger: acceptance, authoritativeLedger, turns, status: "failed", maxTurns });
901
+ }
902
+ }
903
+ }
904
+ const acceptanceFailure = acceptance ? acceptanceFailureMessage(acceptance) : undefined;
905
+ const acceptanceCanFailRun = acceptanceFailure && acceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted;
906
+ const effectiveFinalExitCode = acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
907
+ const effectiveFinalError = acceptanceCanFailRun
908
+ ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
909
+ : finalResult?.error;
720
910
 
721
911
  if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
722
912
  if (ctx.artifactConfig?.includeOutput !== false) {
@@ -729,7 +919,7 @@ async function runSingleStep(
729
919
  runId: ctx.id,
730
920
  agent: step.agent,
731
921
  task,
732
- exitCode: finalResult?.exitCode,
922
+ exitCode: effectiveFinalExitCode,
733
923
  model: finalResult?.model,
734
924
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
735
925
  modelAttempts,
@@ -744,8 +934,8 @@ async function runSingleStep(
744
934
  return {
745
935
  agent: step.agent,
746
936
  output: outputForSummary,
747
- exitCode: finalResult?.exitCode ?? 1,
748
- error: finalResult?.error,
937
+ exitCode: effectiveFinalExitCode,
938
+ error: effectiveFinalError,
749
939
  sessionFile: step.sessionFile,
750
940
  intercomTarget: ctx.childIntercomTarget,
751
941
  model: finalResult?.model,
@@ -754,6 +944,10 @@ async function runSingleStep(
754
944
  artifactPaths,
755
945
  interrupted: finalResult?.interrupted,
756
946
  completionGuardTriggered: completionGuardTriggeredFinal,
947
+ structuredOutput: (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
948
+ structuredOutputPath: effectiveStructuredOutput?.outputPath,
949
+ structuredOutputSchemaPath: effectiveStructuredOutput?.schemaPath,
950
+ acceptance,
757
951
  };
758
952
  }
759
953
 
@@ -796,7 +990,7 @@ function markParallelGroupSetupFailure(input: {
796
990
  input.statusPayload.steps[flatTaskIndex].endedAt = input.failedAt;
797
991
  input.statusPayload.steps[flatTaskIndex].durationMs = 0;
798
992
  input.statusPayload.steps[flatTaskIndex].exitCode = 1;
799
- input.results.push({ agent: input.group.parallel[taskIndex].agent, output: input.setupError, success: false, sessionFile: input.group.parallel[taskIndex].sessionFile });
993
+ input.results.push({ agent: input.group.parallel[taskIndex].agent, output: input.setupError, success: false, exitCode: 1, sessionFile: input.group.parallel[taskIndex].sessionFile });
800
994
  }
801
995
  input.statusPayload.currentStep = input.groupStartFlatIndex;
802
996
  input.statusPayload.lastUpdate = input.failedAt;
@@ -886,6 +1080,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
886
1080
  const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
887
1081
  config;
888
1082
  let previousOutput = "";
1083
+ const outputs: ChainOutputMap = {};
889
1084
  const results: StepResult[] = [];
890
1085
  const overallStartTime = Date.now();
891
1086
  const shareEnabled = config.share === true;
@@ -902,13 +1097,59 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
902
1097
  let latestSessionFile: string | undefined;
903
1098
 
904
1099
  const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
1100
+ const initialStatusSteps: RunnerStatusStep[] = [];
905
1101
  let flatStepCount = 0;
906
1102
  for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
907
1103
  const step = steps[stepIndex]!;
908
1104
  if (isParallelGroup(step)) {
909
1105
  parallelGroups.push({ start: flatStepCount, count: step.parallel.length, stepIndex });
1106
+ for (const task of step.parallel) {
1107
+ initialStatusSteps.push({
1108
+ agent: task.agent,
1109
+ phase: task.phase,
1110
+ label: task.label,
1111
+ outputName: task.outputName,
1112
+ structured: task.structured,
1113
+ status: "pending",
1114
+ ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
1115
+ skills: task.skills,
1116
+ model: task.model,
1117
+ thinking: task.thinking,
1118
+ attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
1119
+ recentTools: [],
1120
+ recentOutput: [],
1121
+ });
1122
+ }
910
1123
  flatStepCount += step.parallel.length;
1124
+ } else if (isDynamicRunnerGroup(step)) {
1125
+ parallelGroups.push({ start: flatStepCount, count: 1, stepIndex });
1126
+ initialStatusSteps.push({
1127
+ agent: `expand:${step.parallel.agent}`,
1128
+ phase: step.phase ?? step.parallel.phase,
1129
+ label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
1130
+ outputName: step.collect.as,
1131
+ structured: Boolean(step.collect.outputSchema),
1132
+ status: "pending",
1133
+ recentTools: [],
1134
+ recentOutput: [],
1135
+ });
1136
+ flatStepCount++;
911
1137
  } else {
1138
+ initialStatusSteps.push({
1139
+ agent: step.agent,
1140
+ phase: step.phase,
1141
+ label: step.label,
1142
+ outputName: step.outputName,
1143
+ structured: step.structured,
1144
+ status: "pending",
1145
+ ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
1146
+ skills: step.skills,
1147
+ model: step.model,
1148
+ thinking: step.thinking,
1149
+ attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
1150
+ recentTools: [],
1151
+ recentOutput: [],
1152
+ });
912
1153
  flatStepCount++;
913
1154
  }
914
1155
  }
@@ -929,17 +1170,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
929
1170
  currentStep: 0,
930
1171
  chainStepCount: steps.length,
931
1172
  parallelGroups,
932
- steps: flatSteps.map((step) => ({
933
- agent: step.agent,
934
- status: "pending",
935
- ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
936
- skills: step.skills,
937
- model: step.model,
938
- thinking: step.thinking,
939
- attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
940
- recentTools: [],
941
- recentOutput: [],
942
- })),
1173
+ workflowGraph: config.workflowGraph,
1174
+ steps: initialStatusSteps,
943
1175
  artifactsDir,
944
1176
  sessionDir: config.sessionDir,
945
1177
  outputFile: path.join(asyncDir, "output-0.log"),
@@ -969,10 +1201,48 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
969
1201
  console.error("Failed to emit nested async status event:", error);
970
1202
  }
971
1203
  };
1204
+ const refreshWorkflowGraph = (): void => {
1205
+ if (!config.workflowGraph) return;
1206
+ const graph = structuredClone(statusPayload.workflowGraph ?? config.workflowGraph);
1207
+ const normalize = (status: RunnerStatusStep["status"]): "pending" | "running" | "completed" | "failed" | "paused" | "detached" => {
1208
+ if (status === "complete" || status === "completed") return "completed";
1209
+ if (status === "running" || status === "failed" || status === "paused" || status === "pending") return status;
1210
+ return "pending";
1211
+ };
1212
+ const updateNode = (node: NonNullable<typeof graph.nodes>[number]): void => {
1213
+ if (node.flatIndex !== undefined) {
1214
+ const step = statusPayload.steps[node.flatIndex];
1215
+ if (step) {
1216
+ node.status = normalize(step.status);
1217
+ node.error = step.error;
1218
+ node.acceptanceStatus = step.acceptance?.status;
1219
+ }
1220
+ if (statusPayload.currentStep === node.flatIndex) graph.currentNodeId = node.id;
1221
+ }
1222
+ for (const child of node.children ?? []) updateNode(child);
1223
+ if (node.children?.length) {
1224
+ if (node.children.every((child) => child.status === "completed")) node.status = "completed";
1225
+ else if (node.children.some((child) => child.status === "running")) node.status = "running";
1226
+ else if (node.children.some((child) => child.status === "failed")) node.status = "failed";
1227
+ else if (node.children.some((child) => child.status === "paused")) node.status = "paused";
1228
+ }
1229
+ if (node.error) node.status = "failed";
1230
+ };
1231
+ for (const node of graph.nodes) updateNode(node);
1232
+ statusPayload.workflowGraph = graph;
1233
+ };
972
1234
  const writeStatusPayload = (): void => {
1235
+ refreshWorkflowGraph();
973
1236
  writeAtomicJson(statusPath, statusPayload);
974
1237
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
975
1238
  };
1239
+ const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running", error?: string, acceptance?: AcceptanceLedger): void => {
1240
+ const groupNode = statusPayload.workflowGraph?.nodes.find((node) => node.id === `step-${stepIndex}`);
1241
+ if (!groupNode) return;
1242
+ groupNode.status = status;
1243
+ groupNode.error = error;
1244
+ groupNode.acceptanceStatus = acceptance?.status ?? groupNode.acceptanceStatus;
1245
+ };
976
1246
 
977
1247
  const stepOutputActivityAt = (index: number): number => {
978
1248
  const step = statusPayload.steps[index];
@@ -989,8 +1259,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
989
1259
  };
990
1260
  const emittedControlEventKeys = new Set<string>();
991
1261
  const activeLongRunningSteps = new Set<number>();
992
- const mutatingFailureStates = flatSteps.map(() => createMutatingFailureState());
993
- const pendingToolResults: Array<{ tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined> = [];
1262
+ const mutatingFailureStates = initialStatusSteps.map(() => createMutatingFailureState());
1263
+ const pendingToolResults: Array<{ tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined> = initialStatusSteps.map(() => undefined);
994
1264
  const mutatingFailureWindowMs = 5 * 60_000;
995
1265
  const appendControlEvent = (event: ReturnType<typeof buildControlEvent>) => {
996
1266
  if (!controlConfig.enabled) return;
@@ -1131,7 +1401,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1131
1401
  resetMutatingFailureState(mutatingFailureStates[flatIndex]!);
1132
1402
  }
1133
1403
  } else if (event.type === "message_end" && event.message?.role === "assistant") {
1134
- appendRecentStepOutput(step, extractTextFromContent(event.message.content).split("\n").slice(-10));
1404
+ appendRecentStepOutput(step, stripAcceptanceReport(extractTextFromContent(event.message.content)).split("\n").slice(-10));
1135
1405
  step.turnCount = (step.turnCount ?? 0) + 1;
1136
1406
  const usage = event.message.usage;
1137
1407
  if (usage) {
@@ -1262,6 +1532,262 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1262
1532
  if (interrupted) break;
1263
1533
  const step = steps[stepIndex];
1264
1534
 
1535
+ if (isDynamicRunnerGroup(step)) {
1536
+ const groupStartFlatIndex = flatIndex;
1537
+ let materialized: ReturnType<typeof materializeDynamicParallelStep>;
1538
+ try {
1539
+ materialized = materializeDynamicParallelStep(step as Parameters<typeof materializeDynamicParallelStep>[0], outputs, stepIndex, { maxItems: config.dynamicFanoutMaxItems, allowRunnerFields: true });
1540
+ if (materialized.collectedOnEmpty) validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
1541
+ } catch (error) {
1542
+ const now = Date.now();
1543
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
1544
+ statusPayload.state = "failed";
1545
+ statusPayload.error = message;
1546
+ statusPayload.currentStep = flatIndex;
1547
+ const placeholder = statusPayload.steps[groupStartFlatIndex];
1548
+ if (placeholder) {
1549
+ placeholder.status = "failed";
1550
+ placeholder.error = message;
1551
+ placeholder.startedAt = now;
1552
+ placeholder.endedAt = now;
1553
+ placeholder.durationMs = 0;
1554
+ placeholder.exitCode = 1;
1555
+ }
1556
+ statusPayload.lastUpdate = now;
1557
+ markDynamicGraphGroup(stepIndex, "failed", message);
1558
+ writeStatusPayload();
1559
+ results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1 });
1560
+ break;
1561
+ }
1562
+
1563
+ if (materialized.parallel.length === 0) {
1564
+ const now = Date.now();
1565
+ const collection = materialized.collectedOnEmpty ?? [];
1566
+ outputs[step.collect.as] = {
1567
+ text: JSON.stringify(collection),
1568
+ structured: collection,
1569
+ agent: step.parallel.agent,
1570
+ stepIndex,
1571
+ };
1572
+ statusPayload.outputs = outputs;
1573
+ const placeholder = statusPayload.steps[groupStartFlatIndex];
1574
+ if (placeholder) {
1575
+ placeholder.status = "complete";
1576
+ placeholder.startedAt = now;
1577
+ placeholder.endedAt = now;
1578
+ placeholder.durationMs = 0;
1579
+ }
1580
+ previousOutput = "Dynamic fanout produced 0 results.";
1581
+ flatIndex++;
1582
+ statusPayload.lastUpdate = now;
1583
+ markDynamicGraphGroup(stepIndex, "completed");
1584
+ writeStatusPayload();
1585
+ continue;
1586
+ }
1587
+
1588
+ const dynamicSteps = materialized.parallel.map((task, itemIndex) => ({
1589
+ ...step.parallel,
1590
+ task: task.task ?? step.parallel.task,
1591
+ label: task.label ?? step.parallel.label,
1592
+ structuredOutput: undefined,
1593
+ structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
1594
+ }));
1595
+ const dynamicStatusSteps: RunnerStatusStep[] = dynamicSteps.map((task) => ({
1596
+ agent: task.agent,
1597
+ phase: task.phase ?? step.phase,
1598
+ label: task.label,
1599
+ outputName: undefined,
1600
+ structured: Boolean(task.structuredOutputSchema),
1601
+ status: "pending",
1602
+ ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
1603
+ skills: task.skills,
1604
+ model: task.model,
1605
+ thinking: task.thinking,
1606
+ attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
1607
+ recentTools: [],
1608
+ recentOutput: [],
1609
+ }));
1610
+ statusPayload.steps.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps);
1611
+ if (config.childIntercomTargets) {
1612
+ config.childIntercomTargets = statusPayload.steps.map((statusStep, index) => resolveSubagentIntercomTarget(id, statusStep.agent, index));
1613
+ }
1614
+ mutatingFailureStates.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps.map(() => createMutatingFailureState()));
1615
+ pendingToolResults.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps.map(() => undefined));
1616
+ const materializedDelta = dynamicStatusSteps.length - 1;
1617
+ for (const group of statusPayload.parallelGroups) {
1618
+ if (group.stepIndex === stepIndex) {
1619
+ group.start = groupStartFlatIndex;
1620
+ group.count = dynamicStatusSteps.length;
1621
+ } else if (group.start > groupStartFlatIndex) {
1622
+ group.start += materializedDelta;
1623
+ }
1624
+ }
1625
+ if (statusPayload.workflowGraph) {
1626
+ const shiftFlatIndexes = (nodes: NonNullable<typeof statusPayload.workflowGraph>["nodes"]): void => {
1627
+ for (const node of nodes) {
1628
+ if (node.stepIndex !== undefined && node.stepIndex > stepIndex && node.flatIndex !== undefined && node.flatIndex >= groupStartFlatIndex) {
1629
+ node.flatIndex += dynamicStatusSteps.length;
1630
+ }
1631
+ if (node.children) shiftFlatIndexes(node.children);
1632
+ }
1633
+ };
1634
+ shiftFlatIndexes(statusPayload.workflowGraph.nodes);
1635
+ const groupNode = statusPayload.workflowGraph.nodes.find((node) => node.id === `step-${stepIndex}`);
1636
+ if (groupNode) {
1637
+ groupNode.children = materialized.items.map((item, itemIndex) => ({
1638
+ id: `step-${stepIndex}-item-${item.idKey}`,
1639
+ kind: "agent",
1640
+ agent: step.parallel.agent,
1641
+ phase: dynamicSteps[itemIndex]?.phase ?? step.phase,
1642
+ label: dynamicSteps[itemIndex]?.label?.trim() || `${step.parallel.agent} ${item.key}`,
1643
+ status: "pending",
1644
+ flatIndex: groupStartFlatIndex + itemIndex,
1645
+ stepIndex,
1646
+ itemKey: item.key,
1647
+ structured: Boolean(dynamicSteps[itemIndex]?.structuredOutputSchema),
1648
+ }));
1649
+ }
1650
+ }
1651
+ writeStatusPayload();
1652
+
1653
+ const concurrency = step.concurrency ?? MAX_PARALLEL_CONCURRENCY;
1654
+ const failFast = step.failFast ?? false;
1655
+ let aborted = false;
1656
+ const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
1657
+ const fi = groupStartFlatIndex + taskIdx;
1658
+ if (aborted && failFast) {
1659
+ const skippedAt = Date.now();
1660
+ statusPayload.steps[fi].status = "failed";
1661
+ statusPayload.steps[fi].error = "Skipped due to fail-fast";
1662
+ statusPayload.steps[fi].startedAt = skippedAt;
1663
+ statusPayload.steps[fi].endedAt = skippedAt;
1664
+ statusPayload.steps[fi].durationMs = 0;
1665
+ statusPayload.steps[fi].exitCode = -1;
1666
+ statusPayload.lastUpdate = skippedAt;
1667
+ writeStatusPayload();
1668
+ return { agent: task.agent, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
1669
+ }
1670
+ const taskStartTime = Date.now();
1671
+ statusPayload.currentStep = fi;
1672
+ statusPayload.steps[fi].status = "running";
1673
+ statusPayload.steps[fi].error = undefined;
1674
+ statusPayload.steps[fi].activityState = undefined;
1675
+ resetStepLiveDetail(statusPayload.steps[fi]);
1676
+ statusPayload.steps[fi].startedAt = taskStartTime;
1677
+ statusPayload.steps[fi].lastActivityAt = taskStartTime;
1678
+ statusPayload.outputFile = path.join(asyncDir, `output-${fi}.log`);
1679
+ statusPayload.lastActivityAt = taskStartTime;
1680
+ statusPayload.lastUpdate = taskStartTime;
1681
+ writeStatusPayload();
1682
+ appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent }));
1683
+ const singleResult = await runSingleStep(task, {
1684
+ previousOutput, placeholder, cwd, sessionEnabled,
1685
+ outputs,
1686
+ sessionDir: config.sessionDir ? path.join(config.sessionDir, `dynamic-${stepIndex}-${taskIdx}`) : undefined,
1687
+ artifactsDir, artifactConfig, id,
1688
+ flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
1689
+ outputFile: path.join(asyncDir, `output-${fi}.log`),
1690
+ piPackageRoot: config.piPackageRoot,
1691
+ piArgv1: config.piArgv1,
1692
+ childIntercomTarget: config.childIntercomTargets?.[fi],
1693
+ orchestratorIntercomTarget: config.controlIntercomTarget,
1694
+ nestedRoute: config.nestedRoute,
1695
+ registerInterrupt: (interrupt) => {
1696
+ activeChildInterrupt = interrupt;
1697
+ },
1698
+ onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
1699
+ onChildEvent: (event) => updateStepFromChildEvent(fi, event),
1700
+ });
1701
+ const taskEndTime = Date.now();
1702
+ statusPayload.steps[fi].status = singleResult.exitCode === 0 ? "complete" : "failed";
1703
+ statusPayload.steps[fi].endedAt = taskEndTime;
1704
+ statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
1705
+ statusPayload.steps[fi].exitCode = singleResult.exitCode;
1706
+ statusPayload.steps[fi].model = singleResult.model;
1707
+ statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
1708
+ statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
1709
+ statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
1710
+ statusPayload.steps[fi].error = singleResult.error;
1711
+ statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
1712
+ statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
1713
+ statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
1714
+ statusPayload.steps[fi].acceptance = singleResult.acceptance;
1715
+ statusPayload.lastUpdate = taskEndTime;
1716
+ writeStatusPayload();
1717
+ appendJsonl(eventsPath, JSON.stringify({
1718
+ type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
1719
+ ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
1720
+ exitCode: singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
1721
+ }));
1722
+ if (singleResult.exitCode !== 0 && failFast) aborted = true;
1723
+ return { ...singleResult, skipped: false };
1724
+ });
1725
+
1726
+ flatIndex += dynamicSteps.length;
1727
+ for (const pr of parallelResults) {
1728
+ results.push({
1729
+ agent: pr.agent,
1730
+ output: pr.output,
1731
+ error: pr.error,
1732
+ success: pr.exitCode === 0,
1733
+ exitCode: pr.exitCode,
1734
+ skipped: pr.skipped,
1735
+ sessionFile: pr.sessionFile,
1736
+ intercomTarget: pr.intercomTarget,
1737
+ model: pr.model,
1738
+ attemptedModels: pr.attemptedModels,
1739
+ modelAttempts: pr.modelAttempts,
1740
+ artifactPaths: pr.artifactPaths,
1741
+ structuredOutput: pr.structuredOutput,
1742
+ structuredOutputPath: pr.structuredOutputPath,
1743
+ structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
1744
+ acceptance: pr.acceptance,
1745
+ });
1746
+ }
1747
+ const collection = collectDynamicResults(step as Parameters<typeof collectDynamicResults>[0], materialized.items, parallelResults);
1748
+ const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
1749
+ if (failures.length === 0) {
1750
+ try {
1751
+ validateDynamicCollection(step.collect.outputSchema, collection);
1752
+ outputs[step.collect.as] = {
1753
+ text: JSON.stringify(collection),
1754
+ structured: collection,
1755
+ agent: step.parallel.agent,
1756
+ stepIndex,
1757
+ };
1758
+ statusPayload.outputs = outputs;
1759
+ markDynamicGraphGroup(stepIndex, "completed");
1760
+ } catch (error) {
1761
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
1762
+ results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
1763
+ statusPayload.error = message;
1764
+ markDynamicGraphGroup(stepIndex, "failed", message);
1765
+ }
1766
+ }
1767
+ previousOutput = aggregateParallelOutputs(
1768
+ parallelResults.map((r, i) => ({
1769
+ agent: r.agent,
1770
+ taskIndex: i,
1771
+ output: r.output,
1772
+ exitCode: r.exitCode,
1773
+ error: r.error,
1774
+ })),
1775
+ (i, agent) => `=== Dynamic Item ${i + 1} (${agent}, key ${materialized.items[i]?.key ?? i}) ===`,
1776
+ );
1777
+ appendJsonl(eventsPath, JSON.stringify({
1778
+ type: "subagent.dynamic.completed",
1779
+ ts: Date.now(),
1780
+ runId: id,
1781
+ stepIndex,
1782
+ success: failures.length === 0,
1783
+ }));
1784
+ if (failures.length > 0) markDynamicGraphGroup(stepIndex, "failed", failures[0]?.error ?? "Dynamic fanout child failed.");
1785
+ statusPayload.lastUpdate = Date.now();
1786
+ writeStatusPayload();
1787
+ if (failures.length > 0 || statusPayload.error) break;
1788
+ continue;
1789
+ }
1790
+
1265
1791
  if (isParallelGroup(step)) {
1266
1792
  const group = step;
1267
1793
  const concurrency = group.concurrency ?? MAX_PARALLEL_CONCURRENCY;
@@ -1379,6 +1905,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1379
1905
 
1380
1906
  const singleResult = await runSingleStep(taskForRun, {
1381
1907
  previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
1908
+ outputs,
1382
1909
  sessionDir: taskSessionDir,
1383
1910
  artifactsDir, artifactConfig, id,
1384
1911
  flatIndex: fi, flatStepCount: flatSteps.length,
@@ -1410,6 +1937,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1410
1937
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
1411
1938
  statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
1412
1939
  statusPayload.steps[fi].error = singleResult.error;
1940
+ statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
1941
+ statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
1942
+ statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
1943
+ statusPayload.steps[fi].acceptance = singleResult.acceptance;
1413
1944
  statusPayload.lastUpdate = taskEndTime;
1414
1945
  writeStatusPayload();
1415
1946
 
@@ -1463,6 +1994,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1463
1994
  output: pr.output,
1464
1995
  error: pr.error,
1465
1996
  success: pr.exitCode === 0,
1997
+ exitCode: pr.exitCode,
1466
1998
  skipped: pr.skipped,
1467
1999
  sessionFile: pr.sessionFile,
1468
2000
  intercomTarget: pr.intercomTarget,
@@ -1470,8 +2002,21 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1470
2002
  attemptedModels: pr.attemptedModels,
1471
2003
  modelAttempts: pr.modelAttempts,
1472
2004
  artifactPaths: pr.artifactPaths,
1473
- });
2005
+ structuredOutput: pr.structuredOutput,
2006
+ structuredOutputPath: pr.structuredOutputPath,
2007
+ structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
2008
+ acceptance: pr.acceptance,
2009
+ });
2010
+ }
2011
+ for (let t = 0; t < group.parallel.length; t++) {
2012
+ const outputName = group.parallel[t]?.outputName;
2013
+ if (outputName) outputs[outputName] = outputEntryFromAsyncResult({
2014
+ agent: parallelResults[t]!.agent,
2015
+ output: parallelResults[t]!.output,
2016
+ structuredOutput: parallelResults[t]!.structuredOutput,
2017
+ }, stepIndex);
1474
2018
  }
2019
+ statusPayload.outputs = outputs;
1475
2020
 
1476
2021
  previousOutput = aggregateParallelOutputs(
1477
2022
  parallelResults.map((r) => ({
@@ -1525,6 +2070,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1525
2070
 
1526
2071
  const singleResult = await runSingleStep(seqStep, {
1527
2072
  previousOutput, placeholder, cwd, sessionEnabled,
2073
+ outputs,
1528
2074
  sessionDir: config.sessionDir,
1529
2075
  artifactsDir, artifactConfig, id,
1530
2076
  flatIndex, flatStepCount: flatSteps.length,
@@ -1550,13 +2096,26 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1550
2096
  output: singleResult.output,
1551
2097
  error: singleResult.error,
1552
2098
  success: singleResult.exitCode === 0,
2099
+ exitCode: singleResult.exitCode,
1553
2100
  sessionFile: singleResult.sessionFile,
1554
2101
  intercomTarget: singleResult.intercomTarget,
1555
2102
  model: singleResult.model,
1556
2103
  attemptedModels: singleResult.attemptedModels,
1557
2104
  modelAttempts: singleResult.modelAttempts,
1558
2105
  artifactPaths: singleResult.artifactPaths,
2106
+ structuredOutput: singleResult.structuredOutput,
2107
+ structuredOutputPath: singleResult.structuredOutputPath,
2108
+ structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
2109
+ acceptance: singleResult.acceptance,
1559
2110
  });
2111
+ if (seqStep.outputName) {
2112
+ outputs[seqStep.outputName] = outputEntryFromAsyncResult({
2113
+ agent: singleResult.agent,
2114
+ output: singleResult.output,
2115
+ structuredOutput: singleResult.structuredOutput,
2116
+ }, stepIndex);
2117
+ }
2118
+ statusPayload.outputs = outputs;
1560
2119
 
1561
2120
  const cumulativeTokens = config.sessionDir ? parseSessionTokens(config.sessionDir) : null;
1562
2121
  let stepTokens: TokenUsage | null = cumulativeTokens
@@ -1589,6 +2148,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1589
2148
  statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
1590
2149
  statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
1591
2150
  statusPayload.steps[flatIndex].error = singleResult.error;
2151
+ statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
2152
+ statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
2153
+ statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
2154
+ statusPayload.steps[flatIndex].acceptance = singleResult.acceptance;
1592
2155
  if (stepTokens) {
1593
2156
  statusPayload.steps[flatIndex].tokens = stepTokens;
1594
2157
  statusPayload.totalTokens = { ...previousCumulativeTokens };
@@ -1690,7 +2253,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1690
2253
  statusPayload.shareUrl = shareUrl;
1691
2254
  statusPayload.gistUrl = gistUrl;
1692
2255
  statusPayload.shareError = shareError;
1693
- if (statusPayload.state === "failed") {
2256
+ if (statusPayload.state === "failed" && !statusPayload.error) {
1694
2257
  const failedStep = statusPayload.steps.find((s) => s.status === "failed");
1695
2258
  if (failedStep?.agent) {
1696
2259
  statusPayload.error = `Step failed: ${failedStep.agent}`;
@@ -1747,7 +2310,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1747
2310
  modelAttempts: r.modelAttempts,
1748
2311
  artifactPaths: r.artifactPaths,
1749
2312
  truncated: r.truncated,
2313
+ structuredOutput: r.structuredOutput,
2314
+ structuredOutputPath: r.structuredOutputPath,
2315
+ structuredOutputSchemaPath: r.structuredOutputSchemaPath,
2316
+ acceptance: r.acceptance,
1750
2317
  })),
2318
+ outputs,
2319
+ workflowGraph: statusPayload.workflowGraph,
1751
2320
  exitCode: interrupted || results.every((r) => r.success) ? 0 : 1,
1752
2321
  timestamp: runEndedAt,
1753
2322
  durationMs: runEndedAt - overallStartTime,