pi-subagents 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +129 -17
  3. package/package.json +1 -1
  4. package/prompts/parallel-context-build.md +3 -1
  5. package/prompts/parallel-handoff-plan.md +3 -1
  6. package/skills/pi-subagents/SKILL.md +32 -17
  7. package/src/agents/agent-management.ts +57 -15
  8. package/src/agents/agent-serializer.ts +3 -2
  9. package/src/agents/agents.ts +47 -16
  10. package/src/agents/chain-serializer.ts +120 -0
  11. package/src/extension/fanout-child.ts +1 -0
  12. package/src/extension/index.ts +1 -0
  13. package/src/extension/schemas.ts +138 -5
  14. package/src/runs/background/async-execution.ts +84 -6
  15. package/src/runs/background/async-status.ts +11 -1
  16. package/src/runs/background/run-status.ts +10 -1
  17. package/src/runs/background/subagent-runner.ts +600 -31
  18. package/src/runs/foreground/chain-execution.ts +325 -118
  19. package/src/runs/foreground/execution.ts +222 -10
  20. package/src/runs/foreground/subagent-executor.ts +67 -0
  21. package/src/runs/shared/acceptance-contract.ts +291 -0
  22. package/src/runs/shared/acceptance-evaluation.ts +221 -0
  23. package/src/runs/shared/acceptance-finalization.ts +161 -0
  24. package/src/runs/shared/acceptance-reports.ts +127 -0
  25. package/src/runs/shared/acceptance.ts +22 -0
  26. package/src/runs/shared/chain-outputs.ts +101 -0
  27. package/src/runs/shared/completion-guard.ts +26 -3
  28. package/src/runs/shared/dynamic-fanout.ts +293 -0
  29. package/src/runs/shared/parallel-utils.ts +31 -1
  30. package/src/runs/shared/pi-args.ts +11 -0
  31. package/src/runs/shared/structured-output.ts +77 -0
  32. package/src/runs/shared/subagent-prompt-runtime.ts +53 -3
  33. package/src/runs/shared/workflow-graph.ts +206 -0
  34. package/src/shared/formatters.ts +2 -2
  35. package/src/shared/settings.ts +53 -4
  36. package/src/shared/types.ts +250 -0
  37. package/src/slash/slash-commands.ts +41 -3
  38. package/src/tui/render.ts +162 -34
@@ -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,
@@ -59,6 +61,11 @@ import {
59
61
  } from "../../shared/types.ts";
60
62
  import { resolveModelCandidate } from "../shared/model-fallback.ts";
61
63
  import { validateFileOnlyOutputMode } from "../shared/single-output.ts";
64
+ import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
65
+ import { ChainOutputValidationError, outputEntryFromResult, resolveOutputReferences, validateChainOutputBindings } from "../shared/chain-outputs.ts";
66
+ import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
67
+ import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection, type DynamicCollectedResult } from "../shared/dynamic-fanout.ts";
68
+ import type { ChainOutputMap } from "../../shared/types.ts";
62
69
 
