@bastani/atomic 0.8.21-0 → 0.8.22-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 (235) hide show
  1. package/CHANGELOG.md +46 -9
  2. package/dist/builtin/intercom/broker/broker.ts +3 -3
  3. package/dist/builtin/intercom/config.ts +3 -3
  4. package/dist/builtin/intercom/index.ts +1 -1
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/intercom/ui/compose.ts +2 -2
  7. package/dist/builtin/mcp/host-html-template.ts +0 -3
  8. package/dist/builtin/mcp/package.json +1 -1
  9. package/dist/builtin/subagents/CHANGELOG.md +13 -4
  10. package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -9
  11. package/dist/builtin/subagents/agents/debugger.md +6 -6
  12. package/dist/builtin/subagents/package.json +1 -1
  13. package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +1 -1
  14. package/dist/builtin/subagents/skills/browser-use/SKILL.md +234 -0
  15. package/dist/builtin/subagents/skills/browser-use/references/cdp-python.md +76 -0
  16. package/dist/builtin/subagents/skills/browser-use/references/multi-session.md +92 -0
  17. package/dist/builtin/subagents/skills/subagent/SKILL.md +4 -4
  18. package/dist/builtin/subagents/src/agents/skills.ts +19 -1
  19. package/dist/builtin/subagents/src/extension/index.ts +24 -22
  20. package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +7 -1
  21. package/dist/builtin/subagents/src/runs/background/async-execution.ts +23 -7
  22. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +98 -3
  23. package/dist/builtin/subagents/src/runs/background/async-status.ts +3 -1
  24. package/dist/builtin/subagents/src/runs/background/run-status.ts +1 -1
  25. package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +3 -0
  26. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +37 -12
  27. package/dist/builtin/subagents/src/runs/foreground/chain-clarify.ts +15 -15
  28. package/dist/builtin/subagents/src/runs/foreground/execution.ts +26 -2
  29. package/dist/builtin/subagents/src/runs/shared/nested-render.ts +1 -1
  30. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +7 -0
  31. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +28 -1
  32. package/dist/builtin/subagents/src/shared/fast-mode.ts +80 -0
  33. package/dist/builtin/subagents/src/shared/formatters.ts +4 -2
  34. package/dist/builtin/subagents/src/shared/types.ts +4 -2
  35. package/dist/builtin/subagents/src/shared/utils.ts +3 -61
  36. package/dist/builtin/subagents/src/tui/render.ts +303 -157
  37. package/dist/builtin/web-access/package.json +1 -1
  38. package/dist/builtin/workflows/CHANGELOG.md +101 -35
  39. package/dist/builtin/workflows/README.md +228 -41
  40. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +535 -541
  41. package/dist/builtin/workflows/builtin/goal.ts +39 -25
  42. package/dist/builtin/workflows/builtin/open-claude-design.ts +66 -69
  43. package/dist/builtin/workflows/builtin/ralph.ts +21 -21
  44. package/dist/builtin/workflows/package.json +6 -5
  45. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +1 -1
  46. package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +2 -2
  47. package/dist/builtin/workflows/src/extension/discovery.ts +25 -146
  48. package/dist/builtin/workflows/src/extension/dispatcher.ts +72 -24
  49. package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +363 -0
  50. package/dist/builtin/workflows/src/extension/index.ts +690 -352
  51. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +99 -62
  52. package/dist/builtin/workflows/src/extension/render-call.ts +2 -1
  53. package/dist/builtin/workflows/src/extension/render-result.ts +9 -3
  54. package/dist/builtin/workflows/src/extension/renderers.ts +5 -3
  55. package/dist/builtin/workflows/src/extension/runtime.ts +68 -33
  56. package/dist/builtin/workflows/src/extension/status-writer.ts +1 -1
  57. package/dist/builtin/workflows/src/extension/wiring.ts +34 -13
  58. package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +142 -0
  59. package/dist/builtin/workflows/src/extension/workflow-schema.ts +4 -4
  60. package/dist/builtin/workflows/src/index.ts +2 -0
  61. package/dist/builtin/workflows/src/intercom/result-intercom.ts +1 -1
  62. package/dist/builtin/workflows/src/runs/background/runner.ts +6 -4
  63. package/dist/builtin/workflows/src/runs/background/status.ts +45 -21
  64. package/dist/builtin/workflows/src/runs/foreground/executor.ts +624 -52
  65. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +1 -1
  66. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +80 -24
  67. package/dist/builtin/workflows/src/runs/shared/validate-inputs.ts +61 -24
  68. package/dist/builtin/workflows/src/runs/shared/workflow-runner.ts +32 -10
  69. package/dist/builtin/workflows/src/sdk-surface.ts +6 -0
  70. package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +178 -0
  71. package/dist/builtin/workflows/src/shared/persistence-restore.ts +92 -12
  72. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +21 -3
  73. package/dist/builtin/workflows/src/shared/render-inputs-schema.ts +1 -2
  74. package/dist/builtin/workflows/src/shared/run-visibility.ts +9 -0
  75. package/dist/builtin/workflows/src/shared/schema-introspection.ts +121 -0
  76. package/dist/builtin/workflows/src/shared/serializable.ts +132 -0
  77. package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +91 -9
  78. package/dist/builtin/workflows/src/shared/store-types.ts +31 -3
  79. package/dist/builtin/workflows/src/shared/store.ts +58 -14
  80. package/dist/builtin/workflows/src/shared/types.ts +105 -40
  81. package/dist/builtin/workflows/src/tui/chat-surface-message.ts +129 -13
  82. package/dist/builtin/workflows/src/tui/chat-surface.ts +6 -1
  83. package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +3 -2
  84. package/dist/builtin/workflows/src/tui/graph-canvas.ts +1 -1
  85. package/dist/builtin/workflows/src/tui/graph-view.ts +91 -65
  86. package/dist/builtin/workflows/src/tui/inline-form-card.ts +1 -1
  87. package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +3 -2
  88. package/dist/builtin/workflows/src/tui/inputs-overlay.ts +3 -2
  89. package/dist/builtin/workflows/src/tui/inputs-picker.ts +8 -7
  90. package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +2 -0
  91. package/dist/builtin/workflows/src/tui/node-card.ts +34 -8
  92. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -11
  93. package/dist/builtin/workflows/src/tui/prompt-card.ts +98 -50
  94. package/dist/builtin/workflows/src/tui/session-list.ts +7 -2
  95. package/dist/builtin/workflows/src/tui/session-picker.ts +2 -0
  96. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +226 -55
  97. package/dist/builtin/workflows/src/tui/status-helpers.ts +2 -0
  98. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +37 -158
  99. package/dist/builtin/workflows/src/tui/toast.ts +2 -2
  100. package/dist/builtin/workflows/src/tui/widget.ts +53 -12
  101. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +270 -19
  102. package/dist/builtin/workflows/src/tui/workflow-notice-card.ts +184 -0
  103. package/dist/builtin/workflows/src/workflows/define-workflow.ts +138 -43
  104. package/dist/config.d.ts +9 -0
  105. package/dist/config.d.ts.map +1 -1
  106. package/dist/config.js +45 -0
  107. package/dist/config.js.map +1 -1
  108. package/dist/core/agent-session.d.ts +27 -9
  109. package/dist/core/agent-session.d.ts.map +1 -1
  110. package/dist/core/agent-session.js +196 -17
  111. package/dist/core/agent-session.js.map +1 -1
  112. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  113. package/dist/core/atomic-guide-command.js +2 -2
  114. package/dist/core/atomic-guide-command.js.map +1 -1
  115. package/dist/core/codex-fast-mode.d.ts +36 -0
  116. package/dist/core/codex-fast-mode.d.ts.map +1 -0
  117. package/dist/core/codex-fast-mode.js +117 -0
  118. package/dist/core/codex-fast-mode.js.map +1 -0
  119. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  120. package/dist/core/compaction/branch-summarization.js +1 -1
  121. package/dist/core/compaction/branch-summarization.js.map +1 -1
  122. package/dist/core/compaction/compaction.d.ts.map +1 -1
  123. package/dist/core/compaction/compaction.js +1 -1
  124. package/dist/core/compaction/compaction.js.map +1 -1
  125. package/dist/core/extensions/index.d.ts +4 -1
  126. package/dist/core/extensions/index.d.ts.map +1 -1
  127. package/dist/core/extensions/index.js +1 -0
  128. package/dist/core/extensions/index.js.map +1 -1
  129. package/dist/core/extensions/loader.d.ts +7 -2
  130. package/dist/core/extensions/loader.d.ts.map +1 -1
  131. package/dist/core/extensions/loader.js +23 -8
  132. package/dist/core/extensions/loader.js.map +1 -1
  133. package/dist/core/extensions/reactive-widget.d.ts +58 -0
  134. package/dist/core/extensions/reactive-widget.d.ts.map +1 -0
  135. package/dist/core/extensions/reactive-widget.js +182 -0
  136. package/dist/core/extensions/reactive-widget.js.map +1 -0
  137. package/dist/core/extensions/runner.d.ts.map +1 -1
  138. package/dist/core/extensions/runner.js +1 -0
  139. package/dist/core/extensions/runner.js.map +1 -1
  140. package/dist/core/extensions/types.d.ts +26 -12
  141. package/dist/core/extensions/types.d.ts.map +1 -1
  142. package/dist/core/extensions/types.js.map +1 -1
  143. package/dist/core/messages.d.ts +1 -1
  144. package/dist/core/messages.d.ts.map +1 -1
  145. package/dist/core/messages.js +8 -2
  146. package/dist/core/messages.js.map +1 -1
  147. package/dist/core/model-registry.d.ts +4 -0
  148. package/dist/core/model-registry.d.ts.map +1 -1
  149. package/dist/core/model-registry.js +11 -0
  150. package/dist/core/model-registry.js.map +1 -1
  151. package/dist/core/resource-loader.d.ts +9 -1
  152. package/dist/core/resource-loader.d.ts.map +1 -1
  153. package/dist/core/resource-loader.js +49 -21
  154. package/dist/core/resource-loader.js.map +1 -1
  155. package/dist/core/sdk.d.ts.map +1 -1
  156. package/dist/core/sdk.js +22 -13
  157. package/dist/core/sdk.js.map +1 -1
  158. package/dist/core/session-manager.d.ts +7 -5
  159. package/dist/core/session-manager.d.ts.map +1 -1
  160. package/dist/core/session-manager.js +5 -3
  161. package/dist/core/session-manager.js.map +1 -1
  162. package/dist/core/settings-manager.d.ts +16 -0
  163. package/dist/core/settings-manager.d.ts.map +1 -1
  164. package/dist/core/settings-manager.js +64 -5
  165. package/dist/core/settings-manager.js.map +1 -1
  166. package/dist/core/slash-commands.d.ts.map +1 -1
  167. package/dist/core/slash-commands.js +1 -0
  168. package/dist/core/slash-commands.js.map +1 -1
  169. package/dist/core/system-prompt.d.ts.map +1 -1
  170. package/dist/core/system-prompt.js +7 -4
  171. package/dist/core/system-prompt.js.map +1 -1
  172. package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
  173. package/dist/core/tools/ask-user-question/ask-user-question.js +2 -2
  174. package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
  175. package/dist/index.d.ts +4 -3
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +3 -2
  178. package/dist/index.js.map +1 -1
  179. package/dist/main.d.ts +3 -0
  180. package/dist/main.d.ts.map +1 -1
  181. package/dist/main.js +12 -0
  182. package/dist/main.js.map +1 -1
  183. package/dist/modes/interactive/chat-input-actions.d.ts.map +1 -1
  184. package/dist/modes/interactive/chat-input-actions.js.map +1 -1
  185. package/dist/modes/interactive/components/diff.d.ts.map +1 -1
  186. package/dist/modes/interactive/components/diff.js +0 -1
  187. package/dist/modes/interactive/components/diff.js.map +1 -1
  188. package/dist/modes/interactive/components/fast-mode-selector.d.ts +27 -0
  189. package/dist/modes/interactive/components/fast-mode-selector.d.ts.map +1 -0
  190. package/dist/modes/interactive/components/fast-mode-selector.js +105 -0
  191. package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -0
  192. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  193. package/dist/modes/interactive/components/footer.js +7 -12
  194. package/dist/modes/interactive/components/footer.js.map +1 -1
  195. package/dist/modes/interactive/components/index.d.ts +1 -0
  196. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  197. package/dist/modes/interactive/components/index.js +1 -0
  198. package/dist/modes/interactive/components/index.js.map +1 -1
  199. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  200. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  201. package/dist/modes/interactive/interactive-mode.js +132 -30
  202. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  203. package/dist/modes/print-mode.d.ts.map +1 -1
  204. package/dist/modes/print-mode.js +53 -6
  205. package/dist/modes/print-mode.js.map +1 -1
  206. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  207. package/dist/modes/rpc/rpc-mode.js +3 -0
  208. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  209. package/docs/compaction.md +1 -1
  210. package/docs/custom-provider.md +2 -2
  211. package/docs/development.md +2 -2
  212. package/docs/docs.json +2 -2
  213. package/docs/extensions.md +18 -13
  214. package/docs/providers.md +5 -1
  215. package/docs/quickstart.md +5 -3
  216. package/docs/rpc.md +5 -5
  217. package/docs/sdk.md +12 -12
  218. package/docs/settings.md +18 -0
  219. package/docs/themes.md +6 -6
  220. package/docs/tui.md +20 -18
  221. package/docs/usage.md +2 -0
  222. package/docs/workflows.md +403 -39
  223. package/examples/extensions/qna.ts +2 -2
  224. package/package.json +4 -4
  225. package/dist/builtin/subagents/skills/playwright-cli/SKILL.md +0 -392
  226. package/dist/builtin/subagents/skills/playwright-cli/references/element-attributes.md +0 -23
  227. package/dist/builtin/subagents/skills/playwright-cli/references/playwright-tests.md +0 -39
  228. package/dist/builtin/subagents/skills/playwright-cli/references/request-mocking.md +0 -87
  229. package/dist/builtin/subagents/skills/playwright-cli/references/running-code.md +0 -241
  230. package/dist/builtin/subagents/skills/playwright-cli/references/session-management.md +0 -225
  231. package/dist/builtin/subagents/skills/playwright-cli/references/spec-driven-testing.md +0 -305
  232. package/dist/builtin/subagents/skills/playwright-cli/references/storage-state.md +0 -275
  233. package/dist/builtin/subagents/skills/playwright-cli/references/test-generation.md +0 -134
  234. package/dist/builtin/subagents/skills/playwright-cli/references/tracing.md +0 -139
  235. package/dist/builtin/subagents/skills/playwright-cli/references/video-recording.md +0 -143
