pi-subagents 0.24.4 → 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 (48) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +145 -27
  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/prompts/review-loop.md +1 -1
  7. package/skills/pi-subagents/SKILL.md +71 -20
  8. package/src/agents/agent-management.ts +57 -15
  9. package/src/agents/agent-serializer.ts +3 -2
  10. package/src/agents/agents.ts +47 -16
  11. package/src/agents/chain-serializer.ts +120 -0
  12. package/src/extension/fanout-child.ts +171 -0
  13. package/src/extension/index.ts +7 -2
  14. package/src/extension/schemas.ts +138 -5
  15. package/src/intercom/result-intercom.ts +108 -0
  16. package/src/runs/background/async-execution.ts +185 -10
  17. package/src/runs/background/async-job-tracker.ts +41 -6
  18. package/src/runs/background/async-resume.ts +28 -15
  19. package/src/runs/background/async-status.ts +71 -31
  20. package/src/runs/background/result-watcher.ts +111 -54
  21. package/src/runs/background/run-id-resolver.ts +83 -0
  22. package/src/runs/background/run-status.ts +89 -4
  23. package/src/runs/background/stale-run-reconciler.ts +46 -1
  24. package/src/runs/background/subagent-runner.ts +648 -42
  25. package/src/runs/foreground/chain-execution.ts +331 -118
  26. package/src/runs/foreground/execution.ts +226 -10
  27. package/src/runs/foreground/subagent-executor.ts +377 -14
  28. package/src/runs/shared/acceptance-contract.ts +291 -0
  29. package/src/runs/shared/acceptance-evaluation.ts +221 -0
  30. package/src/runs/shared/acceptance-finalization.ts +161 -0
  31. package/src/runs/shared/acceptance-reports.ts +127 -0
  32. package/src/runs/shared/acceptance.ts +22 -0
  33. package/src/runs/shared/chain-outputs.ts +101 -0
  34. package/src/runs/shared/completion-guard.ts +26 -3
  35. package/src/runs/shared/dynamic-fanout.ts +293 -0
  36. package/src/runs/shared/nested-events.ts +819 -0
  37. package/src/runs/shared/nested-path.ts +52 -0
  38. package/src/runs/shared/nested-render.ts +115 -0
  39. package/src/runs/shared/parallel-utils.ts +31 -1
  40. package/src/runs/shared/pi-args.ts +73 -5
  41. package/src/runs/shared/structured-output.ts +77 -0
  42. package/src/runs/shared/subagent-prompt-runtime.ts +77 -7
  43. package/src/runs/shared/workflow-graph.ts +206 -0
  44. package/src/shared/formatters.ts +2 -2
  45. package/src/shared/settings.ts +53 -4
  46. package/src/shared/types.ts +345 -0
  47. package/src/slash/slash-commands.ts +41 -3
  48. package/src/tui/render.ts +268 -43
@@ -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,
@@ -51,6 +53,7 @@ import {
51
53
  type ControlEvent,
52
54
  type Details,
53
55
  type IntercomEventBus,
56
+ type NestedRouteInfo,
54
57
  type ResolvedControlConfig,
55
58
  type SingleResult,
56
59
  MAX_CONCURRENCY,
@@ -58,6 +61,11 @@ import {
58
61
  } from "../../shared/types.ts";
59
62
  import { resolveModelCandidate } from "../shared/model-fallback.ts";
60
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";
61
69
 
62
70
  interface ChainExecutionDetailsInput {
63
71
  results: SingleResult[];
@@ -66,12 +74,18 @@ interface ChainExecutionDetailsInput {
66
74
  allArtifactPaths: ArtifactPaths[];
67
75
  artifactsDir: string;
68
76
  chainAgents: string[];
77
+ chainSteps: ChainStep[];
69
78
  totalSteps: number;
70
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"] }>;
71
85
  }
72
86
 
73
87
  interface ParallelChainRunInput {
74
- step: Exclude<ChainStep, SequentialStep>;
88
+ step: ParallelStep;
75
89
  parallelTemplates: string[];
76
90
  parallelBehaviors: ResolvedStepBehavior[];
77
91
  agents: AgentConfig[];
@@ -104,14 +118,23 @@ interface ParallelChainRunInput {
104
118
  lastActivityAt?: number;
105
119
  currentTool?: string;
106
120
  currentToolStartedAt?: number;
121
+ currentPath?: string;
122
+ turnCount?: number;
123
+ tokens?: number;
124
+ toolCount?: number;
107
125
  interrupt?: () => boolean;
108
126
  };
109
127
  results: SingleResult[];
110
128
  allProgress: AgentProgress[];
129
+ outputs: ChainOutputMap;
111
130
  chainAgents: string[];
131
+ chainSteps: ChainStep[];
112
132
  totalSteps: number;
133
+ dynamicChildren?: ChainExecutionDetailsInput["dynamicChildren"];
134
+ dynamicGroupStatuses?: ChainExecutionDetailsInput["dynamicGroupStatuses"];
113
135
  worktreeSetup?: WorktreeSetup;
114
136
  maxSubagentDepth: number;
137
+ nestedRoute?: NestedRouteInfo;
115
138
  }
116
139
 
117
140
  function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details {
@@ -123,6 +146,17 @@ function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details
123
146
  chainAgents: input.chainAgents,
124
147
  totalSteps: input.totalSteps,
125
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
+ }),
126
160
  });