63
70
  interface ChainExecutionDetailsInput {
64
71
  results: SingleResult[];
@@ -67,12 +74,18 @@ interface ChainExecutionDetailsInput {
67
74
  allArtifactPaths: ArtifactPaths[];
68
75
  artifactsDir: string;
69
76
  chainAgents: string[];
77
+ chainSteps: ChainStep[];
70
78
  totalSteps: number;
71
79
  currentStepIndex?: number;
80
+ runId: string;
81
+ outputs?: ChainOutputMap;
82
+ currentFlatIndex?: number;
83
+ dynamicChildren?: Record<number, Array<{ agent: string; label?: string; flatIndex: number; itemKey: string; outputName?: string; structured?: boolean; error?: string }>>;
84
+ dynamicGroupStatuses?: Record<number, { status: "pending" | "running" | "completed" | "failed" | "paused" | "detached"; error?: string; acceptance?: SingleResult["acceptance"] }>;
72
85
  }
73
86
 
74
87
  interface ParallelChainRunInput {
75
- step: Exclude<ChainStep, SequentialStep>;
88
+ step: ParallelStep;
76
89
  parallelTemplates: string[];
77
90
  parallelBehaviors: ResolvedStepBehavior[];
78
91
  agents: AgentConfig[];
@@ -105,12 +118,20 @@ interface ParallelChainRunInput {
105
118
  lastActivityAt?: number;
106
119
  currentTool?: string;
107
120
  currentToolStartedAt?: number;
121
+ currentPath?: string;
122
+ turnCount?: number;
123
+ tokens?: number;
124
+ toolCount?: number;
108
125
  interrupt?: () => boolean;
109
126
  };
110
127
  results: SingleResult[];
111
128
  allProgress: AgentProgress[];
129
+ outputs: ChainOutputMap;
112
130
  chainAgents: string[];
131
+ chainSteps: ChainStep[];
113
132
  totalSteps: number;
133
+ dynamicChildren?: ChainExecutionDetailsInput["dynamicChildren"];
134
+ dynamicGroupStatuses?: ChainExecutionDetailsInput["dynamicGroupStatuses"];
114
135
  worktreeSetup?: WorktreeSetup;
115
136
  maxSubagentDepth: number;
116
137
  nestedRoute?: NestedRouteInfo;
@@ -125,6 +146,17 @@ function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details
125
146
  chainAgents: input.chainAgents,
126
147
  totalSteps: input.totalSteps,
127
148
  currentStepIndex: input.currentStepIndex,
149
+ outputs: input.outputs,
150
+ workflowGraph: buildWorkflowGraphSnapshot({
151
+ runId: input.runId,
152
+ mode: "chain",
153
+ steps: input.chainSteps,
154
+ results: input.results,
155
+ currentStepIndex: input.currentStepIndex,
156
+ currentFlatIndex: input.currentFlatIndex,
157
+ dynamicChildren: input.dynamicChildren,
158
+ dynamicGroupStatuses: input.dynamicGroupStatuses,
159
+ }),
128
160
  });
129
161
  }
130
162
 
@@ -191,7 +223,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
191
223
  templateHasPrevious ? undefined : input.prev,
192
224
  );
193
225
 
194
- let taskStr = taskTemplate;
226
+ let taskStr = resolveOutputReferences(taskTemplate, input.outputs);
195
227
  taskStr = taskStr.replace(/\{task\}/g, input.originalTask);
196
228
  taskStr = taskStr.replace(/\{previous\}/g, input.prev);
197
229
  taskStr = taskStr.replace(/\{chain_dir\}/g, input.chainDir);
@@ -226,6 +258,9 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
226
258
  };
227
259
  }
228
260
 
261
+ const structuredRuntime = task.outputSchema
262
+ ? createStructuredOutputRuntime(task.outputSchema, path.join(input.chainDir, "structured-output"))
263
+ : undefined;
229
264
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
230
265
  cwd: taskCwd,
231
266
  signal: input.signal,
@@ -251,6 +286,9 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
251
286
  availableModels: input.availableModels,
252
287
  preferredModelProvider: input.ctx.model?.provider,
253
288
  skills: behavior.skills === false ? [] : behavior.skills,
289
+ structuredOutput: structuredRuntime,
290
+ acceptance: task.acceptance,
291
+ acceptanceContext: { mode: "chain" },
254
292
  onUpdate: input.onUpdate
255
293
  ? (progressUpdate) => {
256
294
  const stepResults = progressUpdate.details?.results || [];
@@ -279,6 +317,17 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
279
317
  chainAgents: input.chainAgents,
280
318
  totalSteps: input.totalSteps,
281
319
  currentStepIndex: input.stepIndex,
320
+ outputs: input.outputs,
321
+ workflowGraph: buildWorkflowGraphSnapshot({
322
+ runId: input.runId,
323
+ mode: "chain",
324
+ steps: input.chainSteps,
325
+ results: input.results.concat(stepResults),
326
+ currentStepIndex: input.stepIndex,
327
+ currentFlatIndex: input.globalTaskIndex + taskIndex,
328
+ dynamicChildren: input.dynamicChildren,
329
+ dynamicGroupStatuses: input.dynamicGroupStatuses,
330
+ }),
282
331
  },
283
332
  });
284
333
  }
@@ -329,10 +378,15 @@ interface ChainExecutionParams {
329
378
  lastActivityAt?: number;
330
379
  currentTool?: string;
331
380
  currentToolStartedAt?: number;
381
+ currentPath?: string;
382
+ turnCount?: number;
383
+ tokens?: number;
384
+ toolCount?: number;
332
385
  interrupt?: () => boolean;
333
386
  };
