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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -1
  3. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  4. package/dist/builtin/intercom/package.json +1 -1
  5. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  6. package/dist/builtin/mcp/package.json +1 -1
  7. package/dist/builtin/subagents/CHANGELOG.md +10 -0
  8. package/dist/builtin/subagents/README.md +132 -21
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/subagents/prompts/parallel-context-build.md +4 -2
  11. package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +3 -1
  12. package/dist/builtin/subagents/skills/subagent/SKILL.md +49 -11
  13. package/dist/builtin/subagents/src/agents/agent-management.ts +79 -16
  14. package/dist/builtin/subagents/src/agents/agents.ts +47 -16
  15. package/dist/builtin/subagents/src/agents/chain-serializer.ts +114 -0
  16. package/dist/builtin/subagents/src/extension/schemas.ts +139 -3
  17. package/dist/builtin/subagents/src/runs/background/async-execution.ts +92 -6
  18. package/dist/builtin/subagents/src/runs/background/async-status.ts +11 -1
  19. package/dist/builtin/subagents/src/runs/background/run-status.ts +4 -1
  20. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +529 -32
  21. package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +361 -118
  22. package/dist/builtin/subagents/src/runs/foreground/execution.ts +75 -7
  23. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +33 -0
  24. package/dist/builtin/subagents/src/runs/shared/acceptance.ts +611 -0
  25. package/dist/builtin/subagents/src/runs/shared/chain-outputs.ts +101 -0
  26. package/dist/builtin/subagents/src/runs/shared/dynamic-fanout.ts +293 -0
  27. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +29 -1
  28. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +11 -0
  29. package/dist/builtin/subagents/src/runs/shared/structured-output.ts +79 -0
  30. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +52 -2
  31. package/dist/builtin/subagents/src/runs/shared/workflow-graph.ts +206 -0
  32. package/dist/builtin/subagents/src/shared/formatters.ts +2 -2
  33. package/dist/builtin/subagents/src/shared/settings.ts +53 -4
  34. package/dist/builtin/subagents/src/shared/types.ts +226 -0
  35. package/dist/builtin/subagents/src/shared/utils.ts +2 -1
  36. package/dist/builtin/subagents/src/slash/slash-commands.ts +41 -3
  37. package/dist/builtin/subagents/src/tui/render.ts +152 -34
  38. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  39. package/dist/builtin/web-access/package.json +1 -1
  40. package/dist/builtin/workflows/CHANGELOG.md +6 -0
  41. package/dist/builtin/workflows/package.json +1 -1
  42. package/dist/builtin/workflows/skills/create-spec/SKILL.md +1 -1
  43. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +0 -1
  44. package/dist/core/slash-commands.d.ts.map +1 -1
  45. package/dist/core/slash-commands.js +1 -0
  46. package/dist/core/slash-commands.js.map +1 -1
  47. package/dist/core/system-prompt.d.ts.map +1 -1
  48. package/dist/core/system-prompt.js +4 -3
  49. package/dist/core/system-prompt.js.map +1 -1
  50. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  51. package/dist/modes/interactive/interactive-mode.js +1 -1
  52. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  53. package/docs/usage.md +1 -0
  54. package/docs/workflows.md +173 -0
  55. package/package.json +1 -1
@@ -20,9 +20,11 @@ import {
20
20
  createParallelDirs,
21
21
  suppressProgressForReadOnlyTask,
22
22
  aggregateParallelOutputs,
23
+ isDynamicParallelStep,
23
24
  isParallelStep,
24
25
  type StepOverrides,
25
26
  type ChainStep,
27
+ type ParallelStep,
26
28
  type SequentialStep,
27
29
  type ParallelTaskResult,
28
30
  type ResolvedStepBehavior,
@@ -60,6 +62,12 @@ import {
60
62
  } from "../../shared/types.ts";
61
63
  import { resolveModelCandidate } from "../shared/model-fallback.ts";
62
64
  import { validateFileOnlyOutputMode } from "../shared/single-output.ts";
65
+ import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
66
+ import { ChainOutputValidationError, outputEntryFromResult, resolveOutputReferences, validateChainOutputBindings } from "../shared/chain-outputs.ts";
67
+ import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
68
+ import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection, type DynamicCollectedResult } from "../shared/dynamic-fanout.ts";
69
+ import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, resolveEffectiveAcceptance } from "../shared/acceptance.ts";
70
+ import type { ChainOutputMap } from "../../shared/types.ts";
63
71
 
