pi-cohort 2.0.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 (92) hide show
  1. package/CHANGELOG.md +1151 -0
  2. package/LICENSE +22 -0
  3. package/README.md +1220 -0
  4. package/agents/context-builder.md +45 -0
  5. package/agents/delegate.md +12 -0
  6. package/agents/oracle.md +73 -0
  7. package/agents/planner.md +55 -0
  8. package/agents/reviewer.md +91 -0
  9. package/agents/scout.md +50 -0
  10. package/agents/worker.md +67 -0
  11. package/package.json +87 -0
  12. package/prompts/gather-context-and-clarify.md +13 -0
  13. package/prompts/parallel-cleanup.md +59 -0
  14. package/prompts/parallel-context-build.md +55 -0
  15. package/prompts/parallel-handoff-plan.md +61 -0
  16. package/prompts/parallel-review.md +54 -0
  17. package/prompts/review-loop.md +41 -0
  18. package/skills/pi-cohort/SKILL.md +818 -0
  19. package/src/agents/agent-management.ts +685 -0
  20. package/src/agents/agent-scope.ts +6 -0
  21. package/src/agents/agent-selection.ts +23 -0
  22. package/src/agents/agent-serializer.ts +83 -0
  23. package/src/agents/agents.ts +1141 -0
  24. package/src/agents/chain-serializer.ts +251 -0
  25. package/src/agents/frontmatter.ts +29 -0
  26. package/src/agents/identity.ts +30 -0
  27. package/src/agents/skills.ts +632 -0
  28. package/src/extension/config.ts +16 -0
  29. package/src/extension/control-notices.ts +92 -0
  30. package/src/extension/doctor.ts +236 -0
  31. package/src/extension/fanout-child.ts +170 -0
  32. package/src/extension/grand-total.ts +109 -0
  33. package/src/extension/index.ts +630 -0
  34. package/src/extension/schemas.ts +306 -0
  35. package/src/intercom/intercom-bridge.ts +379 -0
  36. package/src/intercom/result-intercom.ts +377 -0
  37. package/src/runs/background/async-execution.ts +796 -0
  38. package/src/runs/background/async-job-tracker.ts +320 -0
  39. package/src/runs/background/async-resume.ts +345 -0
  40. package/src/runs/background/async-status.ts +335 -0
  41. package/src/runs/background/completion-dedupe.ts +63 -0
  42. package/src/runs/background/notify.ts +108 -0
  43. package/src/runs/background/parallel-groups.ts +45 -0
  44. package/src/runs/background/result-watcher.ts +307 -0
  45. package/src/runs/background/run-id-resolver.ts +83 -0
  46. package/src/runs/background/run-status.ts +272 -0
  47. package/src/runs/background/stale-run-reconciler.ts +336 -0
  48. package/src/runs/background/subagent-runner.ts +2326 -0
  49. package/src/runs/background/top-level-async.ts +13 -0
  50. package/src/runs/foreground/chain-clarify.ts +1333 -0
  51. package/src/runs/foreground/chain-execution.ts +1187 -0
  52. package/src/runs/foreground/execution.ts +1028 -0
  53. package/src/runs/foreground/subagent-executor.ts +2580 -0
  54. package/src/runs/shared/acceptance.ts +605 -0
  55. package/src/runs/shared/chain-outputs.ts +101 -0
  56. package/src/runs/shared/completion-guard.ts +143 -0
  57. package/src/runs/shared/dynamic-fanout.ts +293 -0
  58. package/src/runs/shared/long-running-guard.ts +175 -0
  59. package/src/runs/shared/model-fallback.ts +103 -0
  60. package/src/runs/shared/nested-events.ts +822 -0
  61. package/src/runs/shared/nested-path.ts +52 -0
  62. package/src/runs/shared/nested-render.ts +115 -0
  63. package/src/runs/shared/parallel-utils.ts +136 -0
  64. package/src/runs/shared/pi-args.ts +221 -0
  65. package/src/runs/shared/pi-spawn.ts +115 -0
  66. package/src/runs/shared/run-history.ts +60 -0
  67. package/src/runs/shared/single-output.ts +164 -0
  68. package/src/runs/shared/structured-output.ts +77 -0
  69. package/src/runs/shared/subagent-control.ts +287 -0
  70. package/src/runs/shared/subagent-prompt-runtime.ts +220 -0
  71. package/src/runs/shared/workflow-graph.ts +206 -0
  72. package/src/runs/shared/worktree.ts +577 -0
  73. package/src/shared/artifacts.ts +98 -0
  74. package/src/shared/atomic-json.ts +16 -0
  75. package/src/shared/file-coalescer.ts +40 -0
  76. package/src/shared/fork-context.ts +76 -0
  77. package/src/shared/formatters.ts +133 -0
  78. package/src/shared/jsonl-writer.ts +81 -0
  79. package/src/shared/model-info.ts +78 -0
  80. package/src/shared/post-exit-stdio-guard.ts +85 -0
  81. package/src/shared/session-identity.ts +10 -0
  82. package/src/shared/session-tokens.ts +46 -0
  83. package/src/shared/settings.ts +447 -0
  84. package/src/shared/status-format.ts +59 -0
  85. package/src/shared/types.ts +1072 -0
  86. package/src/shared/utils.ts +451 -0
  87. package/src/slash/prompt-template-bridge.ts +397 -0
  88. package/src/slash/slash-bridge.ts +174 -0
  89. package/src/slash/slash-commands.ts +567 -0
  90. package/src/slash/slash-live-state.ts +292 -0
  91. package/src/tui/render-helpers.ts +80 -0
  92. package/src/tui/render.ts +1476 -0