127
161
  }
128
162
 
@@ -189,7 +223,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
189
223
  templateHasPrevious ? undefined : input.prev,
190
224
  );
191
225
 
192
- let taskStr = taskTemplate;
226
+ let taskStr = resolveOutputReferences(taskTemplate, input.outputs);
193
227
  taskStr = taskStr.replace(/\{task\}/g, input.originalTask);
194
228
  taskStr = taskStr.replace(/\{previous\}/g, input.prev);
195
229
  taskStr = taskStr.replace(/\{chain_dir\}/g, input.chainDir);
@@ -224,6 +258,9 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
224
258
  };
225
259
  }
226
260
 
261
+ const structuredRuntime = task.outputSchema
262
+ ? createStructuredOutputRuntime(task.outputSchema, path.join(input.chainDir, "structured-output"))
263
+ : undefined;
227
264
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
228
265
  cwd: taskCwd,
229
266
  signal: input.signal,
@@ -244,10 +281,14 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
244
281
  onControlEvent: input.onControlEvent,
245
282
  intercomSessionName: input.childIntercomTarget?.(task.agent, input.globalTaskIndex + taskIndex),
246
283
  orchestratorIntercomTarget: input.orchestratorIntercomTarget,
284
+ nestedRoute: input.nestedRoute,
247
285
  modelOverride: effectiveModel,
248
286
  availableModels: input.availableModels,
249
287
  preferredModelProvider: input.ctx.model?.provider,
250
288
  skills: behavior.skills === false ? [] : behavior.skills,
289
+ structuredOutput: structuredRuntime,
290
+ acceptance: task.acceptance,
291
+ acceptanceContext: { mode: "chain" },
251
292
  onUpdate: input.onUpdate
252
293
  ? (progressUpdate) => {
253
294
  const stepResults = progressUpdate.details?.results || [];
@@ -276,6 +317,17 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
276
317
  chainAgents: input.chainAgents,
277
318
  totalSteps: input.totalSteps,
278
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
+ }),
279
331
  },
280
332
  });
281
333
  }
@@ -326,11 +378,17 @@ interface ChainExecutionParams {
326
378
  lastActivityAt?: number;
327
379
  currentTool?: string;
328
380
  currentToolStartedAt?: number;
381
+ currentPath?: string;
382
+ turnCount?: number;
383
+ tokens?: number;
384
+ toolCount?: number;
329
385
  interrupt?: () => boolean;
330
386
  };
331
387
  chainSkills?: string[];
332
388
  chainDir?: string;
389
+ dynamicFanoutMaxItems?: number;
333
390
  maxSubagentDepth: number;
391
+ nestedRoute?: NestedRouteInfo;
334
392
  worktreeSetupHook?: string;
335
393
  worktreeSetupHookTimeoutMs?: number;
336
394
  }
@@ -376,22 +434,60 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
376
434
  } = params;
377
435
  const chainSkills = chainSkillsParam ?? [];
378
436
 
437
+ const results: SingleResult[] = [];
438
+ const outputs: ChainOutputMap = {};
439
+ const dynamicChildren: ChainExecutionDetailsInput["dynamicChildren"] = {};
440
+ const dynamicGroupStatuses: ChainExecutionDetailsInput["dynamicGroupStatuses"] = {};
379
441
  const allProgress: AgentProgress[] = [];
380
442
  const allArtifactPaths: ArtifactPaths[] = [];