64
72
  type RunSyncDependency = typeof runSync;
65
73
 
@@ -85,12 +93,18 @@ interface ChainExecutionDetailsInput {
85
93
  allArtifactPaths: ArtifactPaths[];
86
94
  artifactsDir: string;
87
95
  chainAgents: string[];
96
+ chainSteps: ChainStep[];
88
97
  totalSteps: number;
89
98
  currentStepIndex?: number;
99
+ runId: string;
100
+ outputs?: ChainOutputMap;
101
+ currentFlatIndex?: number;
102
+ dynamicChildren?: Record<number, Array<{ agent: string; label?: string; flatIndex: number; itemKey: string; outputName?: string; structured?: boolean; error?: string }>>;
103
+ dynamicGroupStatuses?: Record<number, { status: "pending" | "running" | "completed" | "failed" | "paused" | "detached"; error?: string; acceptance?: SingleResult["acceptance"] }>;
90
104
  }
91
105
 
92
106
  interface ParallelChainRunInput {
93
- step: Exclude<ChainStep, SequentialStep>;
107
+ step: ParallelStep;
94
108
  parallelTemplates: string[];
95
109
  parallelBehaviors: ResolvedStepBehavior[];
96
110
  agents: AgentConfig[];
@@ -118,8 +132,12 @@ interface ParallelChainRunInput {
118
132
  foregroundControl?: ChainForegroundControl;
119
133
  results: SingleResult[];
120
134
  allProgress: AgentProgress[];
135
+ outputs: ChainOutputMap;
121
136
  chainAgents: string[];
137
+ chainSteps: ChainStep[];
122
138
  totalSteps: number;
139
+ dynamicChildren?: ChainExecutionDetailsInput["dynamicChildren"];
140
+ dynamicGroupStatuses?: ChainExecutionDetailsInput["dynamicGroupStatuses"];
123
141
  worktreeSetup?: WorktreeSetup;
124
142
  maxSubagentDepth: number;
125
143
  workflowStageSubagentGuard?: boolean;
@@ -136,6 +154,17 @@ function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details
136
154
  chainAgents: input.chainAgents,
137
155
  totalSteps: input.totalSteps,
138
156
  currentStepIndex: input.currentStepIndex,
157
+ outputs: input.outputs,
158
+ workflowGraph: buildWorkflowGraphSnapshot({
159
+ runId: input.runId,
160
+ mode: "chain",
161
+ steps: input.chainSteps,
162
+ results: input.results,
163
+ currentStepIndex: input.currentStepIndex,
164
+ currentFlatIndex: input.currentFlatIndex,
165
+ dynamicChildren: input.dynamicChildren,
166
+ dynamicGroupStatuses: input.dynamicGroupStatuses,
167
+ }),
139
168
  });
140
169
  }
141
170
 
@@ -202,7 +231,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
202
231
  templateHasPrevious ? undefined : input.prev,
203
232
  );
204
233
 
205
- let taskStr = taskTemplate;
234
+ let taskStr = resolveOutputReferences(taskTemplate, input.outputs);
206
235
  taskStr = taskStr.replace(/\{task\}/g, input.originalTask);
207
236
  taskStr = taskStr.replace(/\{previous\}/g, input.prev);
208
237
  taskStr = taskStr.replace(/\{chain_dir\}/g, input.chainDir);
@@ -237,6 +266,9 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
237
266
  };
238
267
  }
239
268
 
269
+ const structuredRuntime = task.outputSchema
270
+ ? createStructuredOutputRuntime(task.outputSchema, path.join(input.chainDir, "structured-output"))
271
+ : undefined;
240
272
  const result = await input.runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
241
273
  cwd: taskCwd,
242
274
  signal: input.signal,
@@ -264,6 +296,9 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
264
296
  currentModel: currentModelFullId(input.ctx.model),
265
297
  preferredModelProvider: input.ctx.model?.provider,
266
298
  skills: behavior.skills === false ? [] : behavior.skills,
299
+ structuredOutput: structuredRuntime,
300
+ acceptance: task.acceptance,
301
+ acceptanceContext: { mode: "chain" },
267
302
  onUpdate: input.onUpdate