334
387
  chainSkills?: string[];
335
388
  chainDir?: string;
389
+ dynamicFanoutMaxItems?: number;
336
390
  maxSubagentDepth: number;
337
391
  nestedRoute?: NestedRouteInfo;
338
392
  worktreeSetupHook?: string;
@@ -380,22 +434,60 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
380
434
  } = params;
381
435
  const chainSkills = chainSkillsParam ?? [];
382
436
 
437
+ const results: SingleResult[] = [];
438
+ const outputs: ChainOutputMap = {};
439
+ const dynamicChildren: ChainExecutionDetailsInput["dynamicChildren"] = {};
440
+ const dynamicGroupStatuses: ChainExecutionDetailsInput["dynamicGroupStatuses"] = {};
383
441
  const allProgress: AgentProgress[] = [];
384
442
  const allArtifactPaths: ArtifactPaths[] = [];
385
443
 
386
444
  const chainAgents: string[] = chainSteps.map((step) =>
387
445
  isParallelStep(step)
388
446
  ? `[${step.parallel.map((t) => t.agent).join("+")}]`
447
+ : isDynamicParallelStep(step)
448
+ ? `expand:${step.parallel.agent}`
389
449
  : (step as SequentialStep).agent,
390
450
  );
391
451
  const totalSteps = chainSteps.length;
392
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
+
393
469
  const firstStep = chainSteps[0]!;
394
470
  const originalTask = params.task
395
- ?? (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
+ }
396
488
 
397
489
  const chainDir = createChainDir(runId, chainDirBase);
398
- const hasParallelSteps = chainSteps.some(isParallelStep);
490
+ const hasParallelSteps = chainSteps.some((step) => isParallelStep(step) || isDynamicParallelStep(step));
399
491
  let templates: ResolvedTemplates = resolveChainTemplates(chainSteps);
400
492
  const shouldClarify = clarify !== false && ctx.hasUI && !hasParallelSteps;
401
493
  let tuiBehaviorOverrides: (BehaviorOverride | undefined)[] | undefined;
@@ -412,7 +504,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
412
504
  return {
413
505
  content: [{ type: "text", text: `Unknown agent: ${step.agent}` }],
414
506
  isError: true,
415
- details: { mode: "chain" as const, results: [] },
507
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: seqSteps.indexOf(step) })),
416
508
  };
417
509
  }
418
510
  agentConfigs.push(config);
@@ -457,7 +549,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
457
549
  removeChainDir(chainDir);
458
550
  return {
459
551
  content: [{ type: "text", text: "Chain cancelled" }],
460
- details: { mode: "chain", results: [] },
552
+ details: buildChainExecutionDetails(makeDetailsInput()),
461
553
  };
462
554
  }
463
555
 
@@ -479,7 +571,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
479
571
  });
480
572
  return {
481
573
  content: [{ type: "text", text: "Launching in background..." }],
482
- details: { mode: "chain", results: [] },
574
+ details: buildChainExecutionDetails(makeDetailsInput()),
483
575
  requestedAsync: { chain: updatedChain, chainSkills },
484
576
  };
485
577
  }
@@ -488,7 +580,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
488
580
  tuiBehaviorOverrides = result.behaviorOverrides;
489
581
  }
490
582
 
491
- const results: SingleResult[] = [];
492
583
  let prev = "";
493
584
  let globalTaskIndex = 0;
494
585
  let progressCreated = false;
@@ -506,16 +597,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
506
597
  if (worktreeTaskCwdConflict) {
507
598
  return buildChainExecutionErrorResult(
508
599
  `parallel chain step ${stepIndex + 1}: ${formatWorktreeTaskCwdConflict(worktreeTaskCwdConflict, parallelCwd)}`,
509
- {
510
- results,
511
- includeProgress,
512
- allProgress,
513
- allArtifactPaths,
514
- artifactsDir,
515
- chainAgents,
516
- totalSteps,
517
- currentStepIndex: stepIndex,
518
- },
600
+ makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }),
519
601
  );
520
602
  }
521
603
  try {
@@ -527,16 +609,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
527
609
  });
528
610
  } catch (error) {
529
611
  const message = error instanceof Error ? error.message : String(error);
530
- return buildChainExecutionErrorResult(message, {
531
- results,
532
- includeProgress,
533
- allProgress,
534
- allArtifactPaths,
535
- artifactsDir,
536
- chainAgents,
537
- totalSteps,
538
- currentStepIndex: stepIndex,
539
- });
612
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
540
613
  }