381
443
 
382
444
  const chainAgents: string[] = chainSteps.map((step) =>
383
445
  isParallelStep(step)
384
446
  ? `[${step.parallel.map((t) => t.agent).join("+")}]`
447
+ : isDynamicParallelStep(step)
448
+ ? `expand:${step.parallel.agent}`
385
449
  : (step as SequentialStep).agent,
386
450
  );
387
451
  const totalSteps = chainSteps.length;
388
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
+
389
469
  const firstStep = chainSteps[0]!;
390
470
  const originalTask = params.task
391
- ?? (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
+ }
392
488
 
393
489
  const chainDir = createChainDir(runId, chainDirBase);
394
- const hasParallelSteps = chainSteps.some(isParallelStep);
490
+ const hasParallelSteps = chainSteps.some((step) => isParallelStep(step) || isDynamicParallelStep(step));
395
491
  let templates: ResolvedTemplates = resolveChainTemplates(chainSteps);
396
492
  const shouldClarify = clarify !== false && ctx.hasUI && !hasParallelSteps;
397
493
  let tuiBehaviorOverrides: (BehaviorOverride | undefined)[] | undefined;
@@ -408,7 +504,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
408
504
  return {
409
505
  content: [{ type: "text", text: `Unknown agent: ${step.agent}` }],
410
506
  isError: true,
411
- details: { mode: "chain" as const, results: [] },
507
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: seqSteps.indexOf(step) })),
412
508
  };
413
509
  }
414
510
  agentConfigs.push(config);
@@ -453,7 +549,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
453
549
  removeChainDir(chainDir);
454
550
  return {
455
551
  content: [{ type: "text", text: "Chain cancelled" }],
456
- details: { mode: "chain", results: [] },
552
+ details: buildChainExecutionDetails(makeDetailsInput()),
457
553
  };
458
554
  }
459
555
 
@@ -475,7 +571,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
475
571
  });
476
572
  return {
477
573
  content: [{ type: "text", text: "Launching in background..." }],
478
- details: { mode: "chain", results: [] },
574
+ details: buildChainExecutionDetails(makeDetailsInput()),
479
575
  requestedAsync: { chain: updatedChain, chainSkills },
480
576
  };
481
577
  }
@@ -484,7 +580,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
484
580
  tuiBehaviorOverrides = result.behaviorOverrides;
485
581
  }
486
582
 
487
- const results: SingleResult[] = [];
488
583
  let prev = "";
489
584
  let globalTaskIndex = 0;
490
585
  let progressCreated = false;
@@ -502,16 +597,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
502
597
  if (worktreeTaskCwdConflict) {
503
598
  return buildChainExecutionErrorResult(
504
599
  `parallel chain step ${stepIndex + 1}: ${formatWorktreeTaskCwdConflict(worktreeTaskCwdConflict, parallelCwd)}`,
505
- {
506
- results,
507
- includeProgress,
508
- allProgress,
509
- allArtifactPaths,
510
- artifactsDir,
511
- chainAgents,
512
- totalSteps,
513
- currentStepIndex: stepIndex,
514
- },
600
+ makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }),
515
601
  );
516
602
  }
517
603
  try {
@@ -523,16 +609,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
523
609
  });
524
610
  } catch (error) {
525
611
  const message = error instanceof Error ? error.message : String(error);
526
- return buildChainExecutionErrorResult(message, {
527
- results,
528
- includeProgress,
529
- allProgress,
530
- allArtifactPaths,
531
- artifactsDir,
532
- chainAgents,
533
- totalSteps,
534
- currentStepIndex: stepIndex,
535
- });
612
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
536
613
  }
537
614
  }
538
615
 
@@ -546,16 +623,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
546
623
  ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
547
624
  : undefined;