268
303
  ? (progressUpdate) => {
269
304
  const stepResults = progressUpdate.details?.results || [];
@@ -292,6 +327,17 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
292
327
  chainAgents: input.chainAgents,
293
328
  totalSteps: input.totalSteps,
294
329
  currentStepIndex: input.stepIndex,
330
+ outputs: input.outputs,
331
+ workflowGraph: buildWorkflowGraphSnapshot({
332
+ runId: input.runId,
333
+ mode: "chain",
334
+ steps: input.chainSteps,
335
+ results: input.results.concat(stepResults),
336
+ currentStepIndex: input.stepIndex,
337
+ currentFlatIndex: input.globalTaskIndex + taskIndex,
338
+ dynamicChildren: input.dynamicChildren,
339
+ dynamicGroupStatuses: input.dynamicGroupStatuses,
340
+ }),
295
341
  },
296
342
  });
297
343
  }
@@ -337,6 +383,7 @@ interface ChainExecutionParams {
337
383
  foregroundControl?: ChainForegroundControl;
338
384
  chainSkills?: string[];
339
385
  chainDir?: string;
386
+ dynamicFanoutMaxItems?: number;
340
387
  maxSubagentDepth: number;
341
388
  workflowStageSubagentGuard?: boolean;
342
389
  nestedRoute?: NestedRouteInfo;
@@ -387,22 +434,60 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
387
434
  } = params;
388
435
  const chainSkills = chainSkillsParam ?? [];
389
436
 
437
+ const results: SingleResult[] = [];
438
+ const outputs: ChainOutputMap = {};
439
+ const dynamicChildren: ChainExecutionDetailsInput["dynamicChildren"] = {};
440
+ const dynamicGroupStatuses: ChainExecutionDetailsInput["dynamicGroupStatuses"] = {};
390
441
  const allProgress: AgentProgress[] = [];
391
442
  const allArtifactPaths: ArtifactPaths[] = [];
392
443
 
393
444
  const chainAgents: string[] = chainSteps.map((step) =>
394
445
  isParallelStep(step)
395
446
  ? `[${step.parallel.map((t) => t.agent).join("+")}]`
447
+ : isDynamicParallelStep(step)
448
+ ? `expand:${step.parallel.agent}`
396
449
  : (step as SequentialStep).agent,
397
450
  );
398
451
  const totalSteps = chainSteps.length;
399
452
 
453
+ const makeDetailsInput = (overrides: Pick<Partial<ChainExecutionDetailsInput>, "currentStepIndex" | "currentFlatIndex"> = {}): ChainExecutionDetailsInput => ({
454
+ results,
455
+ ...(includeProgress !== undefined ? { includeProgress } : {}),
456
+ allProgress,
457
+ allArtifactPaths,
458
+ artifactsDir,
459
+ chainAgents,
460
+ chainSteps,
461
+ totalSteps,
462
+ runId,
463
+ outputs,
464
+ dynamicChildren,
465
+ dynamicGroupStatuses,
466
+ ...overrides,
467
+ });
468
+
400
469
  const firstStep = chainSteps[0]!;
401
470
  const originalTask = params.task
402
- ?? (isParallelStep(firstStep) ? firstStep.parallel[0]!.task! : (firstStep as SequentialStep).task!);
471
+ ?? (isParallelStep(firstStep)
472
+ ? firstStep.parallel[0]!.task!
473
+ : isDynamicParallelStep(firstStep)
474
+ ? firstStep.parallel.task!
475
+ : (firstStep as SequentialStep).task!);
476
+ try {
477
+ validateChainOutputBindings(chainSteps, { maxItems: params.dynamicFanoutMaxItems });
478
+ } catch (error) {
479
+ if (error instanceof ChainOutputValidationError) {
480
+ return {
481
+ content: [{ type: "text", text: error.message }],
482
+ isError: true,
483
+ details: buildChainExecutionDetails(makeDetailsInput()),
484
+ };
485
+ }
486
+ throw error;
487
+ }
403
488
 
404
489
  const chainDir = createChainDir(runId, chainDirBase);
405
- const hasParallelSteps = chainSteps.some(isParallelStep);
490
+ const hasParallelSteps = chainSteps.some((step) => isParallelStep(step) || isDynamicParallelStep(step));
406
491
  let templates: ResolvedTemplates = resolveChainTemplates(chainSteps);
407
492
  const shouldClarify = clarify !== false && ctx.hasUI && !hasParallelSteps;