541
614
  }
542
615
 
@@ -550,16 +623,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
550
623
  ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
551
624
  : undefined;
552
625
  const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Parallel chain step ${stepIndex + 1} task ${taskIndex + 1} (${step.parallel[taskIndex]!.agent})`);
553
- if (validationError) return buildChainExecutionErrorResult(validationError, {
554
- results,
555
- includeProgress,
556
- allProgress,
557
- allArtifactPaths,
558
- artifactsDir,
559
- chainAgents,
560
- totalSteps,
561
- currentStepIndex: stepIndex,
562
- });
626
+ if (validationError) return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }));
563
627
  }
564
628
  progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
565
629
  createParallelDirs(chainDir, stepIndex, step.parallel.length, agentNames);
@@ -588,8 +652,12 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
588
652
  onUpdate,
589
653
  results,
590
654
  allProgress,
655
+ outputs,
591
656
  chainAgents,
657
+ chainSteps,
592
658
  totalSteps,
659
+ dynamicChildren,
660
+ dynamicGroupStatuses,
593
661
  controlConfig,
594
662
  onControlEvent,
595
663
  childIntercomTarget,
@@ -606,21 +674,15 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
606
674
  if (result.progress) allProgress.push(result.progress);
607
675
  if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
608
676
  }
609
-
610
- const interrupted = parallelResults.find((result) => result.interrupted);
677
+ const interruptedIndexInStep = parallelResults.findIndex((result) => result.interrupted);
678
+ const interrupted = interruptedIndexInStep >= 0 ? parallelResults[interruptedIndexInStep] : undefined;
611
679
  if (interrupted) {
612
680
  return {
613
681
  content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
614
- details: buildChainExecutionDetails({
615
- results,
616
- includeProgress,
617
- allProgress,
618
- allArtifactPaths,
619
- artifactsDir,
620
- chainAgents,
621
- totalSteps,
682
+ details: buildChainExecutionDetails(makeDetailsInput({
622
683
  currentStepIndex: stepIndex,
623
- }),
684
+ currentFlatIndex: globalTaskIndex - step.parallel.length + interruptedIndexInStep,
685
+ })),
624
686
  };
625
687
  }
626
688
  const detachedIndexInStep = parallelResults.findIndex((result) => result.detached);
@@ -628,16 +690,10 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
628
690
  if (detached) {
629
691
  return {
630
692
  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.` }],
631
- details: buildChainExecutionDetails({
632
- results,
633
- includeProgress,
634
- allProgress,
635
- allArtifactPaths,
636
- artifactsDir,
637
- chainAgents,
638
- totalSteps,
693
+ details: buildChainExecutionDetails(makeDetailsInput({
639
694
  currentStepIndex: stepIndex,
640
- }),
695
+ currentFlatIndex: globalTaskIndex - step.parallel.length + detachedIndexInStep,
696
+ })),
641
697
  };
642
698
  }
643
699
 
@@ -656,19 +712,18 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
656
712
  return {
657
713
  content: [{ type: "text", text: summary }],
658
714
  isError: true,
659
- details: buildChainExecutionDetails({
660
- results,
661
- includeProgress,
662
- allProgress,
663
- allArtifactPaths,
664
- artifactsDir,
665
- chainAgents,
666
- totalSteps,
715
+ details: buildChainExecutionDetails(makeDetailsInput({
667
716
  currentStepIndex: stepIndex,
668
- }),
717
+ currentFlatIndex: globalTaskIndex - step.parallel.length + failures[0]!.originalIndex,
718
+ })),
669
719
  };
670
720
  }
671
721
 
