pi-cohort 7.3.0 → 7.4.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.
@@ -38,6 +38,7 @@ import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolve
38
38
  import { recordRun } from "../shared/run-history.ts";
39
39
  import {
40
40
  cleanupWorktrees,
41
+ removeTrees,
41
42
  createWorktrees,
42
43
  diffWorktrees,
43
44
  findWorktreeTaskCwdConflict,
@@ -64,7 +65,7 @@ import { resolveModelCandidate } from "../shared/model-fallback.ts";
64
65
  import { applyThinkingSuffix } from "../shared/pi-args.ts";
65
66
  import { validateFileOnlyOutputMode } from "../shared/single-output.ts";
66
67
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
67
- import { ChainOutputValidationError, outputEntryFromResult, resolveOutputReferences, validateChainOutputBindings } from "../shared/chain-outputs.ts";
68
+ import { ChainOutputValidationError, TreeRegistry, outputEntryFromResult, resolveOutputReferences, validateChainOutputBindings } from "../shared/chain-outputs.ts";
68
69
  import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
69
70
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection, type DynamicCollectedResult } from "../shared/dynamic-fanout.ts";
70
71
  import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, resolveEffectiveAcceptance } from "../shared/acceptance.ts";
@@ -130,6 +131,8 @@ interface ParallelChainRunInput {
130
131
  results: SingleResult[];
131
132
  allProgress: AgentProgress[];
132
133
  outputs: ChainOutputMap;
134
+ trees: TreeRegistry;
135
+ stepCwd?: string;
133
136
  chainAgents: string[];
134
137
  chainSteps: ChainStep[];
135
138
  totalSteps: number;
@@ -236,8 +239,13 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
236
239
  );
237
240
 
238
241
  let taskStr: string;
242
+ let taskCwd: string;
239
243
  try {
240
- taskStr = resolveOutputReferences(taskTemplate, input.outputs);
244
+ const cwdTemplate = task.cwd ?? input.stepCwd;
245
+ taskCwd = input.worktreeSetup
246
+ ? input.worktreeSetup.worktrees[taskIndex]!.agentCwd
247
+ : resolveChildCwd(input.cwd ?? input.ctx.cwd, cwdTemplate === undefined ? undefined : resolveOutputReferences(cwdTemplate, input.outputs, input.trees));
248
+ taskStr = resolveOutputReferences(taskTemplate, input.outputs, input.trees);
241
249
  } catch (error) {
242
250
  if (!(error instanceof ChainOutputValidationError)) throw error;
243
251
  if (failFast) aborted = true;
@@ -263,10 +271,6 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
263
271
  ?? resolveModelCandidate(taskAgentConfig?.model, input.availableModels, input.ctx.model?.provider);
264
272
  const maxSubagentDepth = resolveChildMaxSubagentDepth(input.maxSubagentDepth, taskAgentConfig?.maxSubagentDepth);
265
273
 
266
- const taskCwd = input.worktreeSetup
267
- ? input.worktreeSetup.worktrees[taskIndex]!.agentCwd
268
- : resolveChildCwd(input.cwd ?? input.ctx.cwd, task.cwd);
269
-
270
274
  const outputPath = typeof behavior.output === "string"
271
275
  ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(input.chainDir, behavior.output))
272
276
  : undefined;
@@ -526,6 +530,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
526
530
  }
527
531
 
528
532
  const chainDir = createChainDir(runId, chainDirBase);
533
+ const trees = new TreeRegistry(chainDir);
529
534
  const hasParallelSteps = chainSteps.some((step) => isParallelStep(step) || isDynamicParallelStep(step));
530
535
  let templates: ResolvedTemplates = resolveChainTemplates(chainSteps);
531
536
  const shouldClarify = clarify === true && ctx.hasUI && !hasParallelSteps;
@@ -631,54 +636,264 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
631
636
  let globalTaskIndex = 0;
632
637
  let progressCreated = false;
633
638
 