408
493
  let tuiBehaviorOverrides: (BehaviorOverride | undefined)[] | undefined;
@@ -419,7 +504,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
419
504
  return {
420
505
  content: [{ type: "text", text: `Unknown agent: ${step.agent}` }],
421
506
  isError: true,
422
- details: { mode: "chain" as const, results: [] },
507
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: seqSteps.indexOf(step) })),
423
508
  };
424
509
  }
425
510
  agentConfigs.push(config);
@@ -464,7 +549,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
464
549
  removeChainDir(chainDir);
465
550
  return {
466
551
  content: [{ type: "text", text: "Chain cancelled" }],
467
- details: { mode: "chain", results: [] },
552
+ details: buildChainExecutionDetails(makeDetailsInput()),
468
553
  };
469
554
  }
470
555
 
@@ -486,7 +571,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
486
571
  });
487
572
  return {
488
573
  content: [{ type: "text", text: "Launching in background..." }],
489
- details: { mode: "chain", results: [] },
574
+ details: buildChainExecutionDetails(makeDetailsInput()),
490
575
  requestedAsync: { chain: updatedChain, chainSkills },
491
576
  };
492
577
  }
@@ -495,7 +580,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
495
580
  tuiBehaviorOverrides = result.behaviorOverrides;
496
581
  }
497
582
 
498
- const results: SingleResult[] = [];
499
583
  let prev = "";
500
584
  let globalTaskIndex = 0;
501
585
  let progressCreated = false;
@@ -513,16 +597,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
513
597
  if (worktreeTaskCwdConflict) {
514
598
  return buildChainExecutionErrorResult(
515
599
  `parallel chain step ${stepIndex + 1}: ${formatWorktreeTaskCwdConflict(worktreeTaskCwdConflict, parallelCwd)}`,
516
- {
517
- results,
518
- includeProgress,
519
- allProgress,
520
- allArtifactPaths,
521
- artifactsDir,
522
- chainAgents,
523
- totalSteps,
524
- currentStepIndex: stepIndex,
525
- },
600
+ makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }),
526
601
  );
527
602
  }
528
603
  try {
@@ -534,16 +609,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
534
609
  });
535
610
  } catch (error) {
536
611
  const message = error instanceof Error ? error.message : String(error);
537
- return buildChainExecutionErrorResult(message, {
538
- results,
539
- includeProgress,
540
- allProgress,
541
- allArtifactPaths,
542
- artifactsDir,
543
- chainAgents,
544
- totalSteps,
545
- currentStepIndex: stepIndex,
546
- });
612
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
547
613
  }
548
614
  }
549
615
 
@@ -557,16 +623,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
557
623
  ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
558
624
  : undefined;