722
+ for (let taskIndex = 0; taskIndex < parallelResults.length; taskIndex++) {
723
+ const outputName = step.parallel[taskIndex]?.as;
724
+ if (outputName) outputs[outputName] = outputEntryFromResult(parallelResults[taskIndex]!, stepIndex);
725
+ }
726
+
672
727
  const taskResults: ParallelTaskResult[] = parallelResults.map((result, i) => {
673
728
  const outputTarget = parallelBehaviors[i]?.output;
674
729
  const outputTargetPath = typeof outputTarget === "string"
@@ -694,6 +749,184 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
694
749
  } finally {
695
750
  if (worktreeSetup) cleanupWorktrees(worktreeSetup);
696
751
  }
752
+ } else if (isDynamicParallelStep(step)) {
753
+ if (Object.hasOwn(step, "acceptance")) {
754
+ const message = `Dynamic fanout step ${stepIndex + 1} does not support group-level acceptance; set acceptance on the child template instead.`;
755
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
756
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
757
+ }
758
+ let materialized: ReturnType<typeof materializeDynamicParallelStep>;
759
+ try {
760
+ materialized = materializeDynamicParallelStep(step, outputs, stepIndex, { maxItems: params.dynamicFanoutMaxItems });
761
+ } catch (error) {
762
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
763
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
764
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
765
+ }
766
+
767
+ dynamicChildren[stepIndex] = materialized.items.map((item, itemIndex) => ({
768
+ agent: step.parallel.agent,
769
+ label: materialized.parallel[itemIndex]?.label,
770
+ flatIndex: globalTaskIndex + itemIndex,
771
+ itemKey: item.key,
772
+ structured: Boolean(step.parallel.outputSchema),
773
+ }));
774
+
775
+ if (materialized.parallel.length === 0) {
776
+ const collection: DynamicCollectedResult[] = [];
777
+ try {
778
+ validateDynamicCollection(step.collect.outputSchema, collection);
779
+ } catch (error) {
780
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
781
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
782
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
783
+ }
784
+ outputs[step.collect.as] = {
785
+ text: JSON.stringify(collection),
786
+ structured: collection,
787
+ agent: step.parallel.agent,
788
+ stepIndex,
789
+ };
790
+ dynamicGroupStatuses[stepIndex] = { status: "completed" };
791
+ prev = "Dynamic fanout produced 0 results.";
792
+ continue;
793
+ }
794
+
795
+ const dynamicParallelStep: ParallelStep = {
796
+ parallel: materialized.parallel,
797
+ concurrency: step.concurrency,
798
+ failFast: step.failFast,
799
+ };
800
+ const parallelTemplates = materialized.parallel.map((task) => task.task ?? "{previous}");
801
+ const parallelBehaviors = resolveParallelBehaviors(dynamicParallelStep.parallel, agents, stepIndex, chainSkills)
802
+ .map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? dynamicParallelStep.parallel[taskIndex]?.task, originalTask));
803
+
804
+ for (let taskIndex = 0; taskIndex < dynamicParallelStep.parallel.length; taskIndex++) {
805
+ const behavior = parallelBehaviors[taskIndex]!;
806
+ const outputPath = typeof behavior.output === "string"
807
+ ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
808
+ : undefined;
809
+ const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Dynamic chain step ${stepIndex + 1} item ${taskIndex + 1} (${dynamicParallelStep.parallel[taskIndex]!.agent})`);
810
+ if (validationError) {
811
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: validationError };
812
+ return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }));
813
+ }
814
+ }
815
+
816
+ progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
817
+ createParallelDirs(chainDir, stepIndex, dynamicParallelStep.parallel.length, dynamicParallelStep.parallel.map((task) => task.agent));
818
+ const parallelResults = await runParallelChainTasks({
819
+ step: dynamicParallelStep,
820
+ parallelTemplates,
821
+ parallelBehaviors,
822
+ agents,
823
+ stepIndex,
824
+ availableModels,
825
+ chainDir,
826
+ prev,
827
+ originalTask,
828
+ ctx,
829
+ intercomEvents,
830
+ cwd,
831
+ runId,
832
+ globalTaskIndex,
833
+ sessionDirForIndex,
834
+ sessionFileForIndex,
835
+ shareEnabled,
836
+ artifactConfig,
837
+ artifactsDir,
838
+ signal,
839
+ onUpdate,
840
+ results,
841
+ allProgress,
842
+ outputs,
843
+ chainAgents,
844
+ chainSteps,
845
+ totalSteps,
846
+ dynamicChildren,
847
+ dynamicGroupStatuses,
848
+ controlConfig,
849
+ onControlEvent,
850
+ childIntercomTarget,
851
+ orchestratorIntercomTarget,
852
+ foregroundControl,
853
+ nestedRoute: params.nestedRoute,
854
+ maxSubagentDepth: params.maxSubagentDepth,
855
+ });
856
+ globalTaskIndex += dynamicParallelStep.parallel.length;
857
+
858
+ for (const result of parallelResults) {
859
+ results.push(result);
860
+ if (result.progress) allProgress.push(result.progress);
861
+ if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
862
+ }
863
+ const collected = collectDynamicResults(step, materialized.items, parallelResults);
864
+ const interruptedIndexInStep = parallelResults.findIndex((result) => result.interrupted);
865
+ const interrupted = interruptedIndexInStep >= 0 ? parallelResults[interruptedIndexInStep] : undefined;
866
+ if (interrupted) {
867
+ return {
868
+ content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
869
+ details: buildChainExecutionDetails(makeDetailsInput({
870
+ currentStepIndex: stepIndex,
871
+ currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + interruptedIndexInStep,
872
+ })),
873
+ };
874
+ }
875
+ const detachedIndexInStep = parallelResults.findIndex((result) => result.detached);
876
+ const detached = detachedIndexInStep >= 0 ? parallelResults[detachedIndexInStep] : undefined;
877
+ if (detached) {
878
+ return {
879
+ 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.` }],
880
+ details: buildChainExecutionDetails(makeDetailsInput({
881
+ currentStepIndex: stepIndex,
882
+ currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + detachedIndexInStep,
883
+ })),
884
+ };
885
+ }
886
+ const failures = parallelResults
887
+ .map((result, originalIndex) => ({ ...result, originalIndex }))
888
+ .filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
889
+ if (failures.length > 0) {
890
+ const failureSummary = failures
891
+ .map((failure) => `- Item ${failure.originalIndex + 1} (${failure.agent}, key ${materialized.items[failure.originalIndex]?.key ?? failure.originalIndex}): ${failure.error || "failed"}`)
892
+ .join("\n");
893
+ const errorMsg = `Dynamic step ${stepIndex + 1} failed:\n${failureSummary}`;
894
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: errorMsg };
895
+ const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
896
+ index: stepIndex,
897
+ error: errorMsg,
898
+ });
899
+ return {
900
+ content: [{ type: "text", text: summary }],
901
+ isError: true,
902
+ details: buildChainExecutionDetails(makeDetailsInput({
903
+ currentStepIndex: stepIndex,
904
+ currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + failures[0]!.originalIndex,
905
+ })),
906
+ };
907
+ }
908
+ try {
909
+ validateDynamicCollection(step.collect.outputSchema, collected);
910
+ } catch (error) {
911
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
912
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
913
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }));
914
+ }
915
+ outputs[step.collect.as] = {
916
+ text: JSON.stringify(collected),
917
+ structured: collected,
918
+ agent: step.parallel.agent,
919
+ stepIndex,
920
+ };
921
+ dynamicGroupStatuses[stepIndex] = { status: "completed" };
922
+ const taskResults: ParallelTaskResult[] = parallelResults.map((result, i) => ({
923
+ agent: result.agent,
924
+ taskIndex: i,
925
+ output: getSingleResultOutput(result),
926
+ exitCode: result.exitCode,
927
+ error: result.error,
928
+ }));
929
+ prev = aggregateParallelOutputs(taskResults, (i, agent) => `=== Dynamic Item ${i + 1} (${agent}, key ${materialized.items[i]?.key ?? i}) ===`);
697
930
  } else {
698
931
  const seqStep = step as SequentialStep;
699
932
  const stepTemplate = stepTemplates as string;
@@ -704,7 +937,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
704
937
  return {
705
938
  content: [{ type: "text", text: `Unknown agent: ${seqStep.agent}` }],
706
939
  isError: true,
707
- details: { mode: "chain" as const, results: [] },
940
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex })),
708
941
  };