634
- for (let stepIndex = 0; stepIndex < chainSteps.length; stepIndex++) {
635
- const step = chainSteps[stepIndex]!;
636
- const stepTemplates = templates[stepIndex]!;
637
-
638
- if (isParallelStep(step)) {
639
- const parallelTemplates = stepTemplates as string[];
640
- const parallelCwd = resolveChildCwd(cwd ?? ctx.cwd, step.cwd);
641
- let worktreeSetup: WorktreeSetup | undefined;
642
- if (step.worktree) {
643
- const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(step.parallel, parallelCwd);
644
- if (worktreeTaskCwdConflict) {
645
- return buildChainExecutionErrorResult(
646
- `parallel chain step ${stepIndex + 1}: ${formatWorktreeTaskCwdConflict(worktreeTaskCwdConflict, parallelCwd)}`,
647
- makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }),
648
- failureSummaryOptions(),
649
- );
639
+ try {
640
+ for (let stepIndex = 0; stepIndex < chainSteps.length; stepIndex++) {
641
+ const step = chainSteps[stepIndex]!;
642
+ const stepTemplates = templates[stepIndex]!;
643
+
644
+ if (isParallelStep(step)) {
645
+ const parallelTemplates = stepTemplates as string[];
646
+ const parallelCwd = resolveChildCwd(cwd ?? ctx.cwd, step.cwd);
647
+ let worktreeSetup: WorktreeSetup | undefined;
648
+ if (step.worktree) {
649
+ const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(step.parallel, parallelCwd);
650
+ if (worktreeTaskCwdConflict) {
651
+ return buildChainExecutionErrorResult(
652
+ `parallel chain step ${stepIndex + 1}: ${formatWorktreeTaskCwdConflict(worktreeTaskCwdConflict, parallelCwd)}`,
653
+ makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }),
654
+ failureSummaryOptions(),
655
+ );
656
+ }
657
+ try {
658
+ worktreeSetup = createWorktrees(parallelCwd, `${runId}-s${stepIndex}`, step.parallel.length, {
659
+ agents: step.parallel.map((task) => task.agent),
660
+ setupHook: params.worktreeSetupHook
661
+ ? { hookPath: params.worktreeSetupHook, timeoutMs: params.worktreeSetupHookTimeoutMs }
662
+ : undefined,
663
+ });
664
+ } catch (error) {
665
+ const message = error instanceof Error ? error.message : String(error);
666
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
667
+ }
650
668
  }
669
+
651
670
  try {
652
- worktreeSetup = createWorktrees(parallelCwd, `${runId}-s${stepIndex}`, step.parallel.length, {
653
- agents: step.parallel.map((task) => task.agent),
654
- setupHook: params.worktreeSetupHook
655
- ? { hookPath: params.worktreeSetupHook, timeoutMs: params.worktreeSetupHookTimeoutMs }
656
- : undefined,
671
+ const agentNames = step.parallel.map((task) => task.agent);
672
+ const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills)
673
+ .map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? step.parallel[taskIndex]?.task, originalTask));
674
+ for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
675
+ const behavior = parallelBehaviors[taskIndex]!;
676
+ const outputPath = typeof behavior.output === "string"
677
+ ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
678
+ : undefined;
679
+ const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Parallel chain step ${stepIndex + 1} task ${taskIndex + 1} (${step.parallel[taskIndex]!.agent})`);
680
+ if (validationError) return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }), failureSummaryOptions());
681
+ }
682
+ progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
683
+ createParallelDirs(chainDir, stepIndex, step.parallel.length, agentNames);
684
+
685
+ const parallelResults = await runParallelChainTasks({
686
+ runtime: params.runtime,
687
+ step,
688
+ parallelTemplates,
689
+ parallelBehaviors,
690
+ agents,
691
+ stepIndex,
692
+ availableModels,
693
+ chainDir,
694
+ prev,
695
+ originalTask,
696
+ ctx,
697
+ cwd,
698
+ runId,
699
+ globalTaskIndex,
700
+ forwardedFlags: params.forwardedFlags,
701
+ sessionDirForIndex,
702
+ sessionFileForIndex,
703
+ shareEnabled,
704
+ artifactConfig,
705
+ artifactsDir,
706
+ signal,
707
+ onUpdate,
708
+ results,
709
+ allProgress,
710
+ outputs,
711
+ trees,
712
+ stepCwd: step.cwd,
713
+ chainAgents,
714
+ chainSteps,
715
+ totalSteps,
716
+ dynamicChildren,
717
+ dynamicGroupStatuses,
718
+ controlConfig,
719
+ onControlEvent,
720
+ foregroundControl,
721
+ nestedRoute: params.nestedRoute,
722
+ worktreeSetup,
723
+ maxSubagentDepth: params.maxSubagentDepth,
724
+ });
725
+ globalTaskIndex += step.parallel.length;
726
+
727
+ for (const result of parallelResults) {
728
+ results.push(result);
729
+ if (result.progress) allProgress.push(result.progress);
730
+ if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
731
+ }
732
+ const interruptedIndexInStep = parallelResults.findIndex((result) => result.interrupted);
733
+ const interrupted = interruptedIndexInStep >= 0 ? parallelResults[interruptedIndexInStep] : undefined;
734
+ if (interrupted) {
735
+ return {
736
+ content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
737
+ details: buildChainExecutionDetails(makeDetailsInput({
738
+ currentStepIndex: stepIndex,
739
+ currentFlatIndex: globalTaskIndex - step.parallel.length + interruptedIndexInStep,
740
+ })),
741
+ };
742
+ }
743
+
744
+ const diffs = captureParallelWorktreeDiffs(worktreeSetup, path.join(chainDir, "worktree-diffs", `step-${stepIndex}`), agentNames);
745
+ for (let taskIndex = 0; taskIndex < parallelResults.length; taskIndex++) {
746
+ const result = parallelResults[taskIndex]!;
747
+ const task = step.parallel[taskIndex];
748
+ logTaskOutput(stepIndex, taskIndex, result, task?.as, task?.label);
749
+ if (task?.as && result.exitCode === 0) outputs[task.as] = outputEntryFromResult(result, stepIndex, {
750
+ ...patchInfoForDiff(diffs[taskIndex]),
751
+ ...(worktreeSetup ? { taskIndex, baseCommit: worktreeSetup.baseCommit, repoCwd: worktreeSetup.cwd } : {}),
752
+ });
753
+ }
754
+ if (step.stopOn) {
755
+ const matches = evaluateStopCondition(step.stopOn, stepIndex, parallelResults.map((result, i) => ({
756
+ agent: result.agent, as: step.parallel[i]?.as, label: step.parallel[i]?.label, exitCode: result.exitCode, structuredOutput: result.structuredOutput,
757
+ })));
758
+ if (matches.length > 0) stopRecord = buildStopRecord(step.stopOn, stepIndex, matches);
759
+ }
760
+ const failures = parallelResults
761
+ .map((result, originalIndex) => ({ ...result, originalIndex }))
762
+ .filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
763
+ if (failures.length > 0) {
764
+ const failureSummary = failures
765
+ .map((failure) => `- Task ${failure.originalIndex + 1} (${failure.agent}): ${failure.error || "failed"}`)
766
+ .join("\n");
767
+ const errorMsg = `Parallel step ${stepIndex + 1} failed:\n${failureSummary}`;
768
+ const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
769
+ index: stepIndex,
770
+ error: errorMsg,
771
+ }, failureSummaryOptions());
772
+ return {
773
+ content: [{ type: "text", text: summary }],
774
+ isError: true,
775
+ details: buildChainExecutionDetails(makeDetailsInput({
776
+ currentStepIndex: stepIndex,
777
+ currentFlatIndex: globalTaskIndex - step.parallel.length + failures[0]!.originalIndex,
778
+ })),
779
+ };
780
+ }
781
+
782
+ const taskResults: ParallelTaskResult[] = parallelResults.map((result, i) => {
783
+ const outputTarget = parallelBehaviors[i]?.output;
784
+ const outputTargetPath = typeof outputTarget === "string"
785
+ ? (path.isAbsolute(outputTarget) ? outputTarget : path.join(chainDir, outputTarget))
786
+ : undefined;
787
+ return {
788
+ agent: result.agent,
789
+ taskIndex: i,
790
+ output: getSingleResultOutput(result),
791
+ exitCode: result.exitCode,
792
+ error: result.error,
793
+ outputTargetPath,
794
+ outputTargetExists: outputTargetPath ? fs.existsSync(outputTargetPath) : undefined,
795
+ };
657
796
  });
797
+ prev = aggregateParallelOutputs(taskResults);
798
+ prev = appendParallelWorktreeSummary(prev, diffs);
799
+ if (stopRecord) {
800
+ return {
801
+ content: [{ type: "text", text: buildChainSummary(chainSteps, results, chainDir, "stopped", undefined, summaryOptions()) }],
802
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex })),
803
+ };
804
+ }
805
+
806
+ } finally {
807
+ if (worktreeSetup) cleanupWorktrees(worktreeSetup);
808
+ }
809
+ } else if (isDynamicParallelStep(step)) {
810
+ let materialized: ReturnType<typeof materializeDynamicParallelStep>;
811
+ try {
812
+ materialized = materializeDynamicParallelStep(step, outputs, stepIndex, { maxItems: params.dynamicFanoutMaxItems });
658
813
  } catch (error) {
659
- const message = error instanceof Error ? error.message : String(error);
814
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
815
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
660
816
  return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
661
817
  }
662
- }
663
818
 
664
- try {
665
- const agentNames = step.parallel.map((task) => task.agent);
666
- const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills)
667
- .map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? step.parallel[taskIndex]?.task, originalTask));
668
- for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
819
+ dynamicChildren[stepIndex] = materialized.items.map((item, itemIndex) => ({
820
+ agent: step.parallel.agent,
821
+ label: materialized.parallel[itemIndex]?.label,
822
+ flatIndex: globalTaskIndex + itemIndex,
823
+ itemKey: item.key,
824
+ structured: Boolean(step.parallel.outputSchema),
825
+ }));
826
+
827
+ if (materialized.parallel.length === 0) {
828
+ const collection: DynamicCollectedResult[] = [];
829
+ outputs[step.collect.as] = {
830
+ text: JSON.stringify(collection),
831
+ structured: collection,
832
+ agent: step.parallel.agent,
833
+ stepIndex,
834
+ };
835
+ try {
836
+ validateDynamicCollection(step.collect.outputSchema, collection);
837
+ } catch (error) {
838
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
839
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
840
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
841
+ }
842
+ dynamicGroupStatuses[stepIndex] = { status: "completed" };
843
+ if (step.acceptance !== undefined) {
844
+ const effectiveGroupAcceptance = resolveEffectiveAcceptance({
845
+ explicit: step.acceptance,
846
+ agentName: step.parallel.agent,
847
+ task: step.parallel.task ?? originalTask,
848
+ mode: "chain",
849
+ dynamicGroup: true,
850
+ });
851
+ const groupAcceptance = await evaluateAcceptance({
852
+ acceptance: effectiveGroupAcceptance,
853
+ output: "",
854
+ report: aggregateAcceptanceReport({
855
+ results: [],
856
+ notes: "Dynamic fanout produced 0 results.",
857
+ }),
858
+ cwd: cwd ?? ctx.cwd,
859
+ });
860
+ dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
861
+ const groupAcceptanceFailure = acceptanceFailureMessage(groupAcceptance);
862
+ if (groupAcceptanceFailure) {
863
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
864
+ return buildChainExecutionErrorResult(groupAcceptanceFailure, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
865
+ }
866
+ }
867
+ prev = "Dynamic fanout produced 0 results.";
868
+ continue;
869
+ }
870
+
871
+ const dynamicParallelStep: ParallelStep = {
872
+ parallel: materialized.parallel,
873
+ concurrency: step.concurrency,
874
+ failFast: step.failFast,
875
+ };
876
+ const parallelTemplates = materialized.parallel.map((task) => task.task ?? "{previous}");
877
+ const parallelBehaviors = resolveParallelBehaviors(dynamicParallelStep.parallel, agents, stepIndex, chainSkills)
878
+ .map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? dynamicParallelStep.parallel[taskIndex]?.task, originalTask));
879
+
880
+ for (let taskIndex = 0; taskIndex < dynamicParallelStep.parallel.length; taskIndex++) {
669
881
  const behavior = parallelBehaviors[taskIndex]!;
670
882
  const outputPath = typeof behavior.output === "string"
671
883
  ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
672
884
  : undefined;
673
- const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Parallel chain step ${stepIndex + 1} task ${taskIndex + 1} (${step.parallel[taskIndex]!.agent})`);
674
- if (validationError) return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }), failureSummaryOptions());
885
+ const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Dynamic chain step ${stepIndex + 1} item ${taskIndex + 1} (${dynamicParallelStep.parallel[taskIndex]!.agent})`);
886
+ if (validationError) {
887
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: validationError };
888
+ return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }), failureSummaryOptions());
889
+ }
675
890
  }
676
- progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
677
- createParallelDirs(chainDir, stepIndex, step.parallel.length, agentNames);
678
891
 
892
+ progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
893
+ createParallelDirs(chainDir, stepIndex, dynamicParallelStep.parallel.length, dynamicParallelStep.parallel.map((task) => task.agent));
679
894
  const parallelResults = await runParallelChainTasks({
680
895
  runtime: params.runtime,
681
- step,
896
+ step: dynamicParallelStep,
682
897
  parallelTemplates,
683
898
  parallelBehaviors,
684
899
  agents,
@@ -702,6 +917,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
702
917
  results,
703
918
  allProgress,
704
919
  outputs,
920
+ trees,
705
921
  chainAgents,
706
922
  chainSteps,
707
923
  totalSteps,
@@ -711,16 +927,16 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
711
927
  onControlEvent,
712
928
  foregroundControl,
713
929
  nestedRoute: params.nestedRoute,
714
- worktreeSetup,
715
930
  maxSubagentDepth: params.maxSubagentDepth,
716
931
  });
717
- globalTaskIndex += step.parallel.length;
932
+ globalTaskIndex += dynamicParallelStep.parallel.length;
718
933
 
719
934
  for (const result of parallelResults) {
720
935
  results.push(result);
721
936
  if (result.progress) allProgress.push(result.progress);
722
937
  if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
723
938
  }
939
+ const collected = collectDynamicResults(step, materialized.items, parallelResults);
724
940
  const interruptedIndexInStep = parallelResults.findIndex((result) => result.interrupted);
725
941
  const interrupted = interruptedIndexInStep >= 0 ? parallelResults[interruptedIndexInStep] : undefined;
726
942
  if (interrupted) {
@@ -728,32 +944,36 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
728
944
  content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
729
945
  details: buildChainExecutionDetails(makeDetailsInput({
730
946
  currentStepIndex: stepIndex,
731
- currentFlatIndex: globalTaskIndex - step.parallel.length + interruptedIndexInStep,
947
+ currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + interruptedIndexInStep,
732
948
  })),
733
949
  };
734
950
  }
735
-
736
- const diffs = captureParallelWorktreeDiffs(worktreeSetup, path.join(chainDir, "worktree-diffs", `step-${stepIndex}`), agentNames);
737
951
  for (let taskIndex = 0; taskIndex < parallelResults.length; taskIndex++) {
738
952
  const result = parallelResults[taskIndex]!;
739
- const task = step.parallel[taskIndex];
740
- logTaskOutput(stepIndex, taskIndex, result, task?.as, task?.label);
741
- if (task?.as && result.exitCode === 0) outputs[task.as] = outputEntryFromResult(result, stepIndex, patchInfoForDiff(diffs[taskIndex]));
953
+ const task = materialized.parallel[taskIndex];
954
+ logTaskOutput(stepIndex, taskIndex, result, undefined, task?.label);
742
955
  }
743
956
  if (step.stopOn) {
744
957
  const matches = evaluateStopCondition(step.stopOn, stepIndex, parallelResults.map((result, i) => ({
745
- agent: result.agent, as: step.parallel[i]?.as, label: step.parallel[i]?.label, exitCode: result.exitCode, structuredOutput: result.structuredOutput,
958
+ agent: result.agent, label: materialized.parallel[i]?.label, exitCode: result.exitCode, structuredOutput: result.structuredOutput,
746
959
  })));
747
960
  if (matches.length > 0) stopRecord = buildStopRecord(step.stopOn, stepIndex, matches);
748
961
  }
962
+ outputs[step.collect.as] = {
963
+ text: JSON.stringify(collected),
964
+ structured: collected,
965
+ agent: step.parallel.agent,
966
+ stepIndex,
967
+ };
749
968
  const failures = parallelResults
750
969
  .map((result, originalIndex) => ({ ...result, originalIndex }))
751
970
  .filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
752
971
  if (failures.length > 0) {
753
972
  const failureSummary = failures
754
- .map((failure) => `- Task ${failure.originalIndex + 1} (${failure.agent}): ${failure.error || "failed"}`)
973
+ .map((failure) => `- Item ${failure.originalIndex + 1} (${failure.agent}, key ${materialized.items[failure.originalIndex]?.key ?? failure.originalIndex}): ${failure.error || "failed"}`)
755
974
  .join("\n");
756
- const errorMsg = `Parallel step ${stepIndex + 1} failed:\n${failureSummary}`;
975
+ const errorMsg = `Dynamic step ${stepIndex + 1} failed:\n${failureSummary}`;
976
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: errorMsg };
757
977
  const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
758
978
  index: stepIndex,
759
979
  error: errorMsg,
@@ -763,492 +983,289 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
763
983
  isError: true,
764
984
  details: buildChainExecutionDetails(makeDetailsInput({
765
985
  currentStepIndex: stepIndex,
766
- currentFlatIndex: globalTaskIndex - step.parallel.length + failures[0]!.originalIndex,
986
+ currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + failures[0]!.originalIndex,
767
987
  })),
768
988
  };
769
989
  }
770
-
771
- const taskResults: ParallelTaskResult[] = parallelResults.map((result, i) => {
772
- const outputTarget = parallelBehaviors[i]?.output;
773
- const outputTargetPath = typeof outputTarget === "string"
774
- ? (path.isAbsolute(outputTarget) ? outputTarget : path.join(chainDir, outputTarget))
775
- : undefined;
776
- return {
777
- agent: result.agent,
778
- taskIndex: i,
779
- output: getSingleResultOutput(result),
780
- exitCode: result.exitCode,
781
- error: result.error,
782
- outputTargetPath,
783
- outputTargetExists: outputTargetPath ? fs.existsSync(outputTargetPath) : undefined,
784
- };
990
+ try {
991
+ validateDynamicCollection(step.collect.outputSchema, collected);
992
+ } catch (error) {
993
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
994
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
995
+ return buildChainExecutionErrorResult(
996
+ stopRecord ? buildChainSummary(chainSteps, results, chainDir, "failed", { index: stepIndex, error: message }, failureSummaryOptions()) : message,
997
+ makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }),
998
+ );
999
+ }
1000
+ dynamicGroupStatuses[stepIndex] = { status: "completed" };
1001
+ const effectiveGroupAcceptance = resolveEffectiveAcceptance({
1002
+ explicit: step.acceptance,
1003
+ agentName: step.parallel.agent,
1004
+ task: step.parallel.task ?? originalTask,
1005
+ mode: "chain",
1006
+ dynamicGroup: true,
1007
+ });
1008
+ const groupAcceptance = await evaluateAcceptance({
1009
+ acceptance: effectiveGroupAcceptance,
1010
+ output: "",
1011
+ report: aggregateAcceptanceReport({
1012
+ results: parallelResults,
1013
+ notes: `Dynamic fanout collected ${collected.length} result(s) into ${step.collect.as}.`,
1014
+ }),
1015
+ cwd: cwd ?? ctx.cwd,
785
1016
  });
786
- prev = aggregateParallelOutputs(taskResults);
787
- prev = appendParallelWorktreeSummary(prev, diffs);
1017
+ dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
1018
+ const groupAcceptanceFailure = acceptanceFailureMessage(groupAcceptance);
1019
+ if (groupAcceptanceFailure) {
1020
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
1021
+ return buildChainExecutionErrorResult(
1022
+ stopRecord ? buildChainSummary(chainSteps, results, chainDir, "failed", { index: stepIndex, error: groupAcceptanceFailure }, failureSummaryOptions()) : groupAcceptanceFailure,
1023
+ makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }),
1024
+ );
1025
+ }
1026
+ const taskResults: ParallelTaskResult[] = parallelResults.map((result, i) => ({
1027
+ agent: result.agent,
1028
+ taskIndex: i,
1029
+ output: getSingleResultOutput(result),
1030
+ exitCode: result.exitCode,
1031
+ error: result.error,
1032
+ }));
1033
+ prev = aggregateParallelOutputs(taskResults, (i, agent) => `=== Dynamic Item ${i + 1} (${agent}, key ${materialized.items[i]?.key ?? i}) ===`);
788
1034
  if (stopRecord) {
789
1035
  return {
790
1036
  content: [{ type: "text", text: buildChainSummary(chainSteps, results, chainDir, "stopped", undefined, summaryOptions()) }],
791
1037
  details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex })),
792
1038
  };
793
1039
  }
1040
+ } else {
1041
+ const seqStep = step as SequentialStep;
1042
+ const stepTemplate = stepTemplates as string;
794
1043
 
795
- } finally {
796
- if (worktreeSetup) cleanupWorktrees(worktreeSetup);
797
- }
798
- } else if (isDynamicParallelStep(step)) {
799
- let materialized: ReturnType<typeof materializeDynamicParallelStep>;
800
- try {
801
- materialized = materializeDynamicParallelStep(step, outputs, stepIndex, { maxItems: params.dynamicFanoutMaxItems });
802
- } catch (error) {
803
- const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
804
- dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
805
- return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
806
- }
807
-
808
- dynamicChildren[stepIndex] = materialized.items.map((item, itemIndex) => ({
809
- agent: step.parallel.agent,
810
- label: materialized.parallel[itemIndex]?.label,
811
- flatIndex: globalTaskIndex + itemIndex,
812
- itemKey: item.key,
813
- structured: Boolean(step.parallel.outputSchema),
814
- }));
1044
+ const agentConfig = agents.find((a) => a.name === seqStep.agent);
1045
+ if (!agentConfig) {
1046
+ removeChainDir(chainDir);
1047
+ return {
1048
+ content: [{ type: "text", text: `Unknown agent: ${seqStep.agent}` }],
1049
+ isError: true,
1050
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex })),
1051
+ };
1052
+ }
815
1053
 
816
- if (materialized.parallel.length === 0) {
817
- const collection: DynamicCollectedResult[] = [];
818
- outputs[step.collect.as] = {
819
- text: JSON.stringify(collection),
820
- structured: collection,
821
- agent: step.parallel.agent,
822
- stepIndex,
1054
+ const tuiOverride = tuiBehaviorOverrides?.[stepIndex];
1055
+ const stepOverride: StepOverrides = {
1056
+ output: tuiOverride?.output !== undefined ? tuiOverride.output : seqStep.output,
1057
+ outputMode: seqStep.outputMode,
1058
+ reads: tuiOverride?.reads !== undefined ? tuiOverride.reads : seqStep.reads,
1059
+ progress: tuiOverride?.progress !== undefined ? tuiOverride.progress : seqStep.progress,
1060
+ skills:
1061
+ tuiOverride?.skills !== undefined
1062
+ ? tuiOverride.skills
1063
+ : normalizeSkillInput(seqStep.skill),
823
1064
  };
1065
+ const behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfig, stepOverride, chainSkills), stepTemplate, originalTask);
1066
+
1067
+ const isFirstProgress = behavior.progress && !progressCreated;
1068
+ if (isFirstProgress) {
1069
+ progressCreated = true;
1070
+ }
1071
+
1072
+ const templateHasPrevious = stepTemplate.includes("{previous}");
1073
+ const { prefix, suffix } = buildChainInstructions(
1074
+ behavior,
1075
+ chainDir,
1076
+ isFirstProgress,
1077
+ templateHasPrevious ? undefined : prev,
1078
+ );
1079
+
1080
+ let stepTask: string;
1081
+ let stepCwd: string;
824
1082
  try {
825
- validateDynamicCollection(step.collect.outputSchema, collection);
1083
+ stepCwd = resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd === undefined ? undefined : resolveOutputReferences(seqStep.cwd, outputs, trees));
1084
+ stepTask = resolveOutputReferences(stepTemplate, outputs, trees);
826
1085
  } catch (error) {
827
- const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
828
- dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
829
- return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
830
- }
831
- dynamicGroupStatuses[stepIndex] = { status: "completed" };
832
- if (step.acceptance !== undefined) {
833
- const effectiveGroupAcceptance = resolveEffectiveAcceptance({
834
- explicit: step.acceptance,
835
- agentName: step.parallel.agent,
836
- task: step.parallel.task ?? originalTask,
837
- mode: "chain",
838
- dynamicGroup: true,
839
- });
840
- const groupAcceptance = await evaluateAcceptance({
841
- acceptance: effectiveGroupAcceptance,
842
- output: "",
843
- report: aggregateAcceptanceReport({
844
- results: [],
845
- notes: "Dynamic fanout produced 0 results.",
846
- }),
847
- cwd: cwd ?? ctx.cwd,
848
- });
849
- dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
850
- const groupAcceptanceFailure = acceptanceFailureMessage(groupAcceptance);
851
- if (groupAcceptanceFailure) {
852
- dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
853
- return buildChainExecutionErrorResult(groupAcceptanceFailure, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
854
- }
1086
+ if (!(error instanceof ChainOutputValidationError)) throw error;
1087
+ return buildChainExecutionErrorResult(error.message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
855
1088
  }
856
- prev = "Dynamic fanout produced 0 results.";
857
- continue;
858
- }
1089
+ stepTask = stepTask.replace(/\{task\}/g, originalTask);
1090
+ stepTask = stepTask.replace(/\{previous\}/g, prev);
1091
+ stepTask = stepTask.replace(/\{chain_dir\}/g, chainDir);
1092
+ const cleanTask = stepTask;
1093
+ stepTask = prefix + stepTask + suffix;
859
1094
 
860
- const dynamicParallelStep: ParallelStep = {
861
- parallel: materialized.parallel,
862
- concurrency: step.concurrency,
863
- failFast: step.failFast,
864
- };
865
- const parallelTemplates = materialized.parallel.map((task) => task.task ?? "{previous}");
866
- const parallelBehaviors = resolveParallelBehaviors(dynamicParallelStep.parallel, agents, stepIndex, chainSkills)
867
- .map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? dynamicParallelStep.parallel[taskIndex]?.task, originalTask));
1095
+ const effectiveModel =
1096
+ tuiOverride?.model
1097
+ ?? (seqStep.model ? resolveModelCandidate(seqStep.model, availableModels, ctx.model?.provider) : null)
1098
+ ?? resolveModelCandidate(agentConfig.model, availableModels, ctx.model?.provider);
868
1099
 
869
- for (let taskIndex = 0; taskIndex < dynamicParallelStep.parallel.length; taskIndex++) {
870
- const behavior = parallelBehaviors[taskIndex]!;
871
1100
  const outputPath = typeof behavior.output === "string"
872
1101
  ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
873
1102
  : undefined;
874
- const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Dynamic chain step ${stepIndex + 1} item ${taskIndex + 1} (${dynamicParallelStep.parallel[taskIndex]!.agent})`);
1103
+ const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Chain step ${stepIndex + 1} (${seqStep.agent})`);
875
1104
  if (validationError) {
876
- dynamicGroupStatuses[stepIndex] = { status: "failed", error: validationError };
877
- return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }), failureSummaryOptions());
1105
+ return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
878
1106
  }
879
- }
880
-
881
- progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
882
- createParallelDirs(chainDir, stepIndex, dynamicParallelStep.parallel.length, dynamicParallelStep.parallel.map((task) => task.agent));
883
- const parallelResults = await runParallelChainTasks({
884
- runtime: params.runtime,
885
- step: dynamicParallelStep,
886
- parallelTemplates,
887
- parallelBehaviors,
888
- agents,
889
- stepIndex,
890
- availableModels,
891
- chainDir,
892
- prev,
893
- originalTask,
894
- ctx,
895
- cwd,
896
- runId,
897
- globalTaskIndex,
898
- forwardedFlags: params.forwardedFlags,
899
- sessionDirForIndex,
900
- sessionFileForIndex,
901
- shareEnabled,
902
- artifactConfig,
903
- artifactsDir,
904
- signal,
905
- onUpdate,
906
- results,
907
- allProgress,
908
- outputs,
909
- chainAgents,
910
- chainSteps,
911
- totalSteps,
912
- dynamicChildren,
913
- dynamicGroupStatuses,
914
- controlConfig,
915
- onControlEvent,
916
- foregroundControl,
917
- nestedRoute: params.nestedRoute,
918
- maxSubagentDepth: params.maxSubagentDepth,
919
- });
920
- globalTaskIndex += dynamicParallelStep.parallel.length;
921
-
922
- for (const result of parallelResults) {
923
- results.push(result);
924
- if (result.progress) allProgress.push(result.progress);
925
- if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
926
- }
927
- const collected = collectDynamicResults(step, materialized.items, parallelResults);
928
- const interruptedIndexInStep = parallelResults.findIndex((result) => result.interrupted);
929
- const interrupted = interruptedIndexInStep >= 0 ? parallelResults[interruptedIndexInStep] : undefined;
930
- if (interrupted) {
931
- return {
932
- content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
933
- details: buildChainExecutionDetails(makeDetailsInput({
934
- currentStepIndex: stepIndex,
935
- currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + interruptedIndexInStep,
936
- })),
937
- };
938
- }
939
- for (let taskIndex = 0; taskIndex < parallelResults.length; taskIndex++) {
940
- const result = parallelResults[taskIndex]!;
941
- const task = materialized.parallel[taskIndex];
942
- logTaskOutput(stepIndex, taskIndex, result, undefined, task?.label);
943
- }
944
- if (step.stopOn) {
945
- const matches = evaluateStopCondition(step.stopOn, stepIndex, parallelResults.map((result, i) => ({
946
- agent: result.agent, label: materialized.parallel[i]?.label, exitCode: result.exitCode, structuredOutput: result.structuredOutput,
947
- })));
948
- if (matches.length > 0) stopRecord = buildStopRecord(step.stopOn, stepIndex, matches);
949
- }
950
- outputs[step.collect.as] = {
951
- text: JSON.stringify(collected),
952
- structured: collected,
953
- agent: step.parallel.agent,
954
- stepIndex,
955
- };
956
- const failures = parallelResults
957
- .map((result, originalIndex) => ({ ...result, originalIndex }))
958
- .filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
959
- if (failures.length > 0) {
960
- const failureSummary = failures
961
- .map((failure) => `- Item ${failure.originalIndex + 1} (${failure.agent}, key ${materialized.items[failure.originalIndex]?.key ?? failure.originalIndex}): ${failure.error || "failed"}`)
962
- .join("\n");
963
- const errorMsg = `Dynamic step ${stepIndex + 1} failed:\n${failureSummary}`;
964
- dynamicGroupStatuses[stepIndex] = { status: "failed", error: errorMsg };
965
- const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
966
- index: stepIndex,
967
- error: errorMsg,
968
- }, failureSummaryOptions());
969
- return {
970
- content: [{ type: "text", text: summary }],
971
- isError: true,
972
- details: buildChainExecutionDetails(makeDetailsInput({
973
- currentStepIndex: stepIndex,
974
- currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + failures[0]!.originalIndex,
975
- })),
976
- };
977
- }
978
- try {
979
- validateDynamicCollection(step.collect.outputSchema, collected);
980
- } catch (error) {
981
- const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
982
- dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
983
- return buildChainExecutionErrorResult(
984
- stopRecord ? buildChainSummary(chainSteps, results, chainDir, "failed", { index: stepIndex, error: message }, failureSummaryOptions()) : message,
985
- makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }),
986
- );
987
- }
988
- dynamicGroupStatuses[stepIndex] = { status: "completed" };
989
- const effectiveGroupAcceptance = resolveEffectiveAcceptance({
990
- explicit: step.acceptance,
991
- agentName: step.parallel.agent,
992
- task: step.parallel.task ?? originalTask,
993
- mode: "chain",
994
- dynamicGroup: true,
995
- });
996
- const groupAcceptance = await evaluateAcceptance({
997
- acceptance: effectiveGroupAcceptance,
998
- output: "",
999
- report: aggregateAcceptanceReport({
1000
- results: parallelResults,
1001
- notes: `Dynamic fanout collected ${collected.length} result(s) into ${step.collect.as}.`,
1002
- }),
1003
- cwd: cwd ?? ctx.cwd,
1004
- });
1005
- dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
1006
- const groupAcceptanceFailure = acceptanceFailureMessage(groupAcceptance);
1007
- if (groupAcceptanceFailure) {
1008
- dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
1009
- return buildChainExecutionErrorResult(
1010
- stopRecord ? buildChainSummary(chainSteps, results, chainDir, "failed", { index: stepIndex, error: groupAcceptanceFailure }, failureSummaryOptions()) : groupAcceptanceFailure,
1011
- makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }),
1012
- );
1013
- }
1014
- const taskResults: ParallelTaskResult[] = parallelResults.map((result, i) => ({
1015
- agent: result.agent,
1016
- taskIndex: i,
1017
- output: getSingleResultOutput(result),
1018
- exitCode: result.exitCode,
1019
- error: result.error,
1020
- }));
1021
- prev = aggregateParallelOutputs(taskResults, (i, agent) => `=== Dynamic Item ${i + 1} (${agent}, key ${materialized.items[i]?.key ?? i}) ===`);
1022
- if (stopRecord) {
1023
- return {
1024
- content: [{ type: "text", text: buildChainSummary(chainSteps, results, chainDir, "stopped", undefined, summaryOptions()) }],
1025
- details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex })),
1026
- };
1027
- }
1028
- } else {
1029
- const seqStep = step as SequentialStep;
1030
- const stepTemplate = stepTemplates as string;
1031
-
1032
- const agentConfig = agents.find((a) => a.name === seqStep.agent);
1033
- if (!agentConfig) {
1034
- removeChainDir(chainDir);
1035
- return {
1036
- content: [{ type: "text", text: `Unknown agent: ${seqStep.agent}` }],
1037
- isError: true,
1038
- details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex })),
1039
- };
1040
- }
1041
-
1042
- const tuiOverride = tuiBehaviorOverrides?.[stepIndex];
1043
- const stepOverride: StepOverrides = {
1044
- output: tuiOverride?.output !== undefined ? tuiOverride.output : seqStep.output,
1045
- outputMode: seqStep.outputMode,
1046
- reads: tuiOverride?.reads !== undefined ? tuiOverride.reads : seqStep.reads,
1047
- progress: tuiOverride?.progress !== undefined ? tuiOverride.progress : seqStep.progress,
1048
- skills:
1049
- tuiOverride?.skills !== undefined
1050
- ? tuiOverride.skills
1051
- : normalizeSkillInput(seqStep.skill),
1052
- };
1053
- const behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfig, stepOverride, chainSkills), stepTemplate, originalTask);
1054
-
1055
- const isFirstProgress = behavior.progress && !progressCreated;
1056
- if (isFirstProgress) {
1057
- progressCreated = true;
1058
- }
1059
-
1060
- const templateHasPrevious = stepTemplate.includes("{previous}");
1061
- const { prefix, suffix } = buildChainInstructions(
1062
- behavior,
1063
- chainDir,
1064
- isFirstProgress,
1065
- templateHasPrevious ? undefined : prev,
1066
- );
1067
-
1068
- let stepTask: string;
1069
- try {
1070
- stepTask = resolveOutputReferences(stepTemplate, outputs);
1071
- } catch (error) {
1072
- if (!(error instanceof ChainOutputValidationError)) throw error;
1073
- return buildChainExecutionErrorResult(error.message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
1074
- }
1075
- stepTask = stepTask.replace(/\{task\}/g, originalTask);
1076
- stepTask = stepTask.replace(/\{previous\}/g, prev);
1077
- stepTask = stepTask.replace(/\{chain_dir\}/g, chainDir);
1078
- const cleanTask = stepTask;
1079
- stepTask = prefix + stepTask + suffix;
1080
-
1081
- const effectiveModel =
1082
- tuiOverride?.model
1083
- ?? (seqStep.model ? resolveModelCandidate(seqStep.model, availableModels, ctx.model?.provider) : null)
1084
- ?? resolveModelCandidate(agentConfig.model, availableModels, ctx.model?.provider);
1085
-
1086
- const outputPath = typeof behavior.output === "string"
1087
- ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
1088
- : undefined;
1089
- const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Chain step ${stepIndex + 1} (${seqStep.agent})`);
1090
- if (validationError) {
1091
- return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
1092
- }
1093
- const maxSubagentDepth = resolveChildMaxSubagentDepth(params.maxSubagentDepth, agentConfig.maxSubagentDepth);
1094
- const interruptController = new AbortController();
1095
- if (foregroundControl) {
1096
- foregroundControl.currentAgent = seqStep.agent;
1097
- foregroundControl.currentIndex = globalTaskIndex;
1098
- foregroundControl.currentActivityState = undefined;
1099
- foregroundControl.updatedAt = Date.now();
1100
- foregroundControl.interrupt = () => {
1101
- if (interruptController.signal.aborted) return false;
1102
- interruptController.abort();
1107
+ const maxSubagentDepth = resolveChildMaxSubagentDepth(params.maxSubagentDepth, agentConfig.maxSubagentDepth);
1108
+ const interruptController = new AbortController();
1109
+ if (foregroundControl) {
1110
+ foregroundControl.currentAgent = seqStep.agent;
1111
+ foregroundControl.currentIndex = globalTaskIndex;
1103
1112
  foregroundControl.currentActivityState = undefined;
1104
1113
  foregroundControl.updatedAt = Date.now();
1105
- return true;
1106
- };
1107
- }
1114
+ foregroundControl.interrupt = () => {
1115
+ if (interruptController.signal.aborted) return false;
1116
+ interruptController.abort();
1117
+ foregroundControl.currentActivityState = undefined;
1118
+ foregroundControl.updatedAt = Date.now();
1119
+ return true;
1120
+ };
1121
+ }
1108
1122
 