559
625
  const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Parallel chain step ${stepIndex + 1} task ${taskIndex + 1} (${step.parallel[taskIndex]!.agent})`);
560
- if (validationError) return buildChainExecutionErrorResult(validationError, {
561
- results,
562
- includeProgress,
563
- allProgress,
564
- allArtifactPaths,
565
- artifactsDir,
566
- chainAgents,
567
- totalSteps,
568
- currentStepIndex: stepIndex,
569
- });
626
+ if (validationError) return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }));
570
627
  }
571
628
  progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
572
629
  createParallelDirs(chainDir, stepIndex, step.parallel.length, agentNames);
@@ -595,8 +652,12 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
595
652
  onUpdate,
596
653
  results,
597
654
  allProgress,
655
+ outputs,
598
656
  chainAgents,
657
+ chainSteps,
599
658
  totalSteps,
659
+ dynamicChildren,
660
+ dynamicGroupStatuses,
600
661
  controlConfig,
601
662
  onControlEvent,
602
663
  childIntercomTarget,
@@ -615,21 +676,15 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
615
676
  if (result.progress) allProgress.push(result.progress);
616
677
  if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
617
678
  }
618
-
619
- const interrupted = parallelResults.find((result) => result.interrupted);
679
+ const interruptedIndexInStep = parallelResults.findIndex((result) => result.interrupted);
680
+ const interrupted = interruptedIndexInStep >= 0 ? parallelResults[interruptedIndexInStep] : undefined;
620
681
  if (interrupted) {
621
682
  return {
622
683
  content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
623
- details: buildChainExecutionDetails({
624
- results,
625
- includeProgress,
626
- allProgress,
627
- allArtifactPaths,
628
- artifactsDir,
629
- chainAgents,
630
- totalSteps,
684
+ details: buildChainExecutionDetails(makeDetailsInput({
631
685
  currentStepIndex: stepIndex,
632
- }),
686
+ currentFlatIndex: globalTaskIndex - step.parallel.length + interruptedIndexInStep,
687
+ })),
633
688
  };
634
689
  }
635
690
  const detachedIndexInStep = parallelResults.findIndex((result) => result.detached);
@@ -637,16 +692,10 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
637
692
  if (detached) {
638
693
  return {
639
694
  content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
640
- details: buildChainExecutionDetails({
641
- results,
642
- includeProgress,
643
- allProgress,
644
- allArtifactPaths,
645
- artifactsDir,
646
- chainAgents,
647
- totalSteps,
695
+ details: buildChainExecutionDetails(makeDetailsInput({
648
696
  currentStepIndex: stepIndex,
649
- }),
697
+ currentFlatIndex: globalTaskIndex - step.parallel.length + detachedIndexInStep,
698
+ })),
650
699
  };
651
700
  }
652
701
 
@@ -665,19 +714,18 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
665
714
  return {
666
715
  content: [{ type: "text", text: summary }],
667
716
  isError: true,
668
- details: buildChainExecutionDetails({
669
- results,
670
- includeProgress,
671
- allProgress,
672
- allArtifactPaths,
673
- artifactsDir,
674
- chainAgents,
675
- totalSteps,
717
+ details: buildChainExecutionDetails(makeDetailsInput({
676
718
  currentStepIndex: stepIndex,
677
- }),
719
+ currentFlatIndex: globalTaskIndex - step.parallel.length + failures[0]!.originalIndex,
720
+ })),
678
721
  };
679
722
  }
680
723
 
724
+ for (let taskIndex = 0; taskIndex < parallelResults.length; taskIndex++) {
725
+ const outputName = step.parallel[taskIndex]?.as;
726
+ if (outputName) outputs[outputName] = outputEntryFromResult(parallelResults[taskIndex]!, stepIndex);
727
+ }
728
+
681
729
  const taskResults: ParallelTaskResult[] = parallelResults.map((result, i) => {
682
730
  const outputTarget = parallelBehaviors[i]?.output;
683
731
  const outputTargetPath = typeof outputTarget === "string"
@@ -703,6 +751,227 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
703
751
  } finally {
704
752
  if (worktreeSetup) cleanupWorktrees(worktreeSetup);
705
753
  }
754
+ } else if (isDynamicParallelStep(step)) {
755
+ let materialized: ReturnType<typeof materializeDynamicParallelStep>;
756
+ try {
757
+ materialized = materializeDynamicParallelStep(step, outputs, stepIndex, { maxItems: params.dynamicFanoutMaxItems });
758
+ } catch (error) {
759
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
760
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
761
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
762
+ }
763
+
764
+ dynamicChildren[stepIndex] = materialized.items.map((item, itemIndex) => ({
765
+ agent: step.parallel.agent,
766
+ label: materialized.parallel[itemIndex]?.label,
767
+ flatIndex: globalTaskIndex + itemIndex,
768
+ itemKey: item.key,
769
+ structured: Boolean(step.parallel.outputSchema),
770
+ }));
771
+
772
+ if (materialized.parallel.length === 0) {
773
+ const collection: DynamicCollectedResult[] = [];
774
+ try {
775
+ validateDynamicCollection(step.collect.outputSchema, collection);
776
+ } catch (error) {
777
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
778
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
779
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
780
+ }
781
+ outputs[step.collect.as] = {
782
+ text: JSON.stringify(collection),
783
+ structured: collection,
784
+ agent: step.parallel.agent,
785
+ stepIndex,
786
+ };
787
+ dynamicGroupStatuses[stepIndex] = { status: "completed" };
788
+ if (step.acceptance !== undefined) {
789
+ const effectiveGroupAcceptance = resolveEffectiveAcceptance({
790
+ explicit: step.acceptance,
791
+ agentName: step.parallel.agent,
792
+ task: step.parallel.task ?? originalTask,
793
+ mode: "chain",
794
+ dynamicGroup: true,
795
+ });
796
+ const groupAcceptance = await evaluateAcceptance({
797
+ acceptance: effectiveGroupAcceptance,
798
+ output: "",
799
+ report: aggregateAcceptanceReport({
800
+ results: [],
801
+ notes: "Dynamic fanout produced 0 results.",
802
+ }),
803
+ cwd: cwd ?? ctx.cwd,
804
+ });
805
+ dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
806
+ const groupAcceptanceFailure = acceptanceFailureMessage(groupAcceptance);
807
+ if (groupAcceptanceFailure) {
808
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
809
+ return buildChainExecutionErrorResult(groupAcceptanceFailure, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
810
+ }
811
+ }
812
+ prev = "Dynamic fanout produced 0 results.";
813
+ continue;
814
+ }
815
+
816
+ const dynamicParallelStep: ParallelStep = {
817
+ parallel: materialized.parallel,
818
+ concurrency: step.concurrency,
819
+ failFast: step.failFast,
820
+ };
821
+ const parallelTemplates = materialized.parallel.map((task) => task.task ?? "{previous}");
822
+ const parallelBehaviors = resolveParallelBehaviors(dynamicParallelStep.parallel, agents, stepIndex, chainSkills)
823
+ .map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? dynamicParallelStep.parallel[taskIndex]?.task, originalTask));
824
+
825
+ for (let taskIndex = 0; taskIndex < dynamicParallelStep.parallel.length; taskIndex++) {
826
+ const behavior = parallelBehaviors[taskIndex]!;
827
+ const outputPath = typeof behavior.output === "string"
828
+ ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
829
+ : undefined;
830
+ const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Dynamic chain step ${stepIndex + 1} item ${taskIndex + 1} (${dynamicParallelStep.parallel[taskIndex]!.agent})`);
831
+ if (validationError) {
832
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: validationError };
833
+ return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }));
834
+ }
835
+ }
836
+
837
+ progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
838
+ createParallelDirs(chainDir, stepIndex, dynamicParallelStep.parallel.length, dynamicParallelStep.parallel.map((task) => task.agent));
839
+ const parallelResults = await runParallelChainTasks({
840
+ step: dynamicParallelStep,
841
+ parallelTemplates,
842
+ parallelBehaviors,
843
+ agents,
844
+ stepIndex,
845
+ availableModels,
846
+ chainDir,
847
+ prev,
848
+ originalTask,
849
+ ctx,
850
+ intercomEvents,
851
+ cwd,
852
+ runId,
853
+ globalTaskIndex,
854
+ sessionDirForIndex,
855
+ sessionFileForIndex,
856
+ shareEnabled,
857
+ artifactConfig,
858
+ artifactsDir,
859
+ signal,
860
+ onUpdate,
861
+ results,
862
+ allProgress,
863
+ outputs,
864
+ chainAgents,
865
+ chainSteps,
866
+ totalSteps,
867
+ dynamicChildren,
868
+ dynamicGroupStatuses,
869
+ controlConfig,
870
+ onControlEvent,
871
+ childIntercomTarget,
872
+ orchestratorIntercomTarget,
873
+ foregroundControl,
874
+ nestedRoute: params.nestedRoute,
875
+ maxSubagentDepth: params.maxSubagentDepth,
876
+ workflowStageSubagentGuard: params.workflowStageSubagentGuard,
877
+ runSync: executeRunSync,
878
+ });
879
+ globalTaskIndex += dynamicParallelStep.parallel.length;
880
+
881
+ for (const result of parallelResults) {
882
+ results.push(result);
883
+ if (result.progress) allProgress.push(result.progress);
884
+ if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
885
+ }
886
+ const collected = collectDynamicResults(step, materialized.items, parallelResults);
887
+ const interruptedIndexInStep = parallelResults.findIndex((result) => result.interrupted);
888
+ const interrupted = interruptedIndexInStep >= 0 ? parallelResults[interruptedIndexInStep] : undefined;
889
+ if (interrupted) {
890
+ return {
891
+ content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
892
+ details: buildChainExecutionDetails(makeDetailsInput({
893
+ currentStepIndex: stepIndex,
894
+ currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + interruptedIndexInStep,
895
+ })),
896
+ };
897
+ }
898
+ const detachedIndexInStep = parallelResults.findIndex((result) => result.detached);
899
+ const detached = detachedIndexInStep >= 0 ? parallelResults[detachedIndexInStep] : undefined;
900
+ if (detached) {
901
+ return {
902
+ content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
903
+ details: buildChainExecutionDetails(makeDetailsInput({
904
+ currentStepIndex: stepIndex,
905
+ currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + detachedIndexInStep,
906
+ })),
907
+ };
908
+ }
909
+ const failures = parallelResults
910
+ .map((result, originalIndex) => ({ ...result, originalIndex }))
911
+ .filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
912
+ if (failures.length > 0) {
913
+ const failureSummary = failures
914
+ .map((failure) => `- Item ${failure.originalIndex + 1} (${failure.agent}, key ${materialized.items[failure.originalIndex]?.key ?? failure.originalIndex}): ${failure.error || "failed"}`)
915
+ .join("\n");
916
+ const errorMsg = `Dynamic step ${stepIndex + 1} failed:\n${failureSummary}`;
917
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: errorMsg };
918
+ const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
919
+ index: stepIndex,
920
+ error: errorMsg,
921
+ });
922
+ return {
923
+ content: [{ type: "text", text: summary }],
924
+ isError: true,
925
+ details: buildChainExecutionDetails(makeDetailsInput({
926
+ currentStepIndex: stepIndex,
927
+ currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + failures[0]!.originalIndex,
928
+ })),
929
+ };
930
+ }
931
+ try {
932
+ validateDynamicCollection(step.collect.outputSchema, collected);
933
+ } catch (error) {
934
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
935
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
936
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }));
937
+ }
938
+ outputs[step.collect.as] = {
939
+ text: JSON.stringify(collected),
940
+ structured: collected,
941
+ agent: step.parallel.agent,
942
+ stepIndex,
943
+ };
944
+ dynamicGroupStatuses[stepIndex] = { status: "completed" };
945
+ const effectiveGroupAcceptance = resolveEffectiveAcceptance({
946
+ explicit: step.acceptance,
947
+ agentName: step.parallel.agent,
948
+ task: step.parallel.task ?? originalTask,
949
+ mode: "chain",
950
+ dynamicGroup: true,
951
+ });
952
+ const groupAcceptance = await evaluateAcceptance({
953
+ acceptance: effectiveGroupAcceptance,
954
+ output: "",
955
+ report: aggregateAcceptanceReport({
956
+ results: parallelResults,
957
+ notes: `Dynamic fanout collected ${collected.length} result(s) into ${step.collect.as}.`,
958
+ }),
959
+ cwd: cwd ?? ctx.cwd,
960
+ });
961
+ dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
962
+ const groupAcceptanceFailure = acceptanceFailureMessage(groupAcceptance);
963
+ if (groupAcceptanceFailure) {
964
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
965
+ return buildChainExecutionErrorResult(groupAcceptanceFailure, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }));
966
+ }
967
+ const taskResults: ParallelTaskResult[] = parallelResults.map((result, i) => ({
968
+ agent: result.agent,
969
+ taskIndex: i,
970
+ output: getSingleResultOutput(result),
971
+ exitCode: result.exitCode,
972
+ error: result.error,
973
+ }));
974
+ prev = aggregateParallelOutputs(taskResults, (i, agent) => `=== Dynamic Item ${i + 1} (${agent}, key ${materialized.items[i]?.key ?? i}) ===`);
706
975
  } else {
707
976
  const seqStep = step as SequentialStep;
708
977
  const stepTemplate = stepTemplates as string;
@@ -713,7 +982,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
713
982
  return {
714
983
  content: [{ type: "text", text: `Unknown agent: ${seqStep.agent}` }],
715
984
  isError: true,
716
- details: { mode: "chain" as const, results: [] },
985
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex })),
717
986
  };