@@ -5,7 +5,7 @@
5
5
  import { createHash } from "node:crypto";
6
6
  import { mkdir, writeFile } from "node:fs/promises";
7
7
  import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
8
- import { CONFIG_DIR_NAME, createAskUserQuestionToolDefinition } from "@bastani/atomic";
8
+ import { CONFIG_DIR_NAME, createAskUserQuestionToolDefinition, isCodexFastModeCandidateModelId } from "@bastani/atomic";
9
9
  import { stageUiBroker } from "../../shared/stage-ui-broker.js";
10
10
  import { buildStagePromptAdapter } from "../../shared/stage-prompt.js";
11
11
  import type {
@@ -34,6 +34,13 @@ import type {
34
34
  WorkflowPersistencePort,
35
35
  WorkflowRuntimeConfig,
36
36
  WorkflowModelCatalogPort,
37
+ WorkflowExecutionMode,
38
+ WorkflowRunChildOptions,
39
+ WorkflowChildResult,
40
+ WorkflowOutputSchema,
41
+ WorkflowOutputValues,
42
+ WorkflowInputValues,
43
+ WorkflowSerializableValue,
37
44
  } from "../../shared/types.js";
38
45
  import type { InternalStageContext, StageAdapters } from "./stage-runner.js";
39
46
  import type {
@@ -45,9 +52,12 @@ import type {
45
52
  WorkflowFailureKind,
46
53
  PendingPrompt,
47
54
  PromptKind,
55
+ WorkflowChildReplaySnapshot,
56
+ WorkflowChildRunRef,
48
57
  } from "../../shared/store-types.js";
49
58
  import type { StageControlHandle, StageControlRegistry, AgentSessionEventListener } from "./stage-control-registry.js";
50
59
  import type { Store } from "../../shared/store.js";
60
+ import type { WorkflowRegistry } from "../../workflows/registry.js";
51
61
  import type { CancellationRegistry } from "../background/cancellation-registry.js";
52
62
  import { createStageContext } from "./stage-runner.js";
53
63
  import { GraphFrontierTracker } from "../shared/graph-inference.js";
@@ -71,12 +81,21 @@ import {
71
81
  appendStageEnd,
72
82
  appendRunEnd,
73
83
  } from "../../shared/persistence-session-entries.js";
74
- import { validateWorkflowModels } from "../shared/model-fallback.js";
84
+ import { buildModelCandidatesFromCatalog, validateWorkflowModels, workflowModelId } from "../shared/model-fallback.js";
85
+ import { validateInputs, type ValidationError } from "../shared/validate-inputs.js";
86
+ import { Type, type TSchema } from "typebox";
87
+ import { Value } from "typebox/value";
88
+ import { schemaFieldKind, schemaChoices, schemaIsRequired } from "../../shared/schema-introspection.js";
75
89
  import type { WorkflowFailure } from "../../shared/workflow-failures.js";
76
90
  import { classifyWorkflowFailure } from "../../shared/workflow-failures.js";
77
91
  import { selectPromptCallsiteFrame } from "../shared/prompt-callsite.js";
92
+ import {
93
+ assertWorkflowSerializableObject,
94
+ workflowSerializableValidationError,
95
+ workflowSerializableTypeName,
96
+ } from "../../shared/serializable.js";
78
97
 
79
- export interface ResolvedInputs extends Record<string, unknown> {}
98
+ export interface ResolvedInputs extends WorkflowInputValues {}
80
99
 
81
100
  export interface RunContinuationOpts {
82
101
  readonly source: RunSnapshot;
@@ -89,6 +108,8 @@ export interface RunOpts {
89
108
  cwd?: string;
90
109
  /** HIL adapter injected by the pi runtime or test harness. */
91
110
  ui?: WorkflowUIAdapter;
111
+ /** Runtime execution mode. Controls child session policy metadata. */
112
+ executionMode?: WorkflowExecutionMode;
92
113
  /** Internal detached-run mode: surface ctx.ui.* as node-local workflow prompt stages. */
93
114
  usePromptNodesForUi?: boolean;
94
115
  /**
@@ -132,6 +153,8 @@ export interface RunOpts {
132
153
  config?: WorkflowRuntimeConfig;
133
154
  /** Optional model catalog used for fallback validation/resolution. */
134
155
  models?: WorkflowModelCatalogPort;
156
+ /** Registry metadata forwarded to workflow runs launched from discovery/tooling. */
157
+ registry?: WorkflowRegistry;
135
158
  /**
136
159
  * Current nesting depth of this workflow run. Starts at 0 for top-level runs.
137
160
  * Callers that spawn nested runs must increment this by 1 before passing to
@@ -155,16 +178,22 @@ export interface RunOpts {
155
178
  runId?: string;
156
179
  /** Replay completed stages from a failed source run, then resume at this stage. */
157
180
  continuation?: RunContinuationOpts;
181
+ /** Internal parent linkage for nested ctx.workflow(...) runs. */
182
+ parentRun?: {
183
+ readonly runId: string;
184
+ readonly stageId: string;
185
+ readonly rootRunId: string;
186
+ };
158
187
  onRunStart?: (snapshot: RunSnapshot) => void;
159
188
  onStageStart?: (runId: string, snapshot: StageSnapshot) => void;
160
189
  onStageEnd?: (runId: string, snapshot: StageSnapshot) => void;
161
- onRunEnd?: (runId: string, status: RunStatus, result?: Record<string, unknown>, error?: string) => void;
190
+ onRunEnd?: (runId: string, status: RunStatus, result?: WorkflowOutputValues, error?: string) => void;
162
191
  }
163
192
 
164
193
  export interface RunResult {
165
194
  readonly runId: string;
166
195
  readonly status: RunStatus;
167
- readonly result?: Record<string, unknown>;
196
+ readonly result?: WorkflowOutputValues;
168
197
  readonly error?: string;
169
198
  readonly stages: StageSnapshot[];
170
199
  }
@@ -175,19 +204,25 @@ export interface RunResult {
175
204
 
176
205
  export function resolveInputs(
177
206
  schema: Readonly<Record<string, WorkflowInputSchema>>,
178
- provided: Record<string, unknown>,
207
+ provided: Readonly<Record<string, unknown>>,
179
208
  ): ResolvedInputs {
180
- const resolved: Record<string, unknown> = { ...provided };
209
+ const resolved: Record<string, WorkflowSerializableValue> = {};
210
+ for (const [key, value] of Object.entries(provided)) {
211
+ if (value !== undefined) resolved[key] = value as WorkflowSerializableValue;
212
+ }
181
213
 
182
- for (const [key, schemaDef] of Object.entries(schema)) {
183
- if (resolved[key] === undefined && "default" in schemaDef && schemaDef.default !== undefined) {
184
- resolved[key] = schemaDef.default;
185
- }
214
+ // Apply declared TypeBox defaults (top-level and nested) for absent keys.
215
+ const withDefaults = Value.Default(
216
+ Type.Object(schema as Record<string, TSchema>, { additionalProperties: true }),
217
+ resolved,
218
+ ) as Record<string, WorkflowSerializableValue>;
219
+ for (const [key, value] of Object.entries(withDefaults)) {
220
+ if (value !== undefined) resolved[key] = value;
186
221
  }
187
222
 
188
223
  for (const [key, schemaDef] of Object.entries(schema)) {
189
- if (schemaDef.required === true && resolved[key] === undefined) {
190
- throw new TypeError(`pi-workflows: required input "${key}" not provided`);
224
+ if (schemaIsRequired(schemaDef) && resolved[key] === undefined) {
225
+ throw new TypeError(`atomic-workflows: required input "${key}" not provided`);
191
226
  }
192
227
  }
193
228
 
@@ -198,7 +233,10 @@ function resolveInputConcurrency(
198
233
  schema: Readonly<Record<string, WorkflowInputSchema>>,
199
234
  resolvedInputs: ResolvedInputs,
200
235
  ): number | undefined {
201
- if (schema["max_concurrency"]?.type !== "number") return undefined;
236
+ const concurrencySchema = schema["max_concurrency"];
237
+ if (concurrencySchema === undefined || schemaFieldKind(concurrencySchema) !== "number") {
238
+ return undefined;
239
+ }
202
240
 
203
241
  const value = resolvedInputs["max_concurrency"];
204
242
  if (typeof value !== "number" || !Number.isFinite(value) || value < 1) return undefined;
@@ -287,12 +325,12 @@ function promptCallsiteHash(): string {
287
325
  function hilAbortError(signal: AbortSignal): Error {
288
326
  return signal.reason instanceof Error
289
327
  ? signal.reason
290
- : new Error("pi-workflows: HIL aborted");
328
+ : new Error("atomic-workflows: HIL aborted");
291
329
  }
292
330
 
293
331
  function makeUnavailableUIContext(): WorkflowUIContext {
294
332
  const msg = (primitive: string): string =>
295
- `pi-workflows: HIL ctx.ui.${primitive} is unavailable because pi runtime did not provide a UI adapter`;
333
+ `atomic-workflows: HIL ctx.ui.${primitive} is unavailable because Atomic runtime did not provide a UI adapter`;
296
334
  return {
297
335
  input: () => Promise.reject(new Error(msg("input"))),
298
336
  confirm: () => Promise.reject(new Error(msg("confirm"))),
@@ -434,8 +472,9 @@ export async function askReadinessViaStageBroker(
434
472
  // Expose a headless-answer adapter for the gate so it can be answered
435
473
  // programmatically (e.g. `workflow send`) without a TUI host. The gate
436
474
  // question params are known statically here.
475
+ const gatePromptId = `readiness-gate-${stageId}-${crypto.randomUUID()}`;
437
476
  const gateAdapter = buildStagePromptAdapter(
438
- `readiness-gate-${stageId}`,
477
+ gatePromptId,
439
478
  "readiness_gate",
440
479
  READINESS_GATE_QUESTION_PARAMS,
441
480
  Date.now(),
@@ -443,7 +482,7 @@ export async function askReadinessViaStageBroker(
443
482
  if (gateAdapter) stageUiBroker.provideStagePrompt(runId, stageId, gateAdapter);
444
483
  try {
445
484
  const result = await execute(
446
- `readiness-gate-${stageId}`,
485
+ gatePromptId,
447
486
  READINESS_GATE_QUESTION_PARAMS as Parameters<typeof execute>[1],
448
487
  signal,
449
488
  undefined,
@@ -499,7 +538,7 @@ function applyTaskContext(prompt: string, previous: WorkflowTaskOptions["previou
499
538
  function taskPrompt(options: WorkflowTaskOptions): string {
500
539
  const prompt = options.prompt ?? options.task;
501
540
  if (prompt === undefined) {
502
- throw new Error("pi-workflows: ctx.task requires options.prompt or options.task");
541
+ throw new Error("atomic-workflows: ctx.task requires options.prompt or options.task");
503
542
  }
504
543
  return prompt;
505
544
  }
@@ -819,7 +858,7 @@ async function mapParallelSteps<T>(
819
858
  if (failures.length > 0) {
820
859
  throw new AggregateError(
821
860
  failures.map((failure) => failure.error),
822
- `pi-workflows: ${failures.length} parallel ${failures.length === 1 ? "step" : "steps"} failed`,
861
+ `atomic-workflows: ${failures.length} parallel ${failures.length === 1 ? "step" : "steps"} failed`,
823
862
  );
824
863
  }
825
864
 
@@ -831,7 +870,7 @@ function expandedParallelTasks(tasks: readonly WorkflowDirectTaskItem[]): Workfl
831
870
  for (const task of tasks) {
832
871
  const count = task.count ?? 1;
833
872
  if (!Number.isInteger(count) || count < 1) {
834
- throw new Error(`pi-workflows: direct task "${task.name}" count must be a positive integer`);
873
+ throw new Error(`atomic-workflows: direct task "${task.name}" count must be a positive integer`);
835
874
  }
836
875
  for (let index = 0; index < count; index += 1) {
837
876
  expanded.push(count === 1 ? task : {
@@ -933,7 +972,7 @@ function prepareDirectWorktrees(
933
972
  }
934
973
 
935
974
  if (typeof options.gitWorktreeDir === "string" || tasks.some((task) => typeof task.gitWorktreeDir === "string")) {
936
- throw new Error("pi-workflows: worktree and gitWorktreeDir are mutually exclusive; use gitWorktreeDir for a reusable worktree or worktree:true for temporary isolated worktrees.");
975
+ throw new Error("atomic-workflows: worktree and gitWorktreeDir are mutually exclusive; use gitWorktreeDir for a reusable worktree or worktree:true for temporary isolated worktrees.");
937
976
  }
938
977
 
939
978
  const sharedCwd = resolveSharedDirectWorktreeCwd(tasks);
@@ -1088,13 +1127,27 @@ function workflowDetailsFromRun(
1088
1127
  };
1089
1128
  }
1090
1129
 
1091
- const EMPTY_WORKFLOW_GRAPH_ERROR_MESSAGE = "Workflow run completed without creating any workflow stages. Create at least one stage with ctx.stage(), ctx.task(), ctx.chain(), or ctx.parallel().";
1130
+ const EMPTY_WORKFLOW_GRAPH_ERROR_MESSAGE = "Workflow run completed without creating any workflow stages. Create at least one stage with ctx.stage(), ctx.task(), ctx.chain(), ctx.parallel(), or ctx.workflow().";
1092
1131
 
1093
1132
  function assertWorkflowCreatedStage(runSnapshot: RunSnapshot): void {
1094
1133
  if (runSnapshot.stages.length > 0) return;
1095
1134
  throw new Error(EMPTY_WORKFLOW_GRAPH_ERROR_MESSAGE);
1096
1135
  }
1097
1136
 
1137
+ // Direct (task/parallel/chain) execution synthesizes ephemeral workflows that
1138
+ // expose tool-parity outputs. They are declared explicitly like any other
1139
+ // workflow so the fully-explicit output contract holds on the direct path too.
1140
+ // `unknown` accepts any serializable value, and every key is optional because a
1141
+ // given direct mode only returns the subset it produces (e.g. `count` for chain/
1142
+ // parallel, `text` for single task, `worktreeSummary` only with worktrees).
1143
+ const DIRECT_WORKFLOW_OUTPUTS: Readonly<Record<string, WorkflowOutputSchema>> = Object.freeze({
1144
+ results: Type.Optional(Type.Unknown()),
1145
+ text: Type.Optional(Type.Unknown()),
1146
+ count: Type.Optional(Type.Unknown()),
1147
+ artifacts: Type.Optional(Type.Unknown()),
1148
+ worktreeSummary: Type.Optional(Type.Unknown()),
1149
+ });
1150
+
1098
1151
  function defineDirectWorkflow(
1099
1152
  name: string,
1100
1153
  runFn: WorkflowDefinition["run"],
@@ -1105,6 +1158,7 @@ function defineDirectWorkflow(
1105
1158
  normalizedName: name,
1106
1159
  description: "Direct workflow execution",
1107
1160
  inputs: Object.freeze({}),
1161
+ outputs: DIRECT_WORKFLOW_OUTPUTS,
1108
1162
  run: runFn,
1109
1163
  });
1110
1164
  }
@@ -1337,7 +1391,7 @@ function appendRunEndWhenRecorded(
1337
1391
  payload: {
1338
1392
  readonly runId: string;
1339
1393
  readonly status: RunStatus;
1340
- readonly result?: Record<string, unknown>;
1394
+ readonly result?: WorkflowOutputValues;
1341
1395
  readonly error?: string;
1342
1396
  readonly failureKind?: WorkflowFailureKind;
1343
1397
  readonly failureMessage?: string;
@@ -1419,7 +1473,7 @@ interface ContinuationReplayInput {
1419
1473
  readonly replayKey: string;
1420
1474
  readonly parentIds: readonly string[];
1421
1475
  readonly stageId: string;
1422
- readonly kind: "stage" | "prompt";
1476
+ readonly kind: "stage" | "prompt" | "workflow";
1423
1477
  }
1424
1478
 
1425
1479
  interface ContinuationReplayIndex {
@@ -1450,7 +1504,7 @@ function createContinuationReplayIndex(continuation: RunContinuationOpts | undef
1450
1504
  }
1451
1505
  const resumeStage = continuation.source.stages.find((stage) => stage.id === continuation.resumeFromStageId);
1452
1506
  if (resumeStage === undefined) {
1453
- throw new Error(`pi-workflows: insufficient_state: resume stage ${continuation.resumeFromStageId} was not found in source run ${continuation.source.id}`);
1507
+ throw new Error(`atomic-workflows: insufficient_state: resume stage ${continuation.resumeFromStageId} was not found in source run ${continuation.source.id}`);
1454
1508
  }
1455
1509
 
1456
1510
  const stagesByReplayIdentity = new Map<string, StageSnapshot[]>();
@@ -1472,7 +1526,7 @@ function createContinuationReplayIndex(continuation: RunContinuationOpts | undef
1472
1526
  const replayablePromptContinuationStageIds = new Set<string>();
1473
1527
 
1474
1528
  const failTopology = (displayName: string, replayKey: string, reason: "mismatch" | "ambiguous"): never => {
1475
- throw new Error(`pi-workflows: insufficient_state: replay topology ${reason} for stage "${displayName}" (replayKey "${replayKey}") in source run ${continuation.source.id}`);
1529
+ throw new Error(`atomic-workflows: insufficient_state: replay topology ${reason} for stage "${displayName}" (replayKey "${replayKey}") in source run ${continuation.source.id}`);
1476
1530
  };
1477
1531
 
1478
1532
  const translateSourceParents = (source: StageSnapshot): string[] | undefined => {
@@ -1618,15 +1672,197 @@ function nextEventLoopTurn(): Promise<void> {
1618
1672
  return new Promise((resolve) => setTimeout(resolve, 0));
1619
1673
  }
1620
1674
 
1621
- export async function run<TInputs extends Record<string, unknown>>(
1675
+ function formatValidationErrors(errors: readonly ValidationError[]): string {
1676
+ return errors.map((error) => ` - ${error.key}: ${error.reason}`).join("\n");
1677
+ }
1678
+
1679
+ export function resolveAndValidateInputs(
1680
+ schema: Readonly<Record<string, WorkflowInputSchema>>,
1681
+ provided: Readonly<Record<string, unknown>>,
1682
+ scope: string,
1683
+ ): ResolvedInputs {
1684
+ const resolved = resolveInputs(schema, provided);
1685
+ const errors = validateInputs(schema, resolved);
1686
+ if (errors.length > 0) {
1687
+ throw new TypeError(
1688
+ `atomic-workflows: invalid inputs for ${scope}:\n${formatValidationErrors(errors)}`,
1689
+ );
1690
+ }
1691
+ return resolved;
1692
+ }
1693
+
1694
+ function hasOwnWorkflowOutput(record: WorkflowOutputValues | Readonly<Record<string, WorkflowOutputSchema>>, key: string): boolean {
1695
+ return Object.prototype.hasOwnProperty.call(record, key);
1696
+ }
1697
+
1698
+ // Workflow outputs are fully explicit: a workflow exposes exactly the outputs it
1699
+ // declares with `.output(...)`. There is no implicit `result` fallback, and a
1700
+ // `.run()` return that contains a key the workflow did not declare is an error,
1701
+ // so authors cannot silently leak undeclared values across a workflow boundary.
1702
+ function assertWorkflowOutputsExplicit(
1703
+ scope: string,
1704
+ sourceOutput: WorkflowOutputValues,
1705
+ declarations: Readonly<Record<string, WorkflowOutputSchema>>,
1706
+ missingOutputSuffix = "",
1707
+ ): void {
1708
+ for (const key of Object.keys(sourceOutput)) {
1709
+ if (!hasOwnWorkflowOutput(declarations, key)) {
1710
+ throw new Error(
1711
+ `atomic-workflows: ${scope} returned undeclared output "${key}"; declare it with .output("${key}", Type....) or remove it from the .run() return`,
1712
+ );
1713
+ }
1714
+ }
1715
+ for (const [key, schema] of Object.entries(declarations)) {
1716
+ if (!(key in sourceOutput)) {
1717
+ if (schemaIsRequired(schema)) {
1718
+ throw new Error(
1719
+ `atomic-workflows: ${scope} missing output "${key}"${missingOutputSuffix}`,
1720
+ );
1721
+ }
1722
+ continue;
1723
+ }
1724
+ const value = sourceOutput[key];
1725
+ const kind = schemaFieldKind(schema);
1726
+ if (!Value.Check(schema, value)) {
1727
+ const choices = schemaChoices(schema);
1728
+ if (kind === "select" && choices !== undefined && typeof value === "string") {
1729
+ throw new Error(
1730
+ `atomic-workflows: ${scope} output "${key}" must be one of [${choices.join(", ")}], got ${JSON.stringify(value)}`,
1731
+ );
1732
+ }
1733
+ throw new Error(
1734
+ `atomic-workflows: ${scope} output "${key}" expected ${kind}, got ${workflowSerializableTypeName(value)}`,
1735
+ );
1736
+ }
1737
+ const serializableError = workflowSerializableValidationError(
1738
+ value,
1739
+ `${scope} output "${key}"`,
1740
+ );
1741
+ if (serializableError !== undefined) {
1742
+ throw new Error(`atomic-workflows: ${serializableError}`);
1743
+ }
1744
+ }
1745
+ }
1746
+
1747
+ function normalizeWorkflowRunOutput(
1748
+ workflowName: string,
1749
+ rawOutput: unknown,
1750
+ ): WorkflowOutputValues | undefined {
1751
+ if (rawOutput === undefined) return undefined;
1752
+ // Drop top-level keys explicitly set to `undefined` so conditional outputs
1753
+ // (e.g. `{ note: cond ? value : undefined }`) satisfy the JSON-serializable
1754
+ // contract instead of failing validation; selectWorkflowOutputs strips the
1755
+ // same way at the child boundary, keeping both paths consistent.
1756
+ const normalized =
1757
+ rawOutput !== null && typeof rawOutput === "object" && !Array.isArray(rawOutput)
1758
+ ? Object.fromEntries(
1759
+ Object.entries(rawOutput as Record<string, unknown>).filter(([, v]) => v !== undefined),
1760
+ )
1761
+ : rawOutput;
1762
+ assertWorkflowSerializableObject(normalized, `workflow "${workflowName}" .run() return`);
1763
+ return normalized;
1764
+ }
1765
+
1766
+ function assertWorkflowRunOutputs(
1767
+ workflowName: string,
1768
+ result: WorkflowOutputValues | undefined,
1769
+ declaredOutputs: Readonly<Record<string, WorkflowOutputSchema>> | undefined,
1770
+ ): void {
1771
+ assertWorkflowOutputsExplicit(
1772
+ `workflow "${workflowName}"`,
1773
+ result ?? {},
1774
+ declaredOutputs ?? {},
1775
+ );
1776
+ }
1777
+
1778
+ function selectWorkflowOutputs(
1779
+ child: WorkflowDefinition,
1780
+ rawOutput: WorkflowOutputValues | undefined,
1781
+ ): WorkflowOutputValues {
1782
+ const declarations = child.outputs ?? {};
1783
+ const sourceOutput = rawOutput ?? {};
1784
+ // The child run already validated its return against these declared outputs
1785
+ // (assertWorkflowRunOutputs) before it could complete, so undeclared keys are
1786
+ // impossible here and a second assertWorkflowOutputsExplicit pass could never
1787
+ // fire. Just project the declared outputs the child returned. (An undeclared
1788
+ // key fails the child run itself; the parent surfaces that as a wrapped
1789
+ // "child workflow ... failed" error.)
1790
+ const selected: Record<string, WorkflowSerializableValue> = {};
1791
+ for (const key of Object.keys(declarations)) {
1792
+ const value = sourceOutput[key];
1793
+ if (value !== undefined) selected[key] = value;
1794
+ }
1795
+
1796
+ return selected;
1797
+ }
1798
+
1799
+ function cloneWorkflowChildValue<T>(value: T): T {
1800
+ return structuredClone(value);
1801
+ }
1802
+
1803
+ function workflowChildSerializationMessage(err: unknown): string {
1804
+ return err instanceof Error ? err.message : String(err);
1805
+ }
1806
+
1807
+ function isWorkflowDefinition(value: unknown): value is WorkflowDefinition {
1808
+ if (value === null || typeof value !== "object") return false;
1809
+ const record = value as Partial<WorkflowDefinition>;
1810
+ return record.__piWorkflow === true &&
1811
+ typeof record.name === "string" && record.name.trim().length > 0 &&
1812
+ typeof record.normalizedName === "string" && record.normalizedName.trim().length > 0 &&
1813
+ typeof record.run === "function" &&
1814
+ // Compiled definitions always set `inputs: {}`; guard it so a handcrafted
1815
+ // object that passes the sentinel still fails here with the clear "requires
1816
+ // a compiled workflow definition" error rather than crashing later inside
1817
+ // resolveAndValidateInputs(child.inputs, ...) on `Object.entries(undefined)`.
1818
+ typeof record.inputs === "object" && record.inputs !== null;
1819
+ }
1820
+
1821
+ function cloneWorkflowChildReplaySnapshot(snapshot: WorkflowChildReplaySnapshot): WorkflowChildReplaySnapshot {
1822
+ return {
1823
+ alias: snapshot.alias,
1824
+ workflow: snapshot.workflow,
1825
+ runId: snapshot.runId,
1826
+ status: snapshot.status,
1827
+ outputs: cloneWorkflowChildValue(snapshot.outputs),
1828
+ };
1829
+ }
1830
+
1831
+ function workflowChildReplaySnapshot(
1832
+ alias: string,
1833
+ childResult: WorkflowChildResult,
1834
+ ): WorkflowChildReplaySnapshot {
1835
+ const outputs: Record<string, WorkflowSerializableValue> = {};
1836
+ for (const [key, value] of Object.entries(childResult.outputs)) {
1837
+ if (value === undefined) continue;
1838
+ try {
1839
+ outputs[key] = cloneWorkflowChildValue(value);
1840
+ } catch (err) {
1841
+ throw new Error(
1842
+ `atomic-workflows: child workflow "${alias}" (${childResult.workflow}) exposed output "${key}" is not serializable for continuation replay: ${workflowChildSerializationMessage(err)}`,
1843
+ { cause: err },
1844
+ );
1845
+ }
1846
+ }
1847
+
1848
+ return {
1849
+ alias,
1850
+ workflow: childResult.workflow,
1851
+ runId: childResult.runId,
1852
+ status: childResult.status,
1853
+ outputs,
1854
+ };
1855
+ }
1856
+
1857
+ export async function run<TInputs extends WorkflowInputValues>(
1622
1858
  def: WorkflowDefinition<TInputs>,
1623
- inputs: Record<string, unknown>,
1859
+ inputs: Readonly<Record<string, unknown>>,
1624
1860
  opts: RunOpts = {},
1625
1861
  ): Promise<RunResult> {
1626
1862
  const activeStore = opts.store ?? defaultStore;
1627
1863
  const adapters = opts.adapters ?? {};
1628
1864
  if (opts.usePromptNodesForUi === true && opts.ui !== undefined) {
1629
- console.warn("pi-workflows: usePromptNodesForUi ignores the provided RunOpts.ui adapter");
1865
+ console.warn("atomic-workflows: usePromptNodesForUi ignores the provided RunOpts.ui adapter");
1630
1866
  }
1631
1867
 
1632
1868
  // 0. maxDepth guard — reject before any store/persistence side effects.
@@ -1637,13 +1873,17 @@ export async function run<TInputs extends Record<string, unknown>>(
1637
1873
  return {
1638
1874
  runId: opts.runId ?? crypto.randomUUID(),
1639
1875
  status: "failed",
1640
- error: `pi-workflows: maxDepth exceeded (max ${max})`,
1876
+ error: `atomic-workflows: maxDepth exceeded (max ${max})`,
1641
1877
  stages: [],
1642
1878
  };
1643
1879
  }
1644
1880
 
1645
1881
  // 1. Resolve + validate inputs
1646
- const resolvedInputs = resolveInputs(def.inputs, inputs);
1882
+ const resolvedInputs = resolveAndValidateInputs(
1883
+ def.inputs,
1884
+ inputs,
1885
+ `workflow "${def.name}"`,
1886
+ );
1647
1887
 
1648
1888
  // 2. Generate runId (or use pre-allocated seam from caller)
1649
1889
  const runId = opts.runId ?? crypto.randomUUID();
@@ -1668,6 +1908,11 @@ export async function run<TInputs extends Record<string, unknown>>(
1668
1908
  status: "running",
1669
1909
  stages: [],
1670
1910
  startedAt: Date.now(),
1911
+ ...(opts.parentRun !== undefined ? {
1912
+ parentRunId: opts.parentRun.runId,
1913
+ parentStageId: opts.parentRun.stageId,
1914
+ rootRunId: opts.parentRun.rootRunId,
1915
+ } : {}),
1671
1916
  ...(opts.continuation !== undefined ? {
1672
1917
  resumedFromRunId: opts.continuation.source.id,
1673
1918
  resumeFromStageId: opts.continuation.resumeFromStageId,
@@ -1692,6 +1937,9 @@ export async function run<TInputs extends Record<string, unknown>>(
1692
1937
  runId,
1693
1938
  name: def.name,
1694
1939
  inputs: resolvedInputs,
1940
+ ...(runSnapshot.parentRunId !== undefined ? { parentRunId: runSnapshot.parentRunId } : {}),
1941
+ ...(runSnapshot.parentStageId !== undefined ? { parentStageId: runSnapshot.parentStageId } : {}),
1942
+ ...(runSnapshot.rootRunId !== undefined ? { rootRunId: runSnapshot.rootRunId } : {}),
1695
1943
  ...(runSnapshot.resumedFromRunId !== undefined ? { resumedFromRunId: runSnapshot.resumedFromRunId } : {}),
1696
1944
  ...(runSnapshot.resumeFromStageId !== undefined ? { resumeFromStageId: runSnapshot.resumeFromStageId } : {}),
1697
1945
  ts: runSnapshot.startedAt,
@@ -1858,10 +2106,162 @@ export async function run<TInputs extends Record<string, unknown>>(
1858
2106
 
1859
2107
  ownController.signal.addEventListener(
1860
2108
  "abort",
1861
- () => rejectReleaseBarriers(ownController.signal.reason ?? new Error("pi-workflows: run aborted")),
2109
+ () => rejectReleaseBarriers(ownController.signal.reason ?? new Error("atomic-workflows: run aborted")),
1862
2110
  { once: true },
1863
2111
  );
1864
2112
 
2113
+ interface WorkflowBoundaryStage {
2114
+ readonly id: string;
2115
+ readonly replayedChild?: WorkflowChildResult;
2116
+ finalizeReplay(): void;
2117
+ linkChildRun(ref: WorkflowChildRunRef): void;
2118
+ complete(summary: string, workflowChild: WorkflowChildReplaySnapshot): void;
2119
+ fail(error: unknown): void;
2120
+ }
2121
+
2122
+ const workflowChildResultFromReplay = (snapshot: WorkflowChildReplaySnapshot): WorkflowChildResult => ({
2123
+ workflow: snapshot.workflow,
2124
+ runId: snapshot.runId,
2125
+ status: snapshot.status,
2126
+ outputs: cloneWorkflowChildValue(snapshot.outputs),
2127
+ });
2128
+
2129
+ const workflowBoundaryReplayCounts = new Map<string, number>();
2130
+ const nextWorkflowBoundaryReplayKey = (name: string): string => {
2131
+ const next = (workflowBoundaryReplayCounts.get(name) ?? 0) + 1;
2132
+ workflowBoundaryReplayCounts.set(name, next);
2133
+ return `workflow:${name}:${next}`;
2134
+ };
2135
+
2136
+ const startWorkflowBoundaryStage = (name: string, replayKey: string): WorkflowBoundaryStage => {
2137
+ const stageId = crypto.randomUUID();
2138
+ const provisionalParentIds = tracker.onSpawn(stageId, name);
2139
+ const replayDecision = replayIndex.decide({
2140
+ displayName: name,
2141
+ replayKey,
2142
+ parentIds: provisionalParentIds,
2143
+ stageId,
2144
+ kind: "workflow",
2145
+ });
2146
+ const parentIds = replayDecision.parentIds;
2147
+ if (!sameStringSet(parentIds, provisionalParentIds)) {
2148
+ tracker.replaceParents(stageId, parentIds);
2149
+ }
2150
+ const replaySource = replayDecision.source;
2151
+ const replayChildSnapshot = replayDecision.kind === "replay" ? replayDecision.source.workflowChild : undefined;
2152
+ const replayedChild = replayChildSnapshot !== undefined
2153
+ ? workflowChildResultFromReplay(replayChildSnapshot)
2154
+ : undefined;
2155
+ const startedAt = Date.now();
2156
+ const stageSnapshot: StageSnapshot = {
2157
+ id: stageId,
2158
+ name,
2159
+ replayKey,
2160
+ status: replayedChild !== undefined ? "completed" : "running",
2161
+ parentIds: Object.freeze([...parentIds]),
2162
+ startedAt,
2163
+ toolEvents: [],
2164
+ attachable: false,
2165
+ ...(replaySource !== undefined ? {
2166
+ replayedFromStageId: replaySource.id,
2167
+ replayed: replayedChild !== undefined,
2168
+ } : {}),
2169
+ ...(replayedChild !== undefined && replayChildSnapshot !== undefined ? {
2170
+ endedAt: startedAt,
2171
+ durationMs: 0,
2172
+ ...(replayDecision.kind === "replay" && replayDecision.source.result !== undefined ? { result: replayDecision.source.result } : {}),
2173
+ workflowChild: cloneWorkflowChildReplaySnapshot(replayChildSnapshot),
2174
+ } : {}),
2175
+ };
2176
+ let finalized = false;
2177
+
2178
+ const appendStageStartOnce = (): void => {
2179
+ if (!opts.persistence) return;
2180
+ appendStageStart(opts.persistence, {
2181
+ runId,
2182
+ stageId,
2183
+ name,
2184
+ parentIds: stageSnapshot.parentIds,
2185
+ ...stageReplayFields(stageSnapshot),
2186
+ ts: startedAt,
2187
+ });
2188
+ };
2189
+
2190
+ const appendStageEndForSnapshot = (): void => {
2191
+ if (!opts.persistence) return;
2192
+ appendStageEnd(opts.persistence, {
2193
+ runId,
2194
+ stageId,
2195
+ status: stageSnapshot.status,
2196
+ durationMs: stageSnapshot.durationMs,
2197
+ ...(stageSnapshot.error !== undefined ? { error: stageSnapshot.error } : {}),
2198
+ ...(stageSnapshot.failureKind !== undefined ? { failureKind: stageSnapshot.failureKind } : {}),
2199
+ ...(stageSnapshot.failureMessage !== undefined ? { failureMessage: stageSnapshot.failureMessage } : {}),
2200
+ ...(stageSnapshot.result !== undefined && stageSnapshot.status === "completed" ? { summary: stageSnapshot.result } : {}),
2201
+ ...stageReplayFields(stageSnapshot),
2202
+ ...(stageSnapshot.workflowChild !== undefined ? { workflowChild: stageSnapshot.workflowChild } : {}),
2203
+ });
2204
+ };
2205
+
2206
+ const finalize = (
2207
+ status: "completed" | "failed",
2208
+ summaryOrError: string,
2209
+ workflowChild?: WorkflowChildReplaySnapshot,
2210
+ failureError?: unknown,
2211
+ ): void => {
2212
+ if (finalized) return;
2213
+ finalized = true;
2214
+ stageSnapshot.status = status;
2215
+ if (status === "completed") {
2216
+ stageSnapshot.result = summaryOrError;
2217
+ if (workflowChild !== undefined) stageSnapshot.workflowChild = workflowChild;
2218
+ } else {
2219
+ const failure = classifyWorkflowFailure(failureError);
2220
+ stageSnapshot.error = failure.userMessage;
2221
+ stageSnapshot.failureKind = failure.kind;
2222
+ stageSnapshot.failureMessage = failure.message;
2223
+ }
2224
+ stageSnapshot.endedAt = Date.now();
2225
+ stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
2226
+ activeStore.recordStageEnd(runId, stageSnapshot);
2227
+ opts.onStageEnd?.(runId, stageSnapshot);
2228
+ appendStageEndForSnapshot();
2229
+ tracker.onSettle(stageId);
2230
+ };
2231
+
2232
+ activeStore.recordStageStart(runId, stageSnapshot);
2233
+ opts.onStageStart?.(runId, stageSnapshot);
2234
+ appendStageStartOnce();
2235
+
2236
+ const finalizeReplay = (): void => {
2237
+ if (replayedChild === undefined || finalized) return;
2238
+ finalized = true;
2239
+ activeStore.recordStageEnd(runId, stageSnapshot);
2240
+ opts.onStageEnd?.(runId, stageSnapshot);
2241
+ appendStageEndForSnapshot();
2242
+ tracker.onSettle(stageId);
2243
+ };
2244
+
2245
+ const linkChildRun = (ref: WorkflowChildRunRef): void => {
2246
+ if (finalized) return;
2247
+ stageSnapshot.workflowChildRun = { ...ref };
2248
+ activeStore.recordStageWorkflowChildRun(runId, stageId, ref);
2249
+ };
2250
+
2251
+ return {
2252
+ id: stageId,
2253
+ ...(replayedChild !== undefined ? { replayedChild } : {}),
2254
+ finalizeReplay,
2255
+ linkChildRun,
2256
+ complete(summary: string, workflowChild: WorkflowChildReplaySnapshot): void {
2257
+ finalize("completed", summary, workflowChild);
2258
+ },
2259
+ fail(error: unknown): void {
2260
+ finalize("failed", error instanceof Error ? error.message : String(error), undefined, error);
2261
+ },
2262
+ };
2263
+ };
2264
+
1865
2265
  const buildPromptNodeUiAdapter = (): WorkflowUIAdapter => {
1866
2266
  const ask = async (descriptor: PromptDescriptor): Promise<unknown> => {
1867
2267
  if (ownController.signal.aborted) {
@@ -2020,7 +2420,7 @@ export async function run<TInputs extends Record<string, unknown>>(
2020
2420
  },
2021
2421
  async select<T extends string>(message: string, options: readonly T[]): Promise<T> {
2022
2422
  if (options.length === 0) {
2023
- throw new Error("pi-workflows: ctx.ui.select requires at least one option");
2423
+ throw new Error("atomic-workflows: ctx.ui.select requires at least one option");
2024
2424
  }
2025
2425
  const response = await ask({ kind: "select", message, choices: options });
2026
2426
  if (typeof response === "string" && (options as readonly string[]).includes(response)) {
@@ -2139,7 +2539,7 @@ export async function run<TInputs extends Record<string, unknown>>(
2139
2539
  return replayResult;
2140
2540
  };
2141
2541
  const rejectReplayMutation = (action: string): never => {
2142
- throw new Error(`pi-workflows: replayed stage "${name}" cannot ${action}`);
2542
+ throw new Error(`atomic-workflows: replayed stage "${name}" cannot ${action}`);
2143
2543
  };
2144
2544
  const replayContext: InternalStageContext = {
2145
2545
  name,
@@ -2175,6 +2575,7 @@ export async function run<TInputs extends Record<string, unknown>>(
2175
2575
  __pendingMessageCount: () => 0,
2176
2576
  __modelFallbackMeta: () => ({
2177
2577
  ...(replaySource.model !== undefined ? { model: replaySource.model } : {}),
2578
+ ...(replaySource.fastMode === true ? { fastMode: replaySource.fastMode } : {}),
2178
2579
  ...(replaySource.attemptedModels !== undefined ? { attemptedModels: replaySource.attemptedModels } : {}),
2179
2580
  ...(replaySource.modelAttempts !== undefined ? { modelAttempts: replaySource.modelAttempts } : {}),
2180
2581
  }),
@@ -2188,6 +2589,16 @@ export async function run<TInputs extends Record<string, unknown>>(
2188
2589
  // d. Create inner AgentSession-like StageContext (raw, without lifecycle wrapping).
2189
2590
  // Must come before the registry registration because the handle
2190
2591
  // delegates to it for every operation.
2592
+ const applyModelFallbackMeta = (meta: ReturnType<InternalStageContext["__modelFallbackMeta"]>): void => {
2593
+ if (meta.model !== undefined) stageSnapshot.model = meta.model;
2594
+ if (meta.fastMode !== undefined) {
2595
+ if (meta.fastMode) stageSnapshot.fastMode = true;
2596
+ else delete stageSnapshot.fastMode;
2597
+ }
2598
+ if (meta.attemptedModels !== undefined) stageSnapshot.attemptedModels = meta.attemptedModels;
2599
+ if (meta.modelAttempts !== undefined) stageSnapshot.modelAttempts = meta.modelAttempts;
2600
+ };
2601
+
2191
2602
  const innerCtx: InternalStageContext = createStageContext({
2192
2603
  stageId,
2193
2604
  stageName: name,
@@ -2196,6 +2607,13 @@ export async function run<TInputs extends Record<string, unknown>>(
2196
2607
  signal: ownController.signal,
2197
2608
  stageOptions: options,
2198
2609
  models: opts.models,
2610
+ executionMode: opts.executionMode,
2611
+ onModelFallbackMetaChange(meta) {
2612
+ applyModelFallbackMeta(meta);
2613
+ if (stageSnapshot.status === "running") {
2614
+ activeStore.recordStageStart(runId, stageSnapshot);
2615
+ }
2616
+ },
2199
2617
  });
2200
2618
  const activeAskUserQuestionCalls = new Set<string>();
2201
2619
  let activeAskUserQuestionAnonymousCalls = 0;
@@ -2213,11 +2631,9 @@ export async function run<TInputs extends Record<string, unknown>>(
2213
2631
  askUserQuestionObservedThisTurn = true;
2214
2632
  if (toolEvent.callId !== undefined) activeAskUserQuestionCalls.add(toolEvent.callId);
2215
2633
  else activeAskUserQuestionAnonymousCalls += 1;
2216
- activeStore.recordStageAwaitingInput(runId, stageId, true);
2217
- // Expose a headless-answer adapter so the prompt can be answered
2218
- // programmatically (e.g. `workflow send`) without a TUI host. The
2219
- // (runId, stageId) key joins this to the broker request the tool's
2220
- // ctx.ui.custom() call raises.
2634
+ // Expose a headless-answer adapter before marking the stage awaiting
2635
+ // input so the main-chat steering notice can include the actual
2636
+ // structured question instead of a promptless placeholder.
2221
2637
  const adapter = buildStagePromptAdapter(
2222
2638
  toolEvent.callId ?? `ask-user-question-${stageId}`,
2223
2639
  "ask_user_question",
@@ -2225,6 +2641,7 @@ export async function run<TInputs extends Record<string, unknown>>(
2225
2641
  Date.now(),
2226
2642
  );
2227
2643
  if (adapter) stageUiBroker.provideStagePrompt(runId, stageId, adapter);
2644
+ activeStore.recordStageAwaitingInput(runId, stageId, true);
2228
2645
  return;
2229
2646
  }
2230
2647
 
@@ -2352,12 +2769,14 @@ export async function run<TInputs extends Record<string, unknown>>(
2352
2769
  stageSnapshot.endedAt = Date.now();
2353
2770
  stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
2354
2771
 
2355
- const finalModelMeta = innerCtx.__modelFallbackMeta();
2356
- if (finalModelMeta.model !== undefined) stageSnapshot.model = finalModelMeta.model;
2357
- if (finalModelMeta.attemptedModels !== undefined) stageSnapshot.attemptedModels = finalModelMeta.attemptedModels;
2358
- if (finalModelMeta.modelAttempts !== undefined) stageSnapshot.modelAttempts = finalModelMeta.modelAttempts;
2772
+ applyModelFallbackMeta(innerCtx.__modelFallbackMeta());
2359
2773
 
2360
2774
  activeStore.recordStageEnd(runId, stageSnapshot);
2775
+ stageUiBroker.cancelStagePrompt(
2776
+ runId,
2777
+ stageId,
2778
+ new Error(`atomic-workflows: stage ${stageId} completed with pending custom UI`),
2779
+ );
2361
2780
  opts.onStageEnd?.(runId, stageSnapshot);
2362
2781
 
2363
2782
  if (opts.persistence) {
@@ -2387,7 +2806,7 @@ export async function run<TInputs extends Record<string, unknown>>(
2387
2806
  stageSnapshot.skippedReason = "fail-fast";
2388
2807
  };
2389
2808
  const parallelFailFastError = (): unknown =>
2390
- stageFailFastScope?.firstFailure ?? new Error("pi-workflows: skipped after parallel fail-fast");
2809
+ stageFailFastScope?.firstFailure ?? new Error("atomic-workflows: skipped after parallel fail-fast");
2391
2810
  const skipForParallelFailFast = (): void => {
2392
2811
  if (isTerminalStage(stageSnapshot)) return;
2393
2812
  markSkippedForParallelFailFast();
@@ -2453,7 +2872,7 @@ export async function run<TInputs extends Record<string, unknown>>(
2453
2872
  }
2454
2873
  };
2455
2874
 
2456
- const runTrackedStageCall = async (call: () => Promise<string>): Promise<string> => {
2875
+ const runTrackedStageCall = async (call: () => Promise<string>, eagerSession = false): Promise<string> => {
2457
2876
  await waitForStageRelease();
2458
2877
  if (stageFinalized) {
2459
2878
  throw parallelFailFastError();
@@ -2481,6 +2900,33 @@ export async function run<TInputs extends Record<string, unknown>>(
2481
2900
  }
2482
2901
  stageSnapshot.status = "running";
2483
2902
  stageSnapshot.startedAt = Date.now();
2903
+ const hasExplicitFastModeCandidate = async (): Promise<boolean> => {
2904
+ const rawCandidate = isCodexFastModeCandidateModelId(workflowModelId(options?.model))
2905
+ || (Array.isArray(options?.fallbackModels) && options.fallbackModels.some((candidate) => isCodexFastModeCandidateModelId(workflowModelId(candidate))));
2906
+ if (rawCandidate) return true;
2907
+ try {
2908
+ const candidates = await buildModelCandidatesFromCatalog({
2909
+ primaryModel: options?.model,
2910
+ fallbackModels: options?.fallbackModels,
2911
+ catalog: opts.models,
2912
+ });
2913
+ return candidates.some((candidate) => isCodexFastModeCandidateModelId(candidate.id));
2914
+ } catch {
2915
+ return false;
2916
+ }
2917
+ };
2918
+ const hasNoExplicitModelConfig = options?.model === undefined && options?.fallbackModels === undefined;
2919
+ const promptAdapterHandlesInitialPrompt = adapters.prompt !== undefined;
2920
+ if (eagerSession && !promptAdapterHandlesInitialPrompt && (hasNoExplicitModelConfig || await hasExplicitFastModeCandidate())) {
2921
+ try {
2922
+ await innerCtx.__ensureSession();
2923
+ } catch (err) {
2924
+ if (!(err instanceof Error && err.message.includes("prompt adapter not configured"))) {
2925
+ throw err;
2926
+ }
2927
+ }
2928
+ }
2929
+ applyModelFallbackMeta(innerCtx.__modelFallbackMeta());
2484
2930
  activeStore.recordStageStart(runId, stageSnapshot);
2485
2931
 
2486
2932
  // Persistence: append stage.start entry
@@ -2556,10 +3002,7 @@ export async function run<TInputs extends Record<string, unknown>>(
2556
3002
  if (meta.sessionId !== undefined || meta.sessionFile !== undefined) {
2557
3003
  activeStore.recordStageSession(runId, stageId, meta);
2558
3004
  }
2559
- const modelMeta = innerCtx.__modelFallbackMeta();
2560
- if (modelMeta.model !== undefined) stageSnapshot.model = modelMeta.model;
2561
- if (modelMeta.attemptedModels !== undefined) stageSnapshot.attemptedModels = modelMeta.attemptedModels;
2562
- if (modelMeta.modelAttempts !== undefined) stageSnapshot.modelAttempts = modelMeta.modelAttempts;
3005
+ applyModelFallbackMeta(innerCtx.__modelFallbackMeta());
2563
3006
  }
2564
3007
  if (stageFailFastScope?.failed === true && stageFailFastScope.activeStages.has(stageId)) {
2565
3008
  markSkippedForParallelFailFast();
@@ -2626,7 +3069,7 @@ export async function run<TInputs extends Record<string, unknown>>(
2626
3069
 
2627
3070
  const stageContext: StageContext & Pick<InternalStageContext, "__modelFallbackMeta"> = {
2628
3071
  name: innerCtx.name,
2629
- prompt: (text, promptOptions) => runTrackedStageCall(() => innerCtx.prompt(text, promptOptions)),
3072
+ prompt: (text, promptOptions) => runTrackedStageCall(() => innerCtx.prompt(text, promptOptions), true),
2630
3073
  complete: (text, completeOptions) => runTrackedStageCall(() => innerCtx.complete(text, completeOptions)),
2631
3074
  steer: (text) => innerCtx.steer(text),
2632
3075
  followUp: (text) => innerCtx.followUp(text),
@@ -2706,6 +3149,7 @@ export async function run<TInputs extends Record<string, unknown>>(
2706
3149
  ...(sessionId !== undefined ? { sessionId } : {}),
2707
3150
  ...(stage.sessionFile !== undefined ? { sessionFile: stage.sessionFile } : {}),
2708
3151
  ...(stageMeta.model !== undefined ? { model: stageMeta.model } : {}),
3152
+ ...(stageMeta.fastMode === true ? { fastMode: stageMeta.fastMode } : {}),
2709
3153
  ...(stageMeta.attemptedModels !== undefined ? { attemptedModels: stageMeta.attemptedModels } : {}),
2710
3154
  ...(stageMeta.modelAttempts !== undefined ? { modelAttempts: stageMeta.modelAttempts } : {}),
2711
3155
  ...(stageMeta.warnings !== undefined ? { warnings: stageMeta.warnings } : {}),
@@ -2768,6 +3212,131 @@ export async function run<TInputs extends Record<string, unknown>>(
2768
3212
  }
2769
3213
  });
2770
3214
  },
3215
+
3216
+ async workflow<TChildInputs extends WorkflowInputValues, TChildOutputs extends WorkflowOutputValues>(
3217
+ child: WorkflowDefinition<TChildInputs, TChildOutputs>,
3218
+ options: WorkflowRunChildOptions<TChildInputs> = {},
3219
+ ): Promise<WorkflowChildResult<TChildOutputs>> {
3220
+ // The executor operates on type-erased definitions at runtime; the child's
3221
+ // declared output contract is validated dynamically by the child run and
3222
+ // selectWorkflowOutputs, so the typed result is reconstructed via casts.
3223
+ if (!isWorkflowDefinition(child)) {
3224
+ throw new Error("atomic-workflows: ctx.workflow(definition) requires a compiled workflow definition");
3225
+ }
3226
+ const childName = child.normalizedName;
3227
+ const boundaryName = options.stageName ?? `workflow:${childName}`;
3228
+ const boundaryReplayKey = nextWorkflowBoundaryReplayKey(boundaryName);
3229
+ const boundary = startWorkflowBoundaryStage(boundaryName, boundaryReplayKey);
3230
+ if (boundary.replayedChild !== undefined) {
3231
+ // Continuation replay returns the persisted child boundary exactly as
3232
+ // written; input validation and output remapping are intentionally not
3233
+ // re-run against edited workflow code for a completed child boundary.
3234
+ // Defer settling by one microtask so concurrent replayed boundaries
3235
+ // spawned in the same turn see the same frontier as the source run.
3236
+ await Promise.resolve();
3237
+ boundary.finalizeReplay();
3238
+ return boundary.replayedChild as WorkflowChildResult<TChildOutputs>;
3239
+ }
3240
+
3241
+ // Tracked so the finally can detach the parent-abort listener and release
3242
+ // the pre-registered child controller on every exit path — including the
3243
+ // maxDepth early return inside run(), which returns before run()'s own
3244
+ // cleanup. Without this, sequential ctx.workflow(...) calls accumulate one
3245
+ // parent-signal listener (and a leaked registry entry) per child.
3246
+ let childRunId: string | undefined;
3247
+ let detachParentAbort: (() => void) | undefined;
3248
+ try {
3249
+ const childInputs = resolveAndValidateInputs(
3250
+ child.inputs,
3251
+ options.inputs ?? {},
3252
+ `child workflow "${childName}" (${child.name})`,
3253
+ );
3254
+
3255
+ childRunId = crypto.randomUUID();
3256
+ boundary.linkChildRun({
3257
+ alias: childName,
3258
+ workflow: child.normalizedName,
3259
+ runId: childRunId,
3260
+ });
3261
+
3262
+ const childController = new AbortController();
3263
+ if (ownController.signal.aborted) {
3264
+ childController.abort(ownController.signal.reason);
3265
+ } else {
3266
+ const onParentAbort = () => childController.abort(ownController.signal.reason);
3267
+ ownController.signal.addEventListener("abort", onParentAbort, { once: true });
3268
+ detachParentAbort = () =>
3269
+ ownController.signal.removeEventListener("abort", onParentAbort);
3270
+ }
3271
+ // Pre-register the child controller under its own runId *before* run()
3272
+ // so a kill targeting the child runId works even before the nested run
3273
+ // would register itself. The nested run() sees opts.signal set and skips
3274
+ // its own cancellation.register (avoiding a double-register on the same
3275
+ // key) while still running its finally{} unregister(runId) cleanup, so
3276
+ // both branches must agree on this key.
3277
+ opts.cancellation?.register(childRunId, childController);
3278
+
3279
+ const {
3280
+ runId: _parentRunId,
3281
+ continuation: _parentContinuation,
3282
+ deferWorkflowStart: _parentDeferWorkflowStart,
3283
+ parentRun: _parentRun,
3284
+ onRunStart: _parentOnRunStart,
3285
+ onRunEnd: _parentOnRunEnd,
3286
+ ...childBaseOpts
3287
+ } = opts;
3288
+ const childRun = await run(child, childInputs, {
3289
+ ...childBaseOpts,
3290
+ runId: childRunId,
3291
+ cwd: resolveWorkflowCwd(),
3292
+ depth: depth + 1,
3293
+ ...(opts.registry !== undefined ? { registry: opts.registry } : {}),
3294
+ parentRun: {
3295
+ runId,
3296
+ stageId: boundary.id,
3297
+ rootRunId: opts.parentRun?.rootRunId ?? runId,
3298
+ },
3299
+ signal: childController.signal,
3300
+ deferWorkflowStart: false,
3301
+ });
3302
+
3303
+ if (childRun.status !== "completed") {
3304
+ const failedChildStage = childRun.stages.find((stage) => stage.failureKind !== undefined);
3305
+ throw new Error(
3306
+ `atomic-workflows: child workflow "${childName}" (${child.name}) failed with status ${childRun.status}${childRun.error !== undefined ? `: ${childRun.error}` : ""}`,
3307
+ {
3308
+ cause: {
3309
+ ...(failedChildStage?.failureKind !== undefined ? { code: failedChildStage.failureKind } : {}),
3310
+ ...(failedChildStage?.failureMessage !== undefined ? { message: failedChildStage.failureMessage } : {}),
3311
+ },
3312
+ },
3313
+ );
3314
+ }
3315
+
3316
+ const outputs = selectWorkflowOutputs(child, childRun.result);
3317
+ const childResult: WorkflowChildResult<TChildOutputs> = {
3318
+ workflow: child.normalizedName,
3319
+ runId: childRun.runId,
3320
+ status: "completed",
3321
+ outputs: outputs as TChildOutputs,
3322
+ };
3323
+ const workflowChild = workflowChildReplaySnapshot(childName, childResult);
3324
+ const outputKeys = Object.keys(outputs);
3325
+ boundary.complete(
3326
+ `Workflow "${child.name}" completed (runId: ${childRun.runId}; outputs: ${outputKeys.length > 0 ? outputKeys.join(", ") : "(none)"})`,
3327
+ workflowChild,
3328
+ );
3329
+ return childResult;
3330
+ } catch (err) {
3331
+ boundary.fail(err);
3332
+ throw err;
3333
+ } finally {
3334
+ detachParentAbort?.();
3335
+ // Idempotent with run()'s own finally on the normal path; required on
3336
+ // the maxDepth early-return path where run() never reaches its cleanup.
3337
+ if (childRunId !== undefined) opts.cancellation?.unregister(childRunId);
3338
+ }
3339
+ },
2771
3340
  };
2772
3341
 
2773
3342
  // 6. Call def.run(ctx)
@@ -2779,7 +3348,7 @@ export async function run<TInputs extends Record<string, unknown>>(
2779
3348
  }
2780
3349
  }
2781
3350
 
2782
- const result = await def.run(ctx);
3351
+ const rawResult = await def.run(ctx);
2783
3352
 
2784
3353
  // Post-body abort check: if signal was aborted at any point before we record
2785
3354
  // completion, the run must be finalized as "killed", never "completed".
@@ -2787,6 +3356,9 @@ export async function run<TInputs extends Record<string, unknown>>(
2787
3356
  return finalizeKilled(runId, runSnapshot, activeStore, opts.persistence, opts.onRunEnd);
2788
3357
  }
2789
3358
 
3359
+ const result = normalizeWorkflowRunOutput(def.name, rawResult);
3360
+ assertWorkflowRunOutputs(def.name, result, def.outputs);
3361
+
2790
3362
  assertWorkflowCreatedStage(runSnapshot);
2791
3363
 
2792
3364
  const recorded = activeStore.recordRunEnd(runId, "completed", result);