@@ -0,0 +1,1187 @@
1
+ /**
2
+ * Chain execution logic for subagent tool
3
+ */
4
+
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
8
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
9
+ import type { AgentConfig } from "../../agents/agents.ts";
10
+ import { ChainClarifyComponent, type ChainClarifyResult, type BehaviorOverride } from "./chain-clarify.ts";
11
+ import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
12
+ import {
13
+ resolveChainTemplates,
14
+ createChainDir,
15
+ removeChainDir,
16
+ resolveStepBehavior,
17
+ resolveParallelBehaviors,
18
+ buildChainInstructions,
19
+ writeInitialProgressFile,
20
+ createParallelDirs,
21
+ suppressProgressForReadOnlyTask,
22
+ aggregateParallelOutputs,
23
+ isDynamicParallelStep,
24
+ isParallelStep,
25
+ type StepOverrides,
26
+ type ChainStep,
27
+ type ParallelStep,
28
+ type SequentialStep,
29
+ type ParallelTaskResult,
30
+ type ResolvedStepBehavior,
31
+ type ResolvedTemplates,
32
+ } from "../../shared/settings.ts";
33
+ import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
34
+ import { INTERCOM_BRIDGE_MARKER } from "../../intercom/intercom-bridge.ts";
35
+ import { runSync } from "./execution.ts";
36
+ import { buildChainSummary } from "../../shared/formatters.ts";
37
+ import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd } from "../../shared/utils.ts";
38
+ import { recordRun } from "../shared/run-history.ts";
39
+ import {
40
+ cleanupWorktrees,
41
+ createWorktrees,
42
+ diffWorktrees,
43
+ findWorktreeTaskCwdConflict,
44
+ formatWorktreeDiffSummary,
45
+ formatWorktreeTaskCwdConflict,
46
+ type WorktreeSetup,
47
+ } from "../shared/worktree.ts";
48
+ import {
49
+ type ActivityState,
50
+ type AgentProgress,
51
+ type ArtifactConfig,
52
+ type ArtifactPaths,
53
+ type ControlEvent,
54
+ type Details,
55
+ type IntercomEventBus,
56
+ type NestedRouteInfo,
57
+ type ResolvedControlConfig,
58
+ type SingleResult,
59
+ MAX_CONCURRENCY,
60
+ resolveChildMaxSubagentDepth,
61
+ } from "../../shared/types.ts";
62
+ import { resolveModelCandidate } from "../shared/model-fallback.ts";
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 { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, resolveEffectiveAcceptance } from "../shared/acceptance.ts";
69
+ import type { ChainOutputMap } from "../../shared/types.ts";
70
+
71
+ interface ChainExecutionDetailsInput {
72
+ results: SingleResult[];
73
+ includeProgress?: boolean;
74
+ allProgress: AgentProgress[];
75
+ allArtifactPaths: ArtifactPaths[];
76
+ artifactsDir: string;
77
+ chainAgents: string[];
78
+ chainSteps: ChainStep[];
79
+ totalSteps: number;
80
+ currentStepIndex?: number;
81
+ runId: string;
82
+ outputs?: ChainOutputMap;
83
+ currentFlatIndex?: number;
84
+ dynamicChildren?: Record<number, Array<{ agent: string; label?: string; flatIndex: number; itemKey: string; outputName?: string; structured?: boolean; error?: string }>>;
85
+ dynamicGroupStatuses?: Record<number, { status: "pending" | "running" | "completed" | "failed" | "paused" | "detached"; error?: string; acceptance?: SingleResult["acceptance"] }>;
86
+ }
87
+
88
+ interface ParallelChainRunInput {
89
+ step: ParallelStep;
90
+ parallelTemplates: string[];
91
+ parallelBehaviors: ResolvedStepBehavior[];
92
+ agents: AgentConfig[];
93
+ stepIndex: number;
94
+ availableModels: ModelInfo[];
95
+ chainDir: string;
96
+ prev: string;
97
+ originalTask: string;
98
+ ctx: ExtensionContext;
99
+ intercomEvents?: IntercomEventBus;
100
+ cwd?: string;
101
+ runId: string;
102
+ globalTaskIndex: number;
103
+ sessionDirForIndex: (idx?: number) => string | undefined;
104
+ sessionFileForIndex?: (idx?: number) => string | undefined;
105
+ shareEnabled: boolean;
106
+ artifactConfig: ArtifactConfig;
107
+ artifactsDir: string;
108
+ signal?: AbortSignal;
109
+ onUpdate?: (r: AgentToolResult<Details>) => void;
110
+ onControlEvent?: (event: ControlEvent) => void;
111
+ controlConfig: ResolvedControlConfig;
112
+ childIntercomTarget?: (agent: string, index: number) => string | undefined;
113
+ orchestratorIntercomTarget?: string;
114
+ foregroundControl?: {
115
+ updatedAt: number;
116
+ currentAgent?: string;
117
+ currentIndex?: number;
118
+ currentActivityState?: ActivityState;
119
+ lastActivityAt?: number;
120
+ currentTool?: string;
121
+ currentToolStartedAt?: number;
122
+ currentPath?: string;
123
+ turnCount?: number;
124
+ tokens?: number;
125
+ toolCount?: number;
126
+ interrupt?: () => boolean;
127
+ };
128
+ results: SingleResult[];
129
+ allProgress: AgentProgress[];
130
+ outputs: ChainOutputMap;
131
+ chainAgents: string[];
132
+ chainSteps: ChainStep[];
133
+ totalSteps: number;
134
+ dynamicChildren?: ChainExecutionDetailsInput["dynamicChildren"];
135
+ dynamicGroupStatuses?: ChainExecutionDetailsInput["dynamicGroupStatuses"];
136
+ worktreeSetup?: WorktreeSetup;
137
+ maxSubagentDepth: number;
138
+ nestedRoute?: NestedRouteInfo;
139
+ }
140
+
141
+ function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details {
142
+ return compactForegroundDetails({
143
+ mode: "chain",
144
+ results: input.results,
145
+ progress: input.includeProgress ? input.allProgress : undefined,
146
+ artifacts: input.allArtifactPaths.length ? { dir: input.artifactsDir, files: input.allArtifactPaths } : undefined,
147
+ chainAgents: input.chainAgents,
148
+ totalSteps: input.totalSteps,
149
+ currentStepIndex: input.currentStepIndex,
150
+ outputs: input.outputs,
151
+ workflowGraph: buildWorkflowGraphSnapshot({
152
+ runId: input.runId,
153
+ mode: "chain",
154
+ steps: input.chainSteps,
155
+ results: input.results,
156
+ currentStepIndex: input.currentStepIndex,
157
+ currentFlatIndex: input.currentFlatIndex,
158
+ dynamicChildren: input.dynamicChildren,
159
+ dynamicGroupStatuses: input.dynamicGroupStatuses,
160
+ }),
161
+ });
162
+ }
163
+
164
+ function buildChainExecutionErrorResult(message: string, input: ChainExecutionDetailsInput): ChainExecutionResult {
165
+ return {
166
+ content: [{ type: "text", text: message }],
167
+ isError: true,
168
+ details: buildChainExecutionDetails(input),
169
+ };
170
+ }
171
+
172
+ function ensureParallelProgressFile(
173
+ chainDir: string,
174
+ progressCreated: boolean,
175
+ parallelBehaviors: ResolvedStepBehavior[],
176
+ ): boolean {
177
+ if (progressCreated || !parallelBehaviors.some((behavior) => behavior.progress)) {
178
+ return progressCreated;
179
+ }
180
+ writeInitialProgressFile(chainDir);
181
+ return true;
182
+ }
183
+
184
+ function appendParallelWorktreeSummary(
185
+ output: string,
186
+ worktreeSetup: WorktreeSetup | undefined,
187
+ diffsDir: string,
188
+ agents: string[],
189
+ ): string {
190
+ if (!worktreeSetup) return output;
191
+ const diffs = diffWorktrees(worktreeSetup, agents, diffsDir);
192
+ const diffSummary = formatWorktreeDiffSummary(diffs);
193
+ if (!diffSummary) return output;
194
+ return `${output}\n\n${diffSummary}`;
195
+ }
196
+
197
+ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<SingleResult[]> {
198
+ const concurrency = input.step.concurrency ?? MAX_CONCURRENCY;
199
+ const failFast = input.step.failFast ?? false;
200
+ let aborted = false;
201
+
202
+ const parallelResults = await mapConcurrent(
203
+ input.step.parallel,
204
+ concurrency,
205
+ async (task, taskIndex) => {
206
+ if (aborted && failFast) {
207
+ return {
208
+ agent: task.agent,
209
+ task: "(skipped)",
210
+ exitCode: -1,
211
+ messages: [],
212
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
213
+ error: "Skipped due to fail-fast",
214
+ } as SingleResult;
215
+ }
216
+
217
+ const taskTemplate = input.parallelTemplates[taskIndex] ?? "{previous}";
218
+ const behavior = suppressProgressForReadOnlyTask(input.parallelBehaviors[taskIndex]!, taskTemplate, input.originalTask);
219
+ const templateHasPrevious = taskTemplate.includes("{previous}");
220
+ const { prefix, suffix } = buildChainInstructions(
221
+ behavior,
222
+ input.chainDir,
223
+ false,
224
+ templateHasPrevious ? undefined : input.prev,
225
+ );
226
+
227
+ let taskStr = resolveOutputReferences(taskTemplate, input.outputs);
228
+ taskStr = taskStr.replace(/\{task\}/g, input.originalTask);
229
+ taskStr = taskStr.replace(/\{previous\}/g, input.prev);
230
+ taskStr = taskStr.replace(/\{chain_dir\}/g, input.chainDir);
231
+ const cleanTask = taskStr;
232
+ taskStr = prefix + taskStr + suffix;
233
+
234
+ const taskAgentConfig = input.agents.find((agent) => agent.name === task.agent);
235
+ const effectiveModel =
236
+ (task.model ? resolveModelCandidate(task.model, input.availableModels, input.ctx.model?.provider) : null)
237
+ ?? resolveModelCandidate(taskAgentConfig?.model, input.availableModels, input.ctx.model?.provider);
238
+ const maxSubagentDepth = resolveChildMaxSubagentDepth(input.maxSubagentDepth, taskAgentConfig?.maxSubagentDepth);
239
+
240
+ const taskCwd = input.worktreeSetup
241
+ ? input.worktreeSetup.worktrees[taskIndex]!.agentCwd
242
+ : resolveChildCwd(input.cwd ?? input.ctx.cwd, task.cwd);
243
+
244
+ const outputPath = typeof behavior.output === "string"
245
+ ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(input.chainDir, behavior.output))
246
+ : undefined;
247
+ const interruptController = new AbortController();
248
+ if (input.foregroundControl) {
249
+ input.foregroundControl.currentAgent = task.agent;
250
+ input.foregroundControl.currentIndex = input.globalTaskIndex + taskIndex;
251
+ input.foregroundControl.currentActivityState = undefined;
252
+ input.foregroundControl.updatedAt = Date.now();
253
+ input.foregroundControl.interrupt = () => {
254
+ if (interruptController.signal.aborted) return false;
255
+ interruptController.abort();
256
+ input.foregroundControl!.currentActivityState = undefined;
257
+ input.foregroundControl!.updatedAt = Date.now();
258
+ return true;
259
+ };
260
+ }
261
+
262
+ const structuredRuntime = task.outputSchema
263
+ ? createStructuredOutputRuntime(task.outputSchema, path.join(input.chainDir, "structured-output"))
264
+ : undefined;
265
+ const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
266
+ cwd: taskCwd,
267
+ signal: input.signal,
268
+ interruptSignal: interruptController.signal,
269
+ allowIntercomDetach: taskAgentConfig?.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
270
+ intercomEvents: input.intercomEvents,
271
+ runId: input.runId,
272
+ index: input.globalTaskIndex + taskIndex,
273
+ sessionDir: input.sessionDirForIndex(input.globalTaskIndex + taskIndex),
274
+ sessionFile: input.sessionFileForIndex?.(input.globalTaskIndex + taskIndex),
275
+ share: input.shareEnabled,
276
+ artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
277
+ artifactConfig: input.artifactConfig,
278
+ outputPath,
279
+ outputMode: behavior.outputMode,
280
+ maxSubagentDepth,
281
+ controlConfig: input.controlConfig,
282
+ onControlEvent: input.onControlEvent,
283
+ intercomSessionName: input.childIntercomTarget?.(task.agent, input.globalTaskIndex + taskIndex),
284
+ orchestratorIntercomTarget: input.orchestratorIntercomTarget,
285
+ nestedRoute: input.nestedRoute,
286
+ modelOverride: effectiveModel,
287
+ availableModels: input.availableModels,
288
+ preferredModelProvider: input.ctx.model?.provider,
289
+ skills: behavior.skills === false ? [] : behavior.skills,
290
+ structuredOutput: structuredRuntime,
291
+ acceptance: task.acceptance,
292
+ acceptanceContext: { mode: "chain" },
293
+ onUpdate: input.onUpdate
294
+ ? (progressUpdate) => {
295
+ const stepResults = progressUpdate.details?.results || [];
296
+ const stepProgress = progressUpdate.details?.progress || [];
297
+ if (input.foregroundControl && stepProgress.length > 0) {
298
+ const current = stepProgress[0];
299
+ input.foregroundControl.currentAgent = task.agent;
300
+ input.foregroundControl.currentIndex = input.globalTaskIndex + taskIndex;
301
+ input.foregroundControl.currentActivityState = current?.activityState;
302
+ input.foregroundControl.lastActivityAt = current?.lastActivityAt;
303
+ input.foregroundControl.currentTool = current?.currentTool;
304
+ input.foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
305
+ input.foregroundControl.currentPath = current?.currentPath;
306
+ input.foregroundControl.turnCount = current?.turnCount;
307
+ input.foregroundControl.tokens = current?.tokens;
308
+ input.foregroundControl.toolCount = current?.toolCount;
309
+ input.foregroundControl.updatedAt = Date.now();
310
+ }
311
+ input.onUpdate?.({
312
+ ...progressUpdate,
313
+ details: {
314
+ mode: "chain",
315
+ results: input.results.concat(stepResults),
316
+ progress: input.allProgress.concat(stepProgress),
317
+ controlEvents: progressUpdate.details?.controlEvents,
318
+ chainAgents: input.chainAgents,
319
+ totalSteps: input.totalSteps,
320
+ currentStepIndex: input.stepIndex,
321
+ outputs: input.outputs,
322
+ workflowGraph: buildWorkflowGraphSnapshot({
323
+ runId: input.runId,
324
+ mode: "chain",
325
+ steps: input.chainSteps,
326
+ results: input.results.concat(stepResults),
327
+ currentStepIndex: input.stepIndex,
328
+ currentFlatIndex: input.globalTaskIndex + taskIndex,
329
+ dynamicChildren: input.dynamicChildren,
330
+ dynamicGroupStatuses: input.dynamicGroupStatuses,
331
+ }),
332
+ },
333
+ });
334
+ }
335
+ : undefined,
336
+ });
337
+ if (input.foregroundControl?.currentIndex === input.globalTaskIndex + taskIndex) {
338
+ input.foregroundControl.interrupt = undefined;
339
+ input.foregroundControl.updatedAt = Date.now();
340
+ }
341
+
342
+ if (result.exitCode !== 0 && failFast) {
343
+ aborted = true;
344
+ }
345
+ recordRun(task.agent, cleanTask, result.exitCode, result.progressSummary?.durationMs ?? 0);
346
+ return result;
347
+ },
348
+ );
349
+
350
+ return parallelResults;
351
+ }
352
+
353
+ interface ChainExecutionParams {
354
+ chain: ChainStep[];
355
+ task?: string;
356
+ agents: AgentConfig[];
357
+ ctx: ExtensionContext;
358
+ intercomEvents?: IntercomEventBus;
359
+ signal?: AbortSignal;
360
+ runId: string;
361
+ cwd?: string;
362
+ shareEnabled: boolean;
363
+ sessionDirForIndex: (idx?: number) => string | undefined;
364
+ sessionFileForIndex?: (idx?: number) => string | undefined;
365
+ artifactsDir: string;
366
+ artifactConfig: ArtifactConfig;
367
+ includeProgress?: boolean;
368
+ clarify?: boolean;
369
+ onUpdate?: (r: AgentToolResult<Details>) => void;
370
+ onControlEvent?: (event: ControlEvent) => void;
371
+ controlConfig: ResolvedControlConfig;
372
+ childIntercomTarget?: (agent: string, index: number) => string | undefined;
373
+ orchestratorIntercomTarget?: string;
374
+ foregroundControl?: {
375
+ updatedAt: number;
376
+ currentAgent?: string;
377
+ currentIndex?: number;
378
+ currentActivityState?: ActivityState;
379
+ lastActivityAt?: number;
380
+ currentTool?: string;
381
+ currentToolStartedAt?: number;
382
+ currentPath?: string;
383
+ turnCount?: number;
384
+ tokens?: number;
385
+ toolCount?: number;
386
+ interrupt?: () => boolean;
387
+ };
388
+ chainSkills?: string[];
389
+ chainDir?: string;
390
+ dynamicFanoutMaxItems?: number;
391
+ maxSubagentDepth: number;
392
+ nestedRoute?: NestedRouteInfo;
393
+ worktreeSetupHook?: string;
394
+ worktreeSetupHookTimeoutMs?: number;
395
+ }
396
+
397
+ interface ChainExecutionResult {
398
+ content: Array<{ type: "text"; text: string }>;
399
+ details: Details;
400
+ isError?: boolean;
401
+ /** User requested async execution via TUI - caller should dispatch to executeAsyncChain */
402
+ requestedAsync?: {
403
+ chain: ChainStep[];
404
+ chainSkills: string[];
405
+ };
406
+ }
407
+
408
+ /**
409
+ * Execute a chain of subagent steps
410
+ */
411
+ export async function executeChain(params: ChainExecutionParams): Promise<ChainExecutionResult> {
412
+ const {
413
+ chain: chainSteps,
414
+ agents,
415
+ ctx,
416
+ signal,
417
+ runId,
418
+ cwd,
419
+ shareEnabled,
420
+ sessionDirForIndex,
421
+ sessionFileForIndex,
422
+ artifactsDir,
423
+ artifactConfig,
424
+ includeProgress,
425
+ clarify,
426
+ onUpdate,
427
+ onControlEvent,
428
+ controlConfig,
429
+ childIntercomTarget,
430
+ orchestratorIntercomTarget,
431
+ foregroundControl,
432
+ intercomEvents,
433
+ chainSkills: chainSkillsParam,
434
+ chainDir: chainDirBase,
435
+ } = params;
436
+ const chainSkills = chainSkillsParam ?? [];
437
+
438
+ const results: SingleResult[] = [];
439
+ const outputs: ChainOutputMap = {};
440
+ const dynamicChildren: ChainExecutionDetailsInput["dynamicChildren"] = {};
441
+ const dynamicGroupStatuses: ChainExecutionDetailsInput["dynamicGroupStatuses"] = {};
442
+ const allProgress: AgentProgress[] = [];
443
+ const allArtifactPaths: ArtifactPaths[] = [];
444
+
445
+ const chainAgents: string[] = chainSteps.map((step) =>
446
+ isParallelStep(step)
447
+ ? `[${step.parallel.map((t) => t.agent).join("+")}]`
448
+ : isDynamicParallelStep(step)
449
+ ? `expand:${step.parallel.agent}`
450
+ : (step as SequentialStep).agent,
451
+ );
452
+ const totalSteps = chainSteps.length;
453
+
454
+ const makeDetailsInput = (overrides: Pick<Partial<ChainExecutionDetailsInput>, "currentStepIndex" | "currentFlatIndex"> = {}): ChainExecutionDetailsInput => ({
455
+ results,
456
+ ...(includeProgress !== undefined ? { includeProgress } : {}),
457
+ allProgress,
458
+ allArtifactPaths,
459
+ artifactsDir,
460
+ chainAgents,
461
+ chainSteps,
462
+ totalSteps,
463
+ runId,
464
+ outputs,
465
+ dynamicChildren,
466
+ dynamicGroupStatuses,
467
+ ...overrides,
468
+ });
469
+
470
+ const firstStep = chainSteps[0]!;
471
+ const originalTask = params.task
472
+ ?? (isParallelStep(firstStep)
473
+ ? firstStep.parallel[0]!.task!
474
+ : isDynamicParallelStep(firstStep)
475
+ ? firstStep.parallel.task!
476
+ : (firstStep as SequentialStep).task!);
477
+ try {
478
+ validateChainOutputBindings(chainSteps, { maxItems: params.dynamicFanoutMaxItems });
479
+ } catch (error) {
480
+ if (error instanceof ChainOutputValidationError) {
481
+ return {
482
+ content: [{ type: "text", text: error.message }],
483
+ isError: true,
484
+ details: buildChainExecutionDetails(makeDetailsInput()),
485
+ };
486
+ }
487
+ throw error;
488
+ }
489
+
490
+ const chainDir = createChainDir(runId, chainDirBase);
491
+ const hasParallelSteps = chainSteps.some((step) => isParallelStep(step) || isDynamicParallelStep(step));
492
+ let templates: ResolvedTemplates = resolveChainTemplates(chainSteps);
493
+ const shouldClarify = clarify !== false && ctx.hasUI && !hasParallelSteps;
494
+ let tuiBehaviorOverrides: (BehaviorOverride | undefined)[] | undefined;
495
+ const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
496
+ const availableSkills = discoverAvailableSkills(cwd ?? ctx.cwd);
497
+
498
+ if (shouldClarify) {
499
+ const seqSteps = chainSteps as SequentialStep[];
500
+ const agentConfigs: AgentConfig[] = [];
501
+ for (const step of seqSteps) {
502
+ const config = agents.find((a) => a.name === step.agent);
503
+ if (!config) {
504
+ removeChainDir(chainDir);
505
+ return {
506
+ content: [{ type: "text", text: `Unknown agent: ${step.agent}` }],
507
+ isError: true,
508
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: seqSteps.indexOf(step) })),
509
+ };
510
+ }
511
+ agentConfigs.push(config);
512
+ }
513
+
514
+ const stepOverrides: StepOverrides[] = seqSteps.map((step) => ({
515
+ output: step.output,
516
+ outputMode: step.outputMode,
517
+ reads: step.reads,
518
+ progress: step.progress,
519
+ skills: normalizeSkillInput(step.skill),
520
+ model: step.model,
521
+ }));
522
+
523
+ const resolvedBehaviors = agentConfigs.map((config, i) =>
524
+ resolveStepBehavior(config, stepOverrides[i]!, chainSkills),
525
+ );
526
+ const flatTemplates = templates as string[];
527
+
528
+ const result = await ctx.ui.custom<ChainClarifyResult>(
529
+ (tui, theme, _kb, done) =>
530
+ new ChainClarifyComponent(
531
+ tui,
532
+ theme,
533
+ agentConfigs,
534
+ flatTemplates,
535
+ originalTask,
536
+ chainDir,
537
+ resolvedBehaviors,
538
+ availableModels,
539
+ ctx.model?.provider,
540
+ availableSkills,
541
+ done,
542
+ ),
543
+ {
544
+ overlay: true,
545
+ overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" },
546
+ },
547
+ );
548
+
549
+ if (!result || !result.confirmed) {
550
+ removeChainDir(chainDir);
551
+ return {
552
+ content: [{ type: "text", text: "Chain cancelled" }],
553
+ details: buildChainExecutionDetails(makeDetailsInput()),
554
+ };
555
+ }
556
+
557
+ if (result.runInBackground) {
558
+ removeChainDir(chainDir);
559
+ const updatedChain: ChainStep[] = chainSteps.map((step, i) => {
560
+ if (isParallelStep(step)) return step;
561
+ const override = result.behaviorOverrides[i];
562
+ return {
563
+ ...step,
564
+ task: result.templates[i]!,
565
+ ...(override?.model ? { model: override.model } : {}),
566
+ ...(override?.output !== undefined ? { output: override.output } : {}),
567
+ ...("outputMode" in step && step.outputMode !== undefined ? { outputMode: step.outputMode } : {}),
568
+ ...(override?.reads !== undefined ? { reads: override.reads } : {}),
569
+ ...(override?.progress !== undefined ? { progress: override.progress } : {}),
570
+ ...(override?.skills !== undefined ? { skill: override.skills } : {}),
571
+ };
572
+ });
573
+ return {
574
+ content: [{ type: "text", text: "Launching in background..." }],
575
+ details: buildChainExecutionDetails(makeDetailsInput()),
576
+ requestedAsync: { chain: updatedChain, chainSkills },
577
+ };
578
+ }
579
+
580
+ templates = result.templates;
581
+ tuiBehaviorOverrides = result.behaviorOverrides;
582
+ }
583
+
584
+ let prev = "";
585
+ let globalTaskIndex = 0;
586
+ let progressCreated = false;
587
+
588
+ for (let stepIndex = 0; stepIndex < chainSteps.length; stepIndex++) {
589
+ const step = chainSteps[stepIndex]!;
590
+ const stepTemplates = templates[stepIndex]!;
591
+
592
+ if (isParallelStep(step)) {
593
+ const parallelTemplates = stepTemplates as string[];
594
+ const parallelCwd = resolveChildCwd(cwd ?? ctx.cwd, step.cwd);
595
+ let worktreeSetup: WorktreeSetup | undefined;
596
+ if (step.worktree) {
597
+ const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(step.parallel, parallelCwd);
598
+ if (worktreeTaskCwdConflict) {
599
+ return buildChainExecutionErrorResult(
600
+ `parallel chain step ${stepIndex + 1}: ${formatWorktreeTaskCwdConflict(worktreeTaskCwdConflict, parallelCwd)}`,
601
+ makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }),
602
+ );
603
+ }
604
+ try {
605
+ worktreeSetup = createWorktrees(parallelCwd, `${runId}-s${stepIndex}`, step.parallel.length, {
606
+ agents: step.parallel.map((task) => task.agent),
607
+ setupHook: params.worktreeSetupHook
608
+ ? { hookPath: params.worktreeSetupHook, timeoutMs: params.worktreeSetupHookTimeoutMs }
609
+ : undefined,
610
+ });
611
+ } catch (error) {
612
+ const message = error instanceof Error ? error.message : String(error);
613
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
614
+ }
615
+ }
616
+
617
+ try {
618
+ const agentNames = step.parallel.map((task) => task.agent);
619
+ const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills)
620
+ .map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? step.parallel[taskIndex]?.task, originalTask));
621
+ for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
622
+ const behavior = parallelBehaviors[taskIndex]!;
623
+ const outputPath = typeof behavior.output === "string"
624
+ ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
625
+ : undefined;
626
+ const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Parallel chain step ${stepIndex + 1} task ${taskIndex + 1} (${step.parallel[taskIndex]!.agent})`);
627
+ if (validationError) return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }));
628
+ }
629
+ progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
630
+ createParallelDirs(chainDir, stepIndex, step.parallel.length, agentNames);
631
+
632
+ const parallelResults = await runParallelChainTasks({
633
+ step,
634
+ parallelTemplates,
635
+ parallelBehaviors,
636
+ agents,
637
+ stepIndex,
638
+ availableModels,
639
+ chainDir,
640
+ prev,
641
+ originalTask,
642
+ ctx,
643
+ intercomEvents,
644
+ cwd,
645
+ runId,
646
+ globalTaskIndex,
647
+ sessionDirForIndex,
648
+ sessionFileForIndex,
649
+ shareEnabled,
650
+ artifactConfig,
651
+ artifactsDir,
652
+ signal,
653
+ onUpdate,
654
+ results,
655
+ allProgress,
656
+ outputs,
657
+ chainAgents,
658
+ chainSteps,
659
+ totalSteps,
660
+ dynamicChildren,
661
+ dynamicGroupStatuses,
662
+ controlConfig,
663
+ onControlEvent,
664
+ childIntercomTarget,
665
+ orchestratorIntercomTarget,
666
+ foregroundControl,
667
+ nestedRoute: params.nestedRoute,
668
+ worktreeSetup,
669
+ maxSubagentDepth: params.maxSubagentDepth,
670
+ });
671
+ globalTaskIndex += step.parallel.length;
672
+
673
+ for (const result of parallelResults) {
674
+ results.push(result);
675
+ if (result.progress) allProgress.push(result.progress);
676
+ if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
677
+ }
678
+ const interruptedIndexInStep = parallelResults.findIndex((result) => result.interrupted);
679
+ const interrupted = interruptedIndexInStep >= 0 ? parallelResults[interruptedIndexInStep] : undefined;
680
+ if (interrupted) {
681
+ return {
682
+ content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
683
+ details: buildChainExecutionDetails(makeDetailsInput({
684
+ currentStepIndex: stepIndex,
685
+ currentFlatIndex: globalTaskIndex - step.parallel.length + interruptedIndexInStep,
686
+ })),
687
+ };
688
+ }
689
+ const detachedIndexInStep = parallelResults.findIndex((result) => result.detached);
690
+ const detached = detachedIndexInStep >= 0 ? parallelResults[detachedIndexInStep] : undefined;
691
+ if (detached) {
692
+ return {
693
+ 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.` }],
694
+ details: buildChainExecutionDetails(makeDetailsInput({
695
+ currentStepIndex: stepIndex,
696
+ currentFlatIndex: globalTaskIndex - step.parallel.length + detachedIndexInStep,
697
+ })),
698
+ };
699
+ }
700
+
701
+ const failures = parallelResults
702
+ .map((result, originalIndex) => ({ ...result, originalIndex }))
703
+ .filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
704
+ if (failures.length > 0) {
705
+ const failureSummary = failures
706
+ .map((failure) => `- Task ${failure.originalIndex + 1} (${failure.agent}): ${failure.error || "failed"}`)
707
+ .join("\n");
708
+ const errorMsg = `Parallel step ${stepIndex + 1} failed:\n${failureSummary}`;
709
+ const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
710
+ index: stepIndex,
711
+ error: errorMsg,
712
+ });
713
+ return {
714
+ content: [{ type: "text", text: summary }],
715
+ isError: true,
716
+ details: buildChainExecutionDetails(makeDetailsInput({
717
+ currentStepIndex: stepIndex,
718
+ currentFlatIndex: globalTaskIndex - step.parallel.length + failures[0]!.originalIndex,
719
+ })),
720
+ };
721
+ }
722
+
723
+ for (let taskIndex = 0; taskIndex < parallelResults.length; taskIndex++) {
724
+ const outputName = step.parallel[taskIndex]?.as;
725
+ if (outputName) outputs[outputName] = outputEntryFromResult(parallelResults[taskIndex]!, stepIndex);
726
+ }
727
+
728
+ const taskResults: ParallelTaskResult[] = parallelResults.map((result, i) => {
729
+ const outputTarget = parallelBehaviors[i]?.output;
730
+ const outputTargetPath = typeof outputTarget === "string"
731
+ ? (path.isAbsolute(outputTarget) ? outputTarget : path.join(chainDir, outputTarget))
732
+ : undefined;
733
+ return {
734
+ agent: result.agent,
735
+ taskIndex: i,
736
+ output: getSingleResultOutput(result),
737
+ exitCode: result.exitCode,
738
+ error: result.error,
739
+ outputTargetPath,
740
+ outputTargetExists: outputTargetPath ? fs.existsSync(outputTargetPath) : undefined,
741
+ };
742
+ });
743
+ prev = aggregateParallelOutputs(taskResults);
744
+ prev = appendParallelWorktreeSummary(
745
+ prev,
746
+ worktreeSetup,
747
+ path.join(chainDir, "worktree-diffs", `step-${stepIndex}`),
748
+ agentNames,
749
+ );
750
+ } finally {
751
+ if (worktreeSetup) cleanupWorktrees(worktreeSetup);
752
+ }
753
+ } else if (isDynamicParallelStep(step)) {
754
+ let materialized: ReturnType<typeof materializeDynamicParallelStep>;
755
+ try {
756
+ materialized = materializeDynamicParallelStep(step, outputs, stepIndex, { maxItems: params.dynamicFanoutMaxItems });
757
+ } catch (error) {
758
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
759
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
760
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
761
+ }
762
+
763
+ dynamicChildren[stepIndex] = materialized.items.map((item, itemIndex) => ({
764
+ agent: step.parallel.agent,
765
+ label: materialized.parallel[itemIndex]?.label,
766
+ flatIndex: globalTaskIndex + itemIndex,
767
+ itemKey: item.key,
768
+ structured: Boolean(step.parallel.outputSchema),
769
+ }));
770
+
771
+ if (materialized.parallel.length === 0) {
772
+ const collection: DynamicCollectedResult[] = [];
773
+ try {
774
+ validateDynamicCollection(step.collect.outputSchema, collection);
775
+ } catch (error) {
776
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
777
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
778
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
779
+ }
780
+ outputs[step.collect.as] = {
781
+ text: JSON.stringify(collection),
782
+ structured: collection,
783
+ agent: step.parallel.agent,
784
+ stepIndex,
785
+ };
786
+ dynamicGroupStatuses[stepIndex] = { status: "completed" };
787
+ if (step.acceptance !== undefined) {
788
+ const effectiveGroupAcceptance = resolveEffectiveAcceptance({
789
+ explicit: step.acceptance,
790
+ agentName: step.parallel.agent,
791
+ task: step.parallel.task ?? originalTask,
792
+ mode: "chain",
793
+ dynamicGroup: true,
794
+ });
795
+ const groupAcceptance = await evaluateAcceptance({
796
+ acceptance: effectiveGroupAcceptance,
797
+ output: "",
798
+ report: aggregateAcceptanceReport({
799
+ results: [],
800
+ notes: "Dynamic fanout produced 0 results.",
801
+ }),
802
+ cwd: cwd ?? ctx.cwd,
803
+ });
804
+ dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
805
+ const groupAcceptanceFailure = acceptanceFailureMessage(groupAcceptance);
806
+ if (groupAcceptanceFailure) {
807
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
808
+ return buildChainExecutionErrorResult(groupAcceptanceFailure, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
809
+ }
810
+ }
811
+ prev = "Dynamic fanout produced 0 results.";
812
+ continue;
813
+ }
814
+
815
+ const dynamicParallelStep: ParallelStep = {
816
+ parallel: materialized.parallel,
817
+ concurrency: step.concurrency,
818
+ failFast: step.failFast,
819
+ };
820
+ const parallelTemplates = materialized.parallel.map((task) => task.task ?? "{previous}");
821
+ const parallelBehaviors = resolveParallelBehaviors(dynamicParallelStep.parallel, agents, stepIndex, chainSkills)
822
+ .map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? dynamicParallelStep.parallel[taskIndex]?.task, originalTask));
823
+
824
+ for (let taskIndex = 0; taskIndex < dynamicParallelStep.parallel.length; taskIndex++) {
825
+ const behavior = parallelBehaviors[taskIndex]!;
826
+ const outputPath = typeof behavior.output === "string"
827
+ ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
828
+ : undefined;
829
+ const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Dynamic chain step ${stepIndex + 1} item ${taskIndex + 1} (${dynamicParallelStep.parallel[taskIndex]!.agent})`);
830
+ if (validationError) {
831
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: validationError };
832
+ return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }));
833
+ }
834
+ }
835
+
836
+ progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
837
+ createParallelDirs(chainDir, stepIndex, dynamicParallelStep.parallel.length, dynamicParallelStep.parallel.map((task) => task.agent));
838
+ const parallelResults = await runParallelChainTasks({
839
+ step: dynamicParallelStep,
840
+ parallelTemplates,
841
+ parallelBehaviors,
842
+ agents,
843
+ stepIndex,
844
+ availableModels,
845
+ chainDir,
846
+ prev,
847
+ originalTask,
848
+ ctx,
849
+ intercomEvents,
850
+ cwd,
851
+ runId,
852
+ globalTaskIndex,
853
+ sessionDirForIndex,
854
+ sessionFileForIndex,
855
+ shareEnabled,
856
+ artifactConfig,
857
+ artifactsDir,
858
+ signal,
859
+ onUpdate,
860
+ results,
861
+ allProgress,
862
+ outputs,
863
+ chainAgents,
864
+ chainSteps,
865
+ totalSteps,
866
+ dynamicChildren,
867
+ dynamicGroupStatuses,
868
+ controlConfig,
869
+ onControlEvent,
870
+ childIntercomTarget,
871
+ orchestratorIntercomTarget,
872
+ foregroundControl,
873
+ nestedRoute: params.nestedRoute,
874
+ maxSubagentDepth: params.maxSubagentDepth,
875
+ });
876
+ globalTaskIndex += dynamicParallelStep.parallel.length;
877
+
878
+ for (const result of parallelResults) {
879
+ results.push(result);
880
+ if (result.progress) allProgress.push(result.progress);
881
+ if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
882
+ }
883
+ const collected = collectDynamicResults(step, materialized.items, parallelResults);
884
+ const interruptedIndexInStep = parallelResults.findIndex((result) => result.interrupted);
885
+ const interrupted = interruptedIndexInStep >= 0 ? parallelResults[interruptedIndexInStep] : undefined;
886
+ if (interrupted) {
887
+ return {
888
+ content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
889
+ details: buildChainExecutionDetails(makeDetailsInput({
890
+ currentStepIndex: stepIndex,
891
+ currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + interruptedIndexInStep,
892
+ })),
893
+ };
894
+ }
895
+ const detachedIndexInStep = parallelResults.findIndex((result) => result.detached);
896
+ const detached = detachedIndexInStep >= 0 ? parallelResults[detachedIndexInStep] : undefined;
897
+ if (detached) {
898
+ return {
899
+ 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.` }],
900
+ details: buildChainExecutionDetails(makeDetailsInput({
901
+ currentStepIndex: stepIndex,
902
+ currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + detachedIndexInStep,
903
+ })),
904
+ };
905
+ }
906
+ const failures = parallelResults
907
+ .map((result, originalIndex) => ({ ...result, originalIndex }))
908
+ .filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
909
+ if (failures.length > 0) {
910
+ const failureSummary = failures
911
+ .map((failure) => `- Item ${failure.originalIndex + 1} (${failure.agent}, key ${materialized.items[failure.originalIndex]?.key ?? failure.originalIndex}): ${failure.error || "failed"}`)
912
+ .join("\n");
913
+ const errorMsg = `Dynamic step ${stepIndex + 1} failed:\n${failureSummary}`;
914
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: errorMsg };
915
+ const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
916
+ index: stepIndex,
917
+ error: errorMsg,
918
+ });
919
+ return {
920
+ content: [{ type: "text", text: summary }],
921
+ isError: true,
922
+ details: buildChainExecutionDetails(makeDetailsInput({
923
+ currentStepIndex: stepIndex,
924
+ currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + failures[0]!.originalIndex,
925
+ })),
926
+ };
927
+ }
928
+ try {
929
+ validateDynamicCollection(step.collect.outputSchema, collected);
930
+ } catch (error) {
931
+ const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
932
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
933
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }));
934
+ }
935
+ outputs[step.collect.as] = {
936
+ text: JSON.stringify(collected),
937
+ structured: collected,
938
+ agent: step.parallel.agent,
939
+ stepIndex,
940
+ };
941
+ dynamicGroupStatuses[stepIndex] = { status: "completed" };
942
+ const effectiveGroupAcceptance = resolveEffectiveAcceptance({
943
+ explicit: step.acceptance,
944
+ agentName: step.parallel.agent,
945
+ task: step.parallel.task ?? originalTask,
946
+ mode: "chain",
947
+ dynamicGroup: true,
948
+ });
949
+ const groupAcceptance = await evaluateAcceptance({
950
+ acceptance: effectiveGroupAcceptance,
951
+ output: "",
952
+ report: aggregateAcceptanceReport({
953
+ results: parallelResults,
954
+ notes: `Dynamic fanout collected ${collected.length} result(s) into ${step.collect.as}.`,
955
+ }),
956
+ cwd: cwd ?? ctx.cwd,
957
+ });
958
+ dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
959
+ const groupAcceptanceFailure = acceptanceFailureMessage(groupAcceptance);
960
+ if (groupAcceptanceFailure) {
961
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
962
+ return buildChainExecutionErrorResult(groupAcceptanceFailure, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }));
963
+ }
964
+ const taskResults: ParallelTaskResult[] = parallelResults.map((result, i) => ({
965
+ agent: result.agent,
966
+ taskIndex: i,
967
+ output: getSingleResultOutput(result),
968
+ exitCode: result.exitCode,
969
+ error: result.error,
970
+ }));
971
+ prev = aggregateParallelOutputs(taskResults, (i, agent) => `=== Dynamic Item ${i + 1} (${agent}, key ${materialized.items[i]?.key ?? i}) ===`);
972
+ } else {
973
+ const seqStep = step as SequentialStep;
974
+ const stepTemplate = stepTemplates as string;
975
+
976
+ const agentConfig = agents.find((a) => a.name === seqStep.agent);
977
+ if (!agentConfig) {
978
+ removeChainDir(chainDir);
979
+ return {
980
+ content: [{ type: "text", text: `Unknown agent: ${seqStep.agent}` }],
981
+ isError: true,
982
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex })),
983
+ };
984
+ }
985
+
986
+ const tuiOverride = tuiBehaviorOverrides?.[stepIndex];
987
+ const stepOverride: StepOverrides = {
988
+ output: tuiOverride?.output !== undefined ? tuiOverride.output : seqStep.output,
989
+ outputMode: seqStep.outputMode,
990
+ reads: tuiOverride?.reads !== undefined ? tuiOverride.reads : seqStep.reads,
991
+ progress: tuiOverride?.progress !== undefined ? tuiOverride.progress : seqStep.progress,
992
+ skills:
993
+ tuiOverride?.skills !== undefined
994
+ ? tuiOverride.skills
995
+ : normalizeSkillInput(seqStep.skill),
996
+ };
997
+ const behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfig, stepOverride, chainSkills), stepTemplate, originalTask);
998
+
999
+ const isFirstProgress = behavior.progress && !progressCreated;
1000
+ if (isFirstProgress) {
1001
+ progressCreated = true;
1002
+ }
1003
+
1004
+ const templateHasPrevious = stepTemplate.includes("{previous}");
1005
+ const { prefix, suffix } = buildChainInstructions(
1006
+ behavior,
1007
+ chainDir,
1008
+ isFirstProgress,
1009
+ templateHasPrevious ? undefined : prev,
1010
+ );
1011
+
1012
+ let stepTask = resolveOutputReferences(stepTemplate, outputs);
1013
+ stepTask = stepTask.replace(/\{task\}/g, originalTask);
1014
+ stepTask = stepTask.replace(/\{previous\}/g, prev);
1015
+ stepTask = stepTask.replace(/\{chain_dir\}/g, chainDir);
1016
+ const cleanTask = stepTask;
1017
+ stepTask = prefix + stepTask + suffix;
1018
+
1019
+ const effectiveModel =
1020
+ tuiOverride?.model
1021
+ ?? (seqStep.model ? resolveModelCandidate(seqStep.model, availableModels, ctx.model?.provider) : null)
1022
+ ?? resolveModelCandidate(agentConfig.model, availableModels, ctx.model?.provider);
1023
+
1024
+ const outputPath = typeof behavior.output === "string"
1025
+ ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
1026
+ : undefined;
1027
+ const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Chain step ${stepIndex + 1} (${seqStep.agent})`);
1028
+ if (validationError) {
1029
+ return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
1030
+ }
1031
+ const maxSubagentDepth = resolveChildMaxSubagentDepth(params.maxSubagentDepth, agentConfig.maxSubagentDepth);
1032
+ const interruptController = new AbortController();
1033
+ if (foregroundControl) {
1034
+ foregroundControl.currentAgent = seqStep.agent;
1035
+ foregroundControl.currentIndex = globalTaskIndex;
1036
+ foregroundControl.currentActivityState = undefined;
1037
+ foregroundControl.updatedAt = Date.now();
1038
+ foregroundControl.interrupt = () => {
1039
+ if (interruptController.signal.aborted) return false;
1040
+ interruptController.abort();
1041
+ foregroundControl.currentActivityState = undefined;
1042
+ foregroundControl.updatedAt = Date.now();
1043
+ return true;
1044
+ };
1045
+ }
1046
+
1047
+ const structuredRuntime = seqStep.outputSchema
1048
+ ? createStructuredOutputRuntime(seqStep.outputSchema, path.join(chainDir, "structured-output"))
1049
+ : undefined;
1050
+ const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1051
+ cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
1052
+ signal,
1053
+ interruptSignal: interruptController.signal,
1054
+ allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
1055
+ intercomEvents,
1056
+ runId,
1057
+ index: globalTaskIndex,
1058
+ sessionDir: sessionDirForIndex(globalTaskIndex),
1059
+ sessionFile: sessionFileForIndex?.(globalTaskIndex),
1060
+ share: shareEnabled,
1061
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
1062
+ artifactConfig,
1063
+ outputPath,
1064
+ outputMode: behavior.outputMode,
1065
+ maxSubagentDepth,
1066
+ controlConfig,
1067
+ onControlEvent,
1068
+ intercomSessionName: childIntercomTarget?.(seqStep.agent, globalTaskIndex),
1069
+ orchestratorIntercomTarget,
1070
+ nestedRoute: params.nestedRoute,
1071
+ modelOverride: effectiveModel,
1072
+ availableModels,
1073
+ preferredModelProvider: ctx.model?.provider,
1074
+ skills: behavior.skills === false ? [] : behavior.skills,
1075
+ structuredOutput: structuredRuntime,
1076
+ acceptance: seqStep.acceptance,
1077
+ acceptanceContext: { mode: "chain" },
1078
+ onUpdate: onUpdate
1079
+ ? (p) => {
1080
+ const stepResults = p.details?.results || [];
1081
+ const stepProgress = p.details?.progress || [];
1082
+ if (foregroundControl && stepProgress.length > 0) {
1083
+ const current = stepProgress[0];
1084
+ foregroundControl.currentAgent = seqStep.agent;
1085
+ foregroundControl.currentIndex = globalTaskIndex;
1086
+ foregroundControl.currentActivityState = current?.activityState;
1087
+ foregroundControl.lastActivityAt = current?.lastActivityAt;
1088
+ foregroundControl.currentTool = current?.currentTool;
1089
+ foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
1090
+ foregroundControl.currentPath = current?.currentPath;
1091
+ foregroundControl.turnCount = current?.turnCount;
1092
+ foregroundControl.tokens = current?.tokens;
1093
+ foregroundControl.toolCount = current?.toolCount;
1094
+ foregroundControl.updatedAt = Date.now();
1095
+ }
1096
+ onUpdate({
1097
+ ...p,
1098
+ details: {
1099
+ mode: "chain",
1100
+ results: results.concat(stepResults),
1101
+ progress: allProgress.concat(stepProgress),
1102
+ controlEvents: p.details?.controlEvents,
1103
+ chainAgents,
1104
+ totalSteps,
1105
+ currentStepIndex: stepIndex,
1106
+ outputs,
1107
+ workflowGraph: buildWorkflowGraphSnapshot({
1108
+ runId,
1109
+ mode: "chain",
1110
+ steps: chainSteps,
1111
+ results: results.concat(stepResults),
1112
+ currentStepIndex: stepIndex,
1113
+ currentFlatIndex: globalTaskIndex,
1114
+ dynamicChildren,
1115
+ dynamicGroupStatuses,
1116
+ }),
1117
+ },
1118
+ });
1119
+ }
1120
+ : undefined,
1121
+ });
1122
+ if (foregroundControl?.currentIndex === globalTaskIndex) {
1123
+ foregroundControl.interrupt = undefined;
1124
+ foregroundControl.updatedAt = Date.now();
1125
+ }
1126
+ recordRun(seqStep.agent, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
1127
+
1128
+ globalTaskIndex++;
1129
+ results.push(r);
1130
+ if (r.progress) allProgress.push(r.progress);
1131
+ if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
1132
+
1133
+ if (r.interrupted) {
1134
+ return {
1135
+ content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${r.agent}). Waiting for explicit next action.` }],
1136
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
1137
+ };
1138
+ }
1139
+ if (r.detached) {
1140
+ return {
1141
+ 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.` }],
1142
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
1143
+ };
1144
+ }
1145
+
1146
+ if (r.exitCode !== 0) {
1147
+ const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
1148
+ index: stepIndex,
1149
+ error: r.error || "Chain failed",
1150
+ });
1151
+ return {
1152
+ content: [{ type: "text", text: summary }],
1153
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
1154
+ isError: true,
1155
+ };
1156
+ }
1157
+
1158
+ if (behavior.output) {
1159
+ try {
1160
+ const expectedPath = path.isAbsolute(behavior.output)
1161
+ ? behavior.output
1162
+ : path.join(chainDir, behavior.output);
1163
+ if (!fs.existsSync(expectedPath)) {
1164
+ const dirFiles = fs.readdirSync(chainDir);
1165
+ const mdFiles = dirFiles.filter((file) => file.endsWith(".md") && file !== "progress.md");
1166
+ const warning = mdFiles.length > 0
1167
+ ? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
1168
+ : `Agent did not create expected output file: ${behavior.output}`;
1169
+ r.error = r.error ? `${r.error}\n${warning}` : warning;
1170
+ }
1171
+ } catch {
1172
+ // Ignore validation errors; this diagnostic should not mask successful chain output.
1173
+ }
1174
+ }
1175
+
1176
+ if (seqStep.as) outputs[seqStep.as] = outputEntryFromResult(r, stepIndex);
1177
+ prev = getSingleResultOutput(r);
1178
+ }
1179
+ }
1180
+
1181
+ const summary = buildChainSummary(chainSteps, results, chainDir, "completed");
1182
+
1183
+ return {
1184
+ content: [{ type: "text", text: summary }],
1185
+ details: buildChainExecutionDetails(makeDetailsInput()),
1186
+ };
1187
+ }