718
987
  }
719
988
 
@@ -743,7 +1012,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
743
1012
  templateHasPrevious ? undefined : prev,
744
1013
  );
745
1014
 
746
- let stepTask = stepTemplate;
1015
+ let stepTask = resolveOutputReferences(stepTemplate, outputs);
747
1016
  stepTask = stepTask.replace(/\{task\}/g, originalTask);
748
1017
  stepTask = stepTask.replace(/\{previous\}/g, prev);
749
1018
  stepTask = stepTask.replace(/\{chain_dir\}/g, chainDir);
@@ -760,16 +1029,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
760
1029
  : undefined;
761
1030
  const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Chain step ${stepIndex + 1} (${seqStep.agent})`);
762
1031
  if (validationError) {
763
- return buildChainExecutionErrorResult(validationError, {
764
- results,
765
- includeProgress,
766
- allProgress,
767
- allArtifactPaths,
768
- artifactsDir,
769
- chainAgents,
770
- totalSteps,
771
- currentStepIndex: stepIndex,
772
- });
1032
+ return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
773
1033
  }
774
1034
  const maxSubagentDepth = resolveChildMaxSubagentDepth(params.maxSubagentDepth, agentConfig.maxSubagentDepth);
775
1035
  const interruptController = new AbortController();
@@ -787,6 +1047,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
787
1047
  };
788
1048
  }
789
1049
 
1050
+ const structuredRuntime = seqStep.outputSchema
1051
+ ? createStructuredOutputRuntime(seqStep.outputSchema, path.join(chainDir, "structured-output"))
1052
+ : undefined;
790
1053
  const r = await executeRunSync(ctx.cwd, agents, seqStep.agent, stepTask, {
791
1054
  cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
792
1055
  signal,
@@ -814,6 +1077,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
814
1077
  currentModel: currentModelFullId(ctx.model),
815
1078
  preferredModelProvider: ctx.model?.provider,
816
1079
  skills: behavior.skills === false ? [] : behavior.skills,
1080
+ structuredOutput: structuredRuntime,
1081
+ acceptance: seqStep.acceptance,
1082
+ acceptanceContext: { mode: "chain" },
817
1083
  onUpdate: onUpdate
818
1084
  ? (p) => {
819
1085
  const stepResults = p.details?.results || [];
@@ -842,6 +1108,17 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
842
1108
  chainAgents,
843
1109
  totalSteps,
844
1110
  currentStepIndex: stepIndex,
1111
+ outputs,
1112
+ workflowGraph: buildWorkflowGraphSnapshot({
1113
+ runId,
1114
+ mode: "chain",
1115
+ steps: chainSteps,
1116
+ results: results.concat(stepResults),
1117
+ currentStepIndex: stepIndex,
1118
+ currentFlatIndex: globalTaskIndex,
1119
+ dynamicChildren,
1120
+ dynamicGroupStatuses,
1121
+ }),
845
1122
  },
846
1123
  });
847
1124
  }
@@ -861,31 +1138,13 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
861
1138
  if (r.interrupted) {
862
1139
  return {
863
1140
  content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${r.agent}). Waiting for explicit next action.` }],