1109
- const structuredRuntime = seqStep.outputSchema
1110
- ? createStructuredOutputRuntime(seqStep.outputSchema, path.join(chainDir, "structured-output"))
1111
- : undefined;
1112
- const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1113
- runtime: params.runtime,
1114
- cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
1115
- signal,
1116
- interruptSignal: interruptController.signal,
1117
- runId,
1118
- index: globalTaskIndex,
1119
- sessionDir: sessionDirForIndex(globalTaskIndex),
1120
- sessionFile: sessionFileForIndex?.(globalTaskIndex),
1121
- share: shareEnabled,
1122
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
1123
- artifactConfig,
1124
- outputPath,
1125
- outputMode: behavior.outputMode,
1126
- forwardedFlags: params.forwardedFlags,
1127
- maxSubagentDepth,
1128
- controlConfig,
1129
- onControlEvent,
1130
- nestedRoute: params.nestedRoute,
1131
- modelOverride: effectiveModel,
1132
- availableModels,
1133
- preferredModelProvider: ctx.model?.provider,
1134
- parentModel: parentModelFullId(ctx.model),
1135
- parentThinking: ctx.thinkingLevel,
1136
- skills: behavior.skills === false ? [] : behavior.skills,
1137
- structuredOutput: structuredRuntime,
1138
- acceptance: seqStep.acceptance,
1139
- acceptanceContext: { mode: "chain" },
1140
- onUpdate: onUpdate
1141
- ? (p) => {
1142
- const stepResults = p.details?.results || [];
1143
- const stepProgress = p.details?.progress || [];
1144
- if (foregroundControl && stepProgress.length > 0) {
1145
- const current = stepProgress[0];
1146
- foregroundControl.currentAgent = seqStep.agent;
1147
- foregroundControl.currentIndex = globalTaskIndex;
1148
- foregroundControl.currentActivityState = current?.activityState;
1149
- foregroundControl.lastActivityAt = current?.lastActivityAt;
1150
- foregroundControl.currentTool = current?.currentTool;
1151
- foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
1152
- foregroundControl.currentPath = current?.currentPath;
1153
- foregroundControl.turnCount = current?.turnCount;
1154
- foregroundControl.tokens = current?.tokens;
1155
- foregroundControl.toolCount = current?.toolCount;
1156
- foregroundControl.updatedAt = Date.now();
1157
- }
1158
- onUpdate({
1159
- ...p,
1160
- details: {
1161
- mode: "chain",
1162
- results: results.concat(stepResults),
1163
- progress: allProgress.concat(stepProgress),
1164
- controlEvents: p.details?.controlEvents,
1165
- chainAgents,
1166
- totalSteps,
1167
- currentStepIndex: stepIndex,
1168
- outputs,
1169
- workflowGraph: buildWorkflowGraphSnapshot({
1170
- runId,
1123
+ const structuredRuntime = seqStep.outputSchema
1124
+ ? createStructuredOutputRuntime(seqStep.outputSchema, path.join(chainDir, "structured-output"))
1125
+ : undefined;
1126
+ const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1127
+ runtime: params.runtime,
1128
+ cwd: stepCwd,
1129
+ signal,
1130
+ interruptSignal: interruptController.signal,
1131
+ runId,
1132
+ index: globalTaskIndex,
1133
+ sessionDir: sessionDirForIndex(globalTaskIndex),
1134
+ sessionFile: sessionFileForIndex?.(globalTaskIndex),
1135
+ share: shareEnabled,
1136
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
1137
+ artifactConfig,
1138
+ outputPath,
1139
+ outputMode: behavior.outputMode,
1140
+ forwardedFlags: params.forwardedFlags,
1141
+ maxSubagentDepth,
1142
+ controlConfig,
1143
+ onControlEvent,
1144
+ nestedRoute: params.nestedRoute,
1145
+ modelOverride: effectiveModel,
1146
+ availableModels,
1147
+ preferredModelProvider: ctx.model?.provider,
1148
+ parentModel: parentModelFullId(ctx.model),
1149
+ parentThinking: ctx.thinkingLevel,
1150
+ skills: behavior.skills === false ? [] : behavior.skills,
1151
+ structuredOutput: structuredRuntime,
1152
+ acceptance: seqStep.acceptance,
1153
+ acceptanceContext: { mode: "chain" },
1154
+ onUpdate: onUpdate
1155
+ ? (p) => {
1156
+ const stepResults = p.details?.results || [];
1157
+ const stepProgress = p.details?.progress || [];
1158
+ if (foregroundControl && stepProgress.length > 0) {
1159
+ const current = stepProgress[0];
1160
+ foregroundControl.currentAgent = seqStep.agent;
1161
+ foregroundControl.currentIndex = globalTaskIndex;
1162
+ foregroundControl.currentActivityState = current?.activityState;
1163
+ foregroundControl.lastActivityAt = current?.lastActivityAt;
1164
+ foregroundControl.currentTool = current?.currentTool;
1165
+ foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
1166
+ foregroundControl.currentPath = current?.currentPath;
1167
+ foregroundControl.turnCount = current?.turnCount;
1168
+ foregroundControl.tokens = current?.tokens;
1169
+ foregroundControl.toolCount = current?.toolCount;
1170
+ foregroundControl.updatedAt = Date.now();
1171
+ }
1172
+ onUpdate({
1173
+ ...p,
1174
+ details: {
1171
1175
  mode: "chain",
1172
- steps: chainSteps,
1173
1176
  results: results.concat(stepResults),
1177
+ progress: allProgress.concat(stepProgress),
1178
+ controlEvents: p.details?.controlEvents,
1179
+ chainAgents,
1180
+ totalSteps,
1174
1181
  currentStepIndex: stepIndex,
1175
- currentFlatIndex: globalTaskIndex,
1176
- dynamicChildren,
1177
- dynamicGroupStatuses,
1178
- }),
1179
- },
1180
- });
1181
- }
1182
- : undefined,
1183
- });
1184
- if (foregroundControl?.currentIndex === globalTaskIndex) {
1185
- foregroundControl.interrupt = undefined;
1186
- foregroundControl.updatedAt = Date.now();
1187
- }
1188
- recordRun(seqStep.agent, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
1182
+ outputs,
1183
+ workflowGraph: buildWorkflowGraphSnapshot({
1184
+ runId,
1185
+ mode: "chain",
1186
+ steps: chainSteps,
1187
+ results: results.concat(stepResults),
1188
+ currentStepIndex: stepIndex,
1189
+ currentFlatIndex: globalTaskIndex,
1190
+ dynamicChildren,
1191
+ dynamicGroupStatuses,
1192
+ }),
1193
+ },
1194
+ });
1195
+ }
1196
+ : undefined,
1197
+ });
1198
+ if (foregroundControl?.currentIndex === globalTaskIndex) {
1199
+ foregroundControl.interrupt = undefined;
1200
+ foregroundControl.updatedAt = Date.now();
1201
+ }
1202
+ recordRun(seqStep.agent, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
1189
1203
 
1190
- globalTaskIndex++;
1191
- results.push(r);
1192
- logTaskOutput(stepIndex, 0, r, seqStep.as, seqStep.label);
1193
- if (r.progress) allProgress.push(r.progress);
1194
- if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
1204
+ globalTaskIndex++;
1205
+ results.push(r);
1206
+ logTaskOutput(stepIndex, 0, r, seqStep.as, seqStep.label);
1207
+ if (r.progress) allProgress.push(r.progress);
1208
+ if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
1195
1209
 
1196
- if (r.interrupted) {
1197
- return {
1198
- content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${r.agent}). Waiting for explicit next action.` }],
1199
- details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
1200
- };
1201
- }
1210
+ if (r.interrupted) {
1211
+ return {
1212
+ content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${r.agent}). Waiting for explicit next action.` }],
1213
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
1214
+ };
1215
+ }
1202
1216
 