709
942
  }
710
943
 
@@ -734,7 +967,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
734
967
  templateHasPrevious ? undefined : prev,
735
968
  );
736
969
 
737
- let stepTask = stepTemplate;
970
+ let stepTask = resolveOutputReferences(stepTemplate, outputs);
738
971
  stepTask = stepTask.replace(/\{task\}/g, originalTask);
739
972
  stepTask = stepTask.replace(/\{previous\}/g, prev);
740
973
  stepTask = stepTask.replace(/\{chain_dir\}/g, chainDir);
@@ -751,16 +984,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
751
984
  : undefined;
752
985
  const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Chain step ${stepIndex + 1} (${seqStep.agent})`);
753
986
  if (validationError) {
754
- return buildChainExecutionErrorResult(validationError, {
755
- results,
756
- includeProgress,
757
- allProgress,
758
- allArtifactPaths,
759
- artifactsDir,
760
- chainAgents,
761
- totalSteps,
762
- currentStepIndex: stepIndex,
763
- });
987
+ return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
764
988
  }
765
989
  const maxSubagentDepth = resolveChildMaxSubagentDepth(params.maxSubagentDepth, agentConfig.maxSubagentDepth);
766
990
  const interruptController = new AbortController();
@@ -778,6 +1002,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
778
1002
  };
779
1003
  }
780
1004
 
1005
+ const structuredRuntime = seqStep.outputSchema
1006
+ ? createStructuredOutputRuntime(seqStep.outputSchema, path.join(chainDir, "structured-output"))
1007
+ : undefined;
781
1008
  const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
782
1009
  cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
783
1010
  signal,
@@ -803,6 +1030,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
803
1030
  availableModels,
804
1031
  preferredModelProvider: ctx.model?.provider,
805
1032
  skills: behavior.skills === false ? [] : behavior.skills,
1033
+ structuredOutput: structuredRuntime,
1034
+ acceptance: seqStep.acceptance,
1035
+ acceptanceContext: { mode: "chain" },
806
1036
  onUpdate: onUpdate
807
1037
  ? (p) => {
808
1038
  const stepResults = p.details?.results || [];
@@ -831,6 +1061,17 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
831
1061
  chainAgents,
832
1062
  totalSteps,
833
1063
  currentStepIndex: stepIndex,
1064
+ outputs,
1065
+ workflowGraph: buildWorkflowGraphSnapshot({
1066
+ runId,
1067
+ mode: "chain",
1068
+ steps: chainSteps,
1069
+ results: results.concat(stepResults),
1070
+ currentStepIndex: stepIndex,
1071
+ currentFlatIndex: globalTaskIndex,
1072
+ dynamicChildren,
1073
+ dynamicGroupStatuses,
1074
+ }),
834
1075
  },
835
1076
  });
836
1077
  }
@@ -850,31 +1091,13 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
850
1091
  if (r.interrupted) {
851
1092
  return {
852
1093
  content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${r.agent}). Waiting for explicit next action.` }],