548
625
  const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Parallel chain step ${stepIndex + 1} task ${taskIndex + 1} (${step.parallel[taskIndex]!.agent})`);
549
- if (validationError) return buildChainExecutionErrorResult(validationError, {
550
- results,
551
- includeProgress,
552
- allProgress,
553
- allArtifactPaths,
554
- artifactsDir,
555
- chainAgents,
556
- totalSteps,
557
- currentStepIndex: stepIndex,
558
- });
626
+ if (validationError) return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }));
559
627
  }
560
628
  progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
561
629
  createParallelDirs(chainDir, stepIndex, step.parallel.length, agentNames);
@@ -584,13 +652,18 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
584
652
  onUpdate,
585
653
  results,
586
654
  allProgress,
655
+ outputs,
587
656
  chainAgents,
657
+ chainSteps,
588
658
  totalSteps,
659
+ dynamicChildren,
660
+ dynamicGroupStatuses,
589
661
  controlConfig,
590
662
  onControlEvent,
591
663
  childIntercomTarget,
592
664
  orchestratorIntercomTarget,
593
665
  foregroundControl,
666
+ nestedRoute: params.nestedRoute,
594
667
  worktreeSetup,
595
668
  maxSubagentDepth: params.maxSubagentDepth,
596
669
  });
@@ -601,21 +674,15 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
601
674
  if (result.progress) allProgress.push(result.progress);
602
675
  if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
603
676
  }
604
-
605
- const interrupted = parallelResults.find((result) => result.interrupted);
677
+ const interruptedIndexInStep = parallelResults.findIndex((result) => result.interrupted);
678
+ const interrupted = interruptedIndexInStep >= 0 ? parallelResults[interruptedIndexInStep] : undefined;
606
679
  if (interrupted) {
607
680
  return {
608
681
  content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
609
- details: buildChainExecutionDetails({
610
- results,
611
- includeProgress,
612
- allProgress,
613
- allArtifactPaths,
614
- artifactsDir,
615
- chainAgents,
616
- totalSteps,
682
+ details: buildChainExecutionDetails(makeDetailsInput({
617
683
  currentStepIndex: stepIndex,
618
- }),
684
+ currentFlatIndex: globalTaskIndex - step.parallel.length + interruptedIndexInStep,
685
+ })),
619
686
  };
620
687
  }
621
688
  const detachedIndexInStep = parallelResults.findIndex((result) => result.detached);
@@ -623,16 +690,10 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
623
690
  if (detached) {
624
691
  return {
625
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.` }],
626
- details: buildChainExecutionDetails({
627
- results,
628
- includeProgress,
629
- allProgress,
630
- allArtifactPaths,
631
- artifactsDir,
632
- chainAgents,
633
- totalSteps,
693
+ details: buildChainExecutionDetails(makeDetailsInput({
634
694
  currentStepIndex: stepIndex,
635
- }),
695
+ currentFlatIndex: globalTaskIndex - step.parallel.length + detachedIndexInStep,
696
+ })),
636
697
  };
637
698
  }
638
699
 
@@ -651,19 +712,18 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
651
712
  return {
652
713
  content: [{ type: "text", text: summary }],
653
714
  isError: true,
654
- details: buildChainExecutionDetails({
655
- results,
656
- includeProgress,
657
- allProgress,
658
- allArtifactPaths,
659
- artifactsDir,
660
- chainAgents,
661
- totalSteps,
715
+ details: buildChainExecutionDetails(makeDetailsInput({
662
716
  currentStepIndex: stepIndex,
663
- }),
717
+ currentFlatIndex: globalTaskIndex - step.parallel.length + failures[0]!.originalIndex,
718
+ })),
664
719
  };
665
720
  }
666
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
+
667
727
  const taskResults: ParallelTaskResult[] = parallelResults.map((result, i) => {
668
728
  const outputTarget = parallelBehaviors[i]?.output;
669
729
  const outputTargetPath = typeof outputTarget === "string"
@@ -689,6 +749,184 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
689
749
  } finally {
690
750
  if (worktreeSetup) cleanupWorktrees(worktreeSetup);
691
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}) ===`);
692
930
  } else {
693
931
  const seqStep = step as SequentialStep;
694
932
  const stepTemplate = stepTemplates as string;
@@ -699,7 +937,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
699
937
  return {
700
938
  content: [{ type: "text", text: `Unknown agent: ${seqStep.agent}` }],
701
939
  isError: true,
702
- details: { mode: "chain" as const, results: [] },
940
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex })),
703
941
  };
704
942
  }
705
943
 
@@ -729,7 +967,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
729
967
  templateHasPrevious ? undefined : prev,
730
968
  );
731
969
 
732
- let stepTask = stepTemplate;
970
+ let stepTask = resolveOutputReferences(stepTemplate, outputs);
733
971
  stepTask = stepTask.replace(/\{task\}/g, originalTask);
734
972
  stepTask = stepTask.replace(/\{previous\}/g, prev);
735
973
  stepTask = stepTask.replace(/\{chain_dir\}/g, chainDir);
@@ -746,16 +984,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
746
984
  : undefined;
747
985
  const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Chain step ${stepIndex + 1} (${seqStep.agent})`);