1203
- if (r.exitCode !== 0) {
1204
- const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
1205
- index: stepIndex,
1206
- error: r.error || "Chain failed",
1207
- }, failureSummaryOptions());
1208
- return {
1209
- content: [{ type: "text", text: summary }],
1210
- details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
1211
- isError: true,
1212
- };
1213
- }
1217
+ if (r.exitCode !== 0) {
1218
+ const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
1219
+ index: stepIndex,
1220
+ error: r.error || "Chain failed",
1221
+ }, failureSummaryOptions());
1222
+ return {
1223
+ content: [{ type: "text", text: summary }],
1224
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
1225
+ isError: true,
1226
+ };
1227
+ }
1214
1228
 
1215
- if (behavior.output) {
1216
- try {
1217
- const expectedPath = path.isAbsolute(behavior.output)
1218
- ? behavior.output
1219
- : path.join(chainDir, behavior.output);
1220
- if (!fs.existsSync(expectedPath)) {
1221
- const dirFiles = fs.readdirSync(chainDir);
1222
- const mdFiles = dirFiles.filter((file) => file.endsWith(".md") && file !== "progress.md");
1223
- const warning = mdFiles.length > 0
1224
- ? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
1225
- : `Agent did not create expected output file: ${behavior.output}`;
1226
- r.error = r.error ? `${r.error}\n${warning}` : warning;
1229
+ if (behavior.output) {
1230
+ try {
1231
+ const expectedPath = path.isAbsolute(behavior.output)
1232
+ ? behavior.output
1233
+ : path.join(chainDir, behavior.output);
1234
+ if (!fs.existsSync(expectedPath)) {
1235
+ const dirFiles = fs.readdirSync(chainDir);
1236
+ const mdFiles = dirFiles.filter((file) => file.endsWith(".md") && file !== "progress.md");
1237
+ const warning = mdFiles.length > 0
1238
+ ? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
1239
+ : `Agent did not create expected output file: ${behavior.output}`;
1240
+ r.error = r.error ? `${r.error}\n${warning}` : warning;
1241
+ }
1242
+ } catch {
1243
+ // Ignore validation errors; this diagnostic should not mask successful chain output.
1227
1244
  }
1228
- } catch {
1229
- // Ignore validation errors; this diagnostic should not mask successful chain output.
1230
1245
  }
1231
- }
1232
1246
 
1233
- if (seqStep.as) outputs[seqStep.as] = outputEntryFromResult(r, stepIndex);
1234
- prev = getSingleResultOutput(r);
1235
- if (seqStep.stopOn) {
1236
- const matches = evaluateStopCondition(seqStep.stopOn, stepIndex, [{ agent: r.agent, as: seqStep.as, label: seqStep.label, exitCode: r.exitCode, structuredOutput: r.structuredOutput }], { sequential: true });
1237
- if (matches.length > 0) stopRecord = buildStopRecord(seqStep.stopOn, stepIndex, matches);
1238
- }
1239
- if (stopRecord) {
1240
- return {
1241
- content: [{ type: "text", text: buildChainSummary(chainSteps, results, chainDir, "stopped", undefined, summaryOptions()) }],
1242
- details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex })),
1243
- };
1247
+ if (seqStep.as) outputs[seqStep.as] = outputEntryFromResult(r, stepIndex);
1248
+ prev = getSingleResultOutput(r);
1249
+ if (seqStep.stopOn) {
1250
+ const matches = evaluateStopCondition(seqStep.stopOn, stepIndex, [{ agent: r.agent, as: seqStep.as, label: seqStep.label, exitCode: r.exitCode, structuredOutput: r.structuredOutput }], { sequential: true });
1251
+ if (matches.length > 0) stopRecord = buildStopRecord(seqStep.stopOn, stepIndex, matches);
1252
+ }
1253
+ if (stopRecord) {
1254
+ return {
1255
+ content: [{ type: "text", text: buildChainSummary(chainSteps, results, chainDir, "stopped", undefined, summaryOptions()) }],
1256
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex })),
1257
+ };
1258
+ }
1244
1259
  }
1245
1260
  }
1246
- }
1247
1261
 
1248
- const summary = buildChainSummary(chainSteps, results, chainDir, "completed", undefined, summaryOptions());
1262
+ const summary = buildChainSummary(chainSteps, results, chainDir, "completed", undefined, summaryOptions());
1249
1263
 
1250
- return {
1251
- content: [{ type: "text", text: summary }],
1252
- details: buildChainExecutionDetails(makeDetailsInput()),
1253
- };
1264
+ return {
1265
+ content: [{ type: "text", text: summary }],
1266
+ details: buildChainExecutionDetails(makeDetailsInput()),
1267
+ };
1268
+ } finally {
1269
+ removeTrees(trees.created);
1270
+ }
1254
1271
  }