853
- details: buildChainExecutionDetails({
854
- results,
855
- includeProgress,
856
- allProgress,
857
- allArtifactPaths,
858
- artifactsDir,
859
- chainAgents,
860
- totalSteps,
861
- currentStepIndex: stepIndex,
862
- }),
1094
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
863
1095
  };
864
1096
  }
865
1097
  if (r.detached) {
866
1098
  return {
867
1099
  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.` }],
868
- details: buildChainExecutionDetails({
869
- results,
870
- includeProgress,
871
- allProgress,
872
- allArtifactPaths,
873
- artifactsDir,
874
- chainAgents,
875
- totalSteps,
876
- currentStepIndex: stepIndex,
877
- }),
1100
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
878
1101
  };
879
1102
  }
880
1103
 
@@ -885,16 +1108,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
885
1108
  });
886
1109
  return {
887
1110
  content: [{ type: "text", text: summary }],
888
- details: buildChainExecutionDetails({
889
- results,
890
- includeProgress,
891
- allProgress,
892
- allArtifactPaths,
893
- artifactsDir,
894
- chainAgents,
895
- totalSteps,
896
- currentStepIndex: stepIndex,
897
- }),
1111
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
898
1112
  isError: true,
899
1113
  };
900
1114
  }
@@ -917,6 +1131,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
917
1131
  }
918
1132
  }
919
1133
 
1134
+ if (seqStep.as) outputs[seqStep.as] = outputEntryFromResult(r, stepIndex);
920
1135
  prev = getSingleResultOutput(r);
921
1136
  }
922
1137
  }
@@ -925,14 +1140,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
925
1140
 
926
1141
  return {
927
1142
  content: [{ type: "text", text: summary }],
928
- details: buildChainExecutionDetails({
929
- results,
930
- includeProgress,
931
- allProgress,
932
- allArtifactPaths,
933
- artifactsDir,
934
- chainAgents,
935
- totalSteps,
936
- }),
1143
+ details: buildChainExecutionDetails(makeDetailsInput()),
937
1144
  };
938
1145
  }