748
986
  if (validationError) {
749
- return buildChainExecutionErrorResult(validationError, {
750
- results,
751
- includeProgress,
752
- allProgress,
753
- allArtifactPaths,
754
- artifactsDir,
755
- chainAgents,
756
- totalSteps,
757
- currentStepIndex: stepIndex,
758
- });
987
+ return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
759
988
  }
760
989
  const maxSubagentDepth = resolveChildMaxSubagentDepth(params.maxSubagentDepth, agentConfig.maxSubagentDepth);
761
990
  const interruptController = new AbortController();
@@ -773,6 +1002,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
773
1002
  };
774
1003
  }
775
1004
 
1005
+ const structuredRuntime = seqStep.outputSchema
1006
+ ? createStructuredOutputRuntime(seqStep.outputSchema, path.join(chainDir, "structured-output"))
1007
+ : undefined;
776
1008
  const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
777
1009
  cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
778
1010
  signal,
@@ -793,10 +1025,14 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
793
1025
  onControlEvent,
794
1026
  intercomSessionName: childIntercomTarget?.(seqStep.agent, globalTaskIndex),
795
1027
  orchestratorIntercomTarget,
1028
+ nestedRoute: params.nestedRoute,
796
1029
  modelOverride: effectiveModel,
797
1030
  availableModels,
798
1031
  preferredModelProvider: ctx.model?.provider,
799
1032
  skills: behavior.skills === false ? [] : behavior.skills,
1033
+ structuredOutput: structuredRuntime,
1034
+ acceptance: seqStep.acceptance,
1035
+ acceptanceContext: { mode: "chain" },
800
1036
  onUpdate: onUpdate
801
1037
  ? (p) => {
802
1038
  const stepResults = p.details?.results || [];
@@ -825,6 +1061,17 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
825
1061
  chainAgents,
826
1062
  totalSteps,
827
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
+ }),
828
1075
  },
829
1076
  });
830
1077
  }
@@ -844,31 +1091,13 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
844
1091
  if (r.interrupted) {
845
1092
  return {
846
1093
  content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${r.agent}). Waiting for explicit next action.` }],
847
- details: buildChainExecutionDetails({
848
- results,
849
- includeProgress,
850
- allProgress,
851
- allArtifactPaths,
852
- artifactsDir,
853
- chainAgents,
854
- totalSteps,
855
- currentStepIndex: stepIndex,
856
- }),
1094
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
857
1095
  };
858
1096
  }
859
1097
  if (r.detached) {
860
1098
  return {
861
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.` }],
862
- details: buildChainExecutionDetails({
863
- results,
864
- includeProgress,
865
- allProgress,
866
- allArtifactPaths,
867
- artifactsDir,
868
- chainAgents,
869
- totalSteps,
870
- currentStepIndex: stepIndex,
871
- }),
1100
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
872
1101
  };
873
1102
  }
874
1103
 
@@ -879,16 +1108,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
879
1108
  });
880
1109
  return {
881
1110
  content: [{ type: "text", text: summary }],
882
- details: buildChainExecutionDetails({
883
- results,
884
- includeProgress,
885
- allProgress,
886
- allArtifactPaths,
887
- artifactsDir,
888
- chainAgents,
889
- totalSteps,
890
- currentStepIndex: stepIndex,
891
- }),
1111
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
892
1112
  isError: true,
893
1113
  };
894
1114
  }
@@ -911,6 +1131,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
911
1131
  }
912
1132
  }
913
1133
 
1134
+ if (seqStep.as) outputs[seqStep.as] = outputEntryFromResult(r, stepIndex);
914
1135
  prev = getSingleResultOutput(r);
915
1136
  }
916
1137
  }
@@ -919,14 +1140,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
919
1140
 
920
1141
  return {
921
1142
  content: [{ type: "text", text: summary }],
922
- details: buildChainExecutionDetails({
923
- results,
924
- includeProgress,
925
- allProgress,
926
- allArtifactPaths,
927
- artifactsDir,
928
- chainAgents,
929
- totalSteps,
930
- }),
1143
+ details: buildChainExecutionDetails(makeDetailsInput()),
931
1144
  };
932
1145
  }