864
- details: buildChainExecutionDetails({
865
- results,
866
- includeProgress,
867
- allProgress,
868
- allArtifactPaths,
869
- artifactsDir,
870
- chainAgents,
871
- totalSteps,
872
- currentStepIndex: stepIndex,
873
- }),
1141
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
874
1142
  };
875
1143
  }
876
1144
  if (r.detached) {
877
1145
  return {
878
1146
  content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${r.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
879
- details: buildChainExecutionDetails({
880
- results,
881
- includeProgress,
882
- allProgress,
883
- allArtifactPaths,
884
- artifactsDir,
885
- chainAgents,
886
- totalSteps,
887
- currentStepIndex: stepIndex,
888
- }),
1147
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
889
1148
  };
890
1149
  }
891
1150
 
@@ -896,16 +1155,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
896
1155
  });
897
1156
  return {
898
1157
  content: [{ type: "text", text: summary }],
899
- details: buildChainExecutionDetails({
900
- results,
901
- includeProgress,
902
- allProgress,
903
- allArtifactPaths,
904
- artifactsDir,
905
- chainAgents,
906
- totalSteps,
907
- currentStepIndex: stepIndex,
908
- }),
1158
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
909
1159
  isError: true,
910
1160
  };
911
1161
  }
@@ -928,6 +1178,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
928
1178
  }
929
1179
  }
930
1180
 
1181
+ if (seqStep.as) outputs[seqStep.as] = outputEntryFromResult(r, stepIndex);
931
1182
  prev = getSingleResultOutput(r);
932
1183
  }
933
1184
  }
@@ -936,14 +1187,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
936
1187
 
937
1188
  return {
938
1189
  content: [{ type: "text", text: summary }],
939
- details: buildChainExecutionDetails({
940
- results,
941
- includeProgress,
942
- allProgress,
943
- allArtifactPaths,
944
- artifactsDir,
945
- chainAgents,
946
- totalSteps,
947
- }),
1190
+ details: buildChainExecutionDetails(makeDetailsInput()),
948
1191
  };
949
1192
  }