@usearazzo/runner 0.0.0-stage → 1.0.1-alpha.7

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 (151) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +20 -0
  3. package/README.md +784 -2
  4. package/dist/arazzo-runner.browser.js +111247 -0
  5. package/dist/arazzo-runner.browser.min.js +1 -0
  6. package/package.json +88 -4
  7. package/src/action/ActionResolver.cjs +160 -0
  8. package/src/action/ActionResolver.mjs +150 -0
  9. package/src/assembler/OpenAPI2DocumentAssembler.cjs +111 -0
  10. package/src/assembler/OpenAPI2DocumentAssembler.mjs +104 -0
  11. package/src/assembler/OpenAPI30DocumentAssembler.cjs +110 -0
  12. package/src/assembler/OpenAPI30DocumentAssembler.mjs +103 -0
  13. package/src/assembler/OpenAPI31DocumentAssembler.cjs +111 -0
  14. package/src/assembler/OpenAPI31DocumentAssembler.mjs +104 -0
  15. package/src/assembler/OpenAPIDocumentAssembler.cjs +58 -0
  16. package/src/assembler/OpenAPIDocumentAssembler.mjs +50 -0
  17. package/src/client/HTTPClient.cjs +5 -0
  18. package/src/client/HTTPClient.mjs +1 -0
  19. package/src/client/HTTPClientFetch.cjs +16 -0
  20. package/src/client/HTTPClientFetch.mjs +10 -0
  21. package/src/client/OpenAPIOperationRequest.cjs +5 -0
  22. package/src/client/OpenAPIOperationRequest.mjs +1 -0
  23. package/src/client/OpenAPIOperationResponse.cjs +42 -0
  24. package/src/client/OpenAPIOperationResponse.mjs +36 -0
  25. package/src/client/OpenAPIOperationResponseNormalizer.cjs +48 -0
  26. package/src/client/OpenAPIOperationResponseNormalizer.mjs +40 -0
  27. package/src/client/ParameterDelivery.cjs +119 -0
  28. package/src/client/ParameterDelivery.mjs +113 -0
  29. package/src/constants.cjs +9 -0
  30. package/src/constants.mjs +3 -0
  31. package/src/criterion/CriterionEvaluator.cjs +205 -0
  32. package/src/criterion/CriterionEvaluator.mjs +196 -0
  33. package/src/criterion/JSONPathCriterionEvaluator.cjs +47 -0
  34. package/src/criterion/JSONPathCriterionEvaluator.mjs +40 -0
  35. package/src/criterion/RegexCriterionEvaluator.cjs +67 -0
  36. package/src/criterion/RegexCriterionEvaluator.mjs +60 -0
  37. package/src/criterion/SimpleCriterionEvaluator.cjs +50 -0
  38. package/src/criterion/SimpleCriterionEvaluator.mjs +43 -0
  39. package/src/criterion/XPathCriterionEvaluator.cjs +117 -0
  40. package/src/criterion/XPathCriterionEvaluator.mjs +109 -0
  41. package/src/document/APIDocument.cjs +35 -0
  42. package/src/document/APIDocument.mjs +30 -0
  43. package/src/document/ArazzoDocument.cjs +81 -0
  44. package/src/document/ArazzoDocument.mjs +74 -0
  45. package/src/document/ArazzoSourceDescriptionIndex.cjs +26 -0
  46. package/src/document/ArazzoSourceDescriptionIndex.mjs +20 -0
  47. package/src/document/ArazzoStepIndex.cjs +25 -0
  48. package/src/document/ArazzoStepIndex.mjs +19 -0
  49. package/src/document/ArazzoWorkflowIndex.cjs +17 -0
  50. package/src/document/ArazzoWorkflowIndex.mjs +11 -0
  51. package/src/document/OpenAPIDocument.cjs +28 -0
  52. package/src/document/OpenAPIDocument.mjs +21 -0
  53. package/src/document/OpenAPIOperationIndex.cjs +24 -0
  54. package/src/document/OpenAPIOperationIndex.mjs +18 -0
  55. package/src/document/ParameterIdentity.cjs +93 -0
  56. package/src/document/ParameterIdentity.mjs +88 -0
  57. package/src/document/openapi-types.cjs +5 -0
  58. package/src/document/openapi-types.mjs +1 -0
  59. package/src/errors/ArazzoRunnerError.cjs +10 -0
  60. package/src/errors/ArazzoRunnerError.mjs +5 -0
  61. package/src/errors/AssemblerError.cjs +11 -0
  62. package/src/errors/AssemblerError.mjs +4 -0
  63. package/src/errors/ClientError.cjs +11 -0
  64. package/src/errors/ClientError.mjs +4 -0
  65. package/src/errors/CriterionError.cjs +11 -0
  66. package/src/errors/CriterionError.mjs +4 -0
  67. package/src/errors/ExecutionError.cjs +11 -0
  68. package/src/errors/ExecutionError.mjs +4 -0
  69. package/src/errors/ExtractionError.cjs +11 -0
  70. package/src/errors/ExtractionError.mjs +4 -0
  71. package/src/errors/InvalidEntryDocumentError.cjs +11 -0
  72. package/src/errors/InvalidEntryDocumentError.mjs +4 -0
  73. package/src/errors/NormalizationError.cjs +11 -0
  74. package/src/errors/NormalizationError.mjs +4 -0
  75. package/src/errors/ResolverError.cjs +11 -0
  76. package/src/errors/ResolverError.mjs +4 -0
  77. package/src/errors/RuntimeExpressionError.cjs +11 -0
  78. package/src/errors/RuntimeExpressionError.mjs +4 -0
  79. package/src/errors/UnmatchedProviderError.cjs +11 -0
  80. package/src/errors/UnmatchedProviderError.mjs +4 -0
  81. package/src/executor/ArazzoWorkflowLocatorNormalizer.cjs +147 -0
  82. package/src/executor/ArazzoWorkflowLocatorNormalizer.mjs +137 -0
  83. package/src/executor/OpenAPIOperationExecutor.cjs +457 -0
  84. package/src/executor/OpenAPIOperationExecutor.mjs +446 -0
  85. package/src/executor/OpenAPIOperationLocatorNormalizer.cjs +197 -0
  86. package/src/executor/OpenAPIOperationLocatorNormalizer.mjs +189 -0
  87. package/src/executor/SourceDescriptionsLocatorNormalizer.cjs +44 -0
  88. package/src/executor/SourceDescriptionsLocatorNormalizer.mjs +38 -0
  89. package/src/executor/StepExecutor.cjs +333 -0
  90. package/src/executor/StepExecutor.mjs +326 -0
  91. package/src/executor/StepRetryRunner.cjs +226 -0
  92. package/src/executor/StepRetryRunner.mjs +215 -0
  93. package/src/executor/StepTransitionInterpreter.cjs +159 -0
  94. package/src/executor/StepTransitionInterpreter.mjs +151 -0
  95. package/src/executor/WorkflowCallStack.cjs +116 -0
  96. package/src/executor/WorkflowCallStack.mjs +106 -0
  97. package/src/executor/WorkflowExecutor.cjs +1091 -0
  98. package/src/executor/WorkflowExecutor.mjs +1077 -0
  99. package/src/executor/abort.cjs +70 -0
  100. package/src/executor/abort.mjs +60 -0
  101. package/src/expression/RuntimeExpressionContext.cjs +5 -0
  102. package/src/expression/RuntimeExpressionContext.mjs +1 -0
  103. package/src/expression/RuntimeExpressionEvaluator.cjs +337 -0
  104. package/src/expression/RuntimeExpressionEvaluator.mjs +328 -0
  105. package/src/extractor/ArazzoStepExtractor.cjs +48 -0
  106. package/src/extractor/ArazzoStepExtractor.mjs +41 -0
  107. package/src/extractor/ArazzoWorkflowExtractor.cjs +50 -0
  108. package/src/extractor/ArazzoWorkflowExtractor.mjs +43 -0
  109. package/src/extractor/OpenAPIOperationExtractor.cjs +84 -0
  110. package/src/extractor/OpenAPIOperationExtractor.mjs +77 -0
  111. package/src/index.cjs +419 -0
  112. package/src/index.mjs +73 -0
  113. package/src/normalizer/ArazzoStepNormalizer.cjs +23 -0
  114. package/src/normalizer/ArazzoStepNormalizer.mjs +17 -0
  115. package/src/normalizer/ArazzoWorkflowNormalizer.cjs +214 -0
  116. package/src/normalizer/ArazzoWorkflowNormalizer.mjs +206 -0
  117. package/src/normalizer/OpenAPI2OperationNormalizer.cjs +128 -0
  118. package/src/normalizer/OpenAPI2OperationNormalizer.mjs +120 -0
  119. package/src/normalizer/OpenAPI30OperationNormalizer.cjs +129 -0
  120. package/src/normalizer/OpenAPI30OperationNormalizer.mjs +121 -0
  121. package/src/normalizer/OpenAPI31OperationNormalizer.cjs +131 -0
  122. package/src/normalizer/OpenAPI31OperationNormalizer.mjs +123 -0
  123. package/src/normalizer/OpenAPIOperationNormalizer.cjs +58 -0
  124. package/src/normalizer/OpenAPIOperationNormalizer.mjs +50 -0
  125. package/src/registry/DocumentRegistry.cjs +160 -0
  126. package/src/registry/DocumentRegistry.mjs +151 -0
  127. package/src/registry/providers/ArazzoDocumentRegistryProvider.cjs +154 -0
  128. package/src/registry/providers/ArazzoDocumentRegistryProvider.mjs +146 -0
  129. package/src/registry/providers/DocumentRegistryProvider.cjs +31 -0
  130. package/src/registry/providers/DocumentRegistryProvider.mjs +25 -0
  131. package/src/registry/providers/OpenAPIDocumentRegistryProvider.cjs +122 -0
  132. package/src/registry/providers/OpenAPIDocumentRegistryProvider.mjs +114 -0
  133. package/src/resolver/ArazzoValueResolver.cjs +42 -0
  134. package/src/resolver/ArazzoValueResolver.mjs +37 -0
  135. package/src/resolver/OpenAPIOperationParameterResolver.cjs +117 -0
  136. package/src/resolver/OpenAPIOperationParameterResolver.mjs +110 -0
  137. package/src/resolver/OutputResolver.cjs +71 -0
  138. package/src/resolver/OutputResolver.mjs +64 -0
  139. package/src/resolver/RequestBodyResolver.cjs +227 -0
  140. package/src/resolver/RequestBodyResolver.mjs +220 -0
  141. package/src/resolver/ResolverScope.cjs +5 -0
  142. package/src/resolver/ResolverScope.mjs +1 -0
  143. package/src/resolver/StepParameterResolver.cjs +22 -0
  144. package/src/resolver/StepParameterResolver.mjs +15 -0
  145. package/src/resolver/WorkflowParameterResolver.cjs +73 -0
  146. package/src/resolver/WorkflowParameterResolver.mjs +66 -0
  147. package/src/state/WorkflowExecutionState.cjs +99 -0
  148. package/src/state/WorkflowExecutionState.mjs +93 -0
  149. package/src/vendor/swagger-client.cjs +1 -0
  150. package/src/vendor/swagger-client.mjs +1 -0
  151. package/types/runner.d.ts +2341 -0
@@ -0,0 +1,1077 @@
1
+ import { toValue } from '@speclynx/apidom-core';
2
+ import { isArrayElement, isObjectElement, isStringElement } from '@speclynx/apidom-datamodel';
3
+ import { isStepElement } from '@speclynx/apidom-ns-arazzo-1';
4
+ import RuntimeExpressionEvaluator from "../expression/RuntimeExpressionEvaluator.mjs";
5
+ import ArazzoWorkflowExtractor from "../extractor/ArazzoWorkflowExtractor.mjs";
6
+ import ArazzoWorkflowNormalizer from "../normalizer/ArazzoWorkflowNormalizer.mjs";
7
+ import OutputResolver from "../resolver/OutputResolver.mjs";
8
+ import WorkflowParameterResolver from "../resolver/WorkflowParameterResolver.mjs";
9
+ import WorkflowExecutionState from "../state/WorkflowExecutionState.mjs";
10
+ import { STEP_TARGET_FIELDS } from "./StepExecutor.mjs";
11
+ import ArazzoWorkflowLocatorNormalizer from "./ArazzoWorkflowLocatorNormalizer.mjs";
12
+ import StepRetryRunner from "./StepRetryRunner.mjs";
13
+ import WorkflowCallStack from "./WorkflowCallStack.mjs";
14
+ import StepTransitionInterpreter, { actionTargets } from "./StepTransitionInterpreter.mjs";
15
+ import ExecutionError from "../errors/ExecutionError.mjs";
16
+ import ResolverError from "../errors/ResolverError.mjs";
17
+ import { readAbortSignal, throwIfAborted } from "./abort.mjs";
18
+ /**
19
+ * Options for the WorkflowExecutor.
20
+ * @public
21
+ */
22
+ /**
23
+ * Per-call options for {@link WorkflowExecutor.execute}.
24
+ * @public
25
+ */
26
+ /**
27
+ * One run of a `retry` action's `stepId` / `workflowId` reference — "the
28
+ * reference is executed and the context is returned, after which the current
29
+ * step is retried". Fires once per firing of its action (i.e. once before every
30
+ * attempt the action grants, not once for the whole retry chain).
31
+ * @public
32
+ */
33
+ /**
34
+ * The trace of a single step execution within a workflow run.
35
+ * @public
36
+ */
37
+ /**
38
+ * The outcome of executing a workflow.
39
+ *
40
+ * `status` is `completed` when the steps ran to the end of the list, `ended`
41
+ * when an `end` action stopped the run early, `failed` when a step failed and no
42
+ * matching `onFailure` action redirected it (the break-and-return default) — or
43
+ * when a `dependsOn` workflow did not complete — and `transferred` when a `goto`
44
+ * action handed control one-way to another workflow: this run's own steps stop
45
+ * at that point, and its `outputs` declaration is never evaluated, since the run
46
+ * never reaches its own end. See {@link WorkflowExecutionResult.settledStatus}
47
+ * for the success/failure verdict a `'transferred'` result defers to.
48
+ * @public
49
+ */
50
+ /**
51
+ * The result a transfer chain settled on: `result` itself when its `status`
52
+ * isn't `'transferred'`, otherwise the terminal, non-`'transferred'` result at
53
+ * the end of its `transferredTo` chain.
54
+ *
55
+ * For the success/failure verdict alone, prefer
56
+ * {@link WorkflowExecutionResult.settledStatus} — every result already
57
+ * carries it, computed eagerly, so no call is needed. Reach for this function
58
+ * when the terminal result *itself* is wanted (its `outputs`, `steps`,
59
+ * `workflowId`), not just where it landed. Iterative, not recursive: a
60
+ * chain's length is bounded only by `maxWorkflowDepth`, and this must not add
61
+ * a matching bound on call-stack depth.
62
+ * @public
63
+ */
64
+ export function settledResult(result) {
65
+ let current = result;
66
+ while (current.transferredTo !== undefined) {
67
+ current = current.transferredTo;
68
+ }
69
+ return current;
70
+ }
71
+
72
+ /**
73
+ * How a `#run` invocation ended, handed to `#result` to assemble the final
74
+ * {@link WorkflowExecutionResult} from.
75
+ *
76
+ * A discriminated union rather than a `status` string plus a separately-set
77
+ * `transferredTo` — the shape this replaced, where nothing but convention
78
+ * kept the two in sync. Here, `status: 'transferred'` with no
79
+ * `transferredTo` (or the reverse) is not a state a caller of `#result` can
80
+ * construct, not merely one they are expected not to.
81
+ * @internal
82
+ */
83
+
84
+ /**
85
+ * One completed `dependsOn` run, remembered with the inputs it was actually
86
+ * given — see {@link RunScope.dependencyRuns}.
87
+ */
88
+
89
+ /**
90
+ * The state shared by every workflow invocation of a single
91
+ * {@link WorkflowExecutor.execute} call — the caller's per-run options plus the
92
+ * two mutable ledgers that must span the whole call tree.
93
+ */
94
+
95
+ /**
96
+ * One workflow invocation in progress: the values fixed for a single `#run` that
97
+ * travel together into everything it delegates to.
98
+ */
99
+
100
+ /**
101
+ * Executes an Arazzo workflow: the stateful loop that turns "run one step" into
102
+ * "run a workflow".
103
+ *
104
+ * It iterates a workflow's steps in list order, delegating each to
105
+ * {@link StepExecutor}, records the resolved step outputs into a
106
+ * {@link WorkflowExecutionState} so later steps read `$steps.{id}.outputs.{name}`,
107
+ * and interprets the {@link SelectedAction} the step executor selects but does
108
+ * not act on — advancing to the next step, jumping via `goto`, transferring
109
+ * one-way to another workflow via a `goto` naming a `workflowId`, or stopping on
110
+ * `end` / the failure break-default. After the loop it resolves the workflow's
111
+ * `outputs` against the final state — unless the run transferred, in which case
112
+ * it never reaches its own end and `outputs` stays `{}`.
113
+ *
114
+ * A step targeting a `workflowId` is a sub-workflow call the executor runs
115
+ * itself, recursively; the workflows named by `dependsOn` are run to completion
116
+ * before the workflow's own steps. Both recurse through one call tree, guarded
117
+ * by a shared step budget, a nesting-depth ceiling, and cycle detection. A
118
+ * `goto`'s transfer joins the same call tree, entered via `'goto'`, but is not
119
+ * charged against the step budget: it fires once per step evaluation, unlike a
120
+ * retry reference, which can fire repeatedly from the same frame.
121
+ *
122
+ * State is created fresh per {@link WorkflowExecutor.execute} call — and per
123
+ * workflow invocation within it — and owned here; the returned result is
124
+ * read-only. Authoring errors (missing workflow, unknown `goto` target, a cycle,
125
+ * budget overflow) throw {@link ExecutionError}, as does a run the caller
126
+ * cancels through {@link WorkflowExecuteOptions.signal}; a step that
127
+ * legitimately fails and breaks, or a dependency that fails, is a normal
128
+ * `status: 'failed'` result, not a throw — the same split {@link StepExecutor}
129
+ * draws.
130
+ *
131
+ * A workflow reference — a sub-workflow step's `workflowId`, a `dependsOn`
132
+ * entry, a `retry` action's reference, a `goto`'s transfer — may also name a
133
+ * workflow of another Arazzo document, as
134
+ * `$sourceDescriptions.{name}.{workflowId}`. The run then follows that
135
+ * document for the foreign workflow: its id lookups, its steps' operations,
136
+ * and its expressions all resolve against the document that owns it, while
137
+ * the `$workflows.{workflowId}` run state stays keyed by the bare id — the
138
+ * only form the runtime-expression grammar can express.
139
+ * @public
140
+ */
141
+ class WorkflowExecutor {
142
+ static #DEFAULT_MAX_STEPS = 1000;
143
+ static #DEFAULT_MAX_WORKFLOW_DEPTH = 32;
144
+ static #DEFAULT_NOW = () => performance.now();
145
+ static #EXECUTE_OPTION_KEYS = new Set(['inputs', 'executeOptions', 'dependencyInputs', 'runDependencies', 'signal']);
146
+ #document;
147
+ #registry;
148
+ #maxSteps;
149
+ #maxWorkflowDepth;
150
+ #now;
151
+ #extractor = new ArazzoWorkflowExtractor();
152
+ #normalizer = new ArazzoWorkflowNormalizer();
153
+ #outputResolver = new OutputResolver();
154
+ #parameterResolver = new WorkflowParameterResolver();
155
+ #stepExecutor;
156
+ #retryRunner;
157
+ #interpreter = new StepTransitionInterpreter();
158
+ #workflowLocatorNormalizer;
159
+ constructor(options) {
160
+ this.#document = options.document;
161
+ this.#registry = options.registry;
162
+ this.#workflowLocatorNormalizer = new ArazzoWorkflowLocatorNormalizer(options.registry);
163
+ this.#maxSteps = options.maxSteps ?? WorkflowExecutor.#DEFAULT_MAX_STEPS;
164
+ this.#maxWorkflowDepth = options.maxWorkflowDepth ?? WorkflowExecutor.#DEFAULT_MAX_WORKFLOW_DEPTH;
165
+ this.#now = options.now ?? WorkflowExecutor.#DEFAULT_NOW;
166
+ this.#stepExecutor = options.stepExecutor;
167
+ this.#retryRunner = new StepRetryRunner({
168
+ sleep: options.sleep
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Runs the named workflow to completion, returning its outcome.
174
+ *
175
+ * The workflows the target names in `dependsOn` are run first (see
176
+ * {@link WorkflowExecuteOptions.runDependencies}), then its own steps.
177
+ *
178
+ * All run-scoped state — the execution state, the step trace, the control-flow
179
+ * position, the call stack, the step budget, and the completed-dependency
180
+ * ledger — is local to this call, so concurrent `execute` calls on one executor
181
+ * do not interfere.
182
+ */
183
+ async execute(workflowId, options = {}) {
184
+ this.#rejectUnknownOptions(options);
185
+
186
+ // a signal in the opaque bag was the only channel before this option existed,
187
+ // and it still reaches the transport. Absorbing it here is what makes it a
188
+ // whole cancellation rather than half of one: read only at dispatch, it would
189
+ // abort requests while the loop walked on through the remaining steps.
190
+ const signal = options.signal ?? readAbortSignal(options.executeOptions ?? {});
191
+ const scope = {
192
+ // the first-class signal is also spread into the opaque bag, because that
193
+ // bag is how anything reaches the transport: the executor's own checks
194
+ // stop the run at the next boundary, and this is what cancels the request
195
+ // already in flight. Spread last, so it wins over one passed in the bag.
196
+ executeOptions: {
197
+ ...options.executeOptions,
198
+ ...(signal === undefined ? {} : {
199
+ signal
200
+ })
201
+ },
202
+ dependencyInputs: options.dependencyInputs ?? {},
203
+ runDependencies: options.runDependencies ?? true,
204
+ signal,
205
+ budget: {
206
+ spent: 0
207
+ },
208
+ dependencyRuns: new Map(),
209
+ workflows: new Map()
210
+ };
211
+ return this.#run({
212
+ document: this.#document,
213
+ workflowId
214
+ }, options.inputs ?? {}, scope, new WorkflowCallStack({
215
+ maxDepth: this.#maxWorkflowDepth,
216
+ entryDocumentURI: this.#document.uri
217
+ }), 'root');
218
+ }
219
+
220
+ /**
221
+ * Rejects an options bag carrying keys this method does not recognize.
222
+ *
223
+ * `execute` used to take `inputs` as its second positional argument, and that
224
+ * bag is now the options object — so the old call reaches here as an object of
225
+ * unrecognized keys. Left unchecked it would run with no inputs at all, and
226
+ * every `$inputs.*` would resolve to `undefined` under lenient evaluation:
227
+ * a completed run issuing wrong requests. A typo in an option name fails the
228
+ * same silent way. Both are worth a loud error.
229
+ */
230
+ #rejectUnknownOptions(options) {
231
+ const unknown = Object.keys(options).filter(key => !WorkflowExecutor.#EXECUTE_OPTION_KEYS.has(key));
232
+ if (unknown.length === 0) return;
233
+ throw new ExecutionError(`execute received unknown option(s) ${unknown.join(', ')}; it takes (workflowId, { inputs, executeOptions, dependencyInputs, runDependencies, signal }) — workflow inputs go under "inputs"`, {
234
+ reason: 'unknown-execute-option'
235
+ });
236
+ }
237
+
238
+ /**
239
+ * Runs one workflow invocation — the recursive worker every entry point funnels
240
+ * through: the public {@link WorkflowExecutor.execute}, a sub-workflow step,
241
+ * and a `dependsOn` prerequisite alike, each entering the caller's call stack
242
+ * with its own edge.
243
+ */
244
+ async #run(locator, inputs, scope, callStack, via) {
245
+ const startedAt = this.#now();
246
+ const {
247
+ workflowId
248
+ } = locator;
249
+ // entering yields a new stack rather than mutating the caller's, so leaving is
250
+ // implicit — and a cycle or over-deep nesting throws here, before any of this
251
+ // workflow's own work begins. The entry is identified by the owning
252
+ // document's URI as well as the id, so a foreign document's reference
253
+ // back into the entry document closes a detectable cycle, and two
254
+ // documents' workflows sharing an id do not falsely form one.
255
+ const nested = callStack.enter(workflowId, via, locator.document.uri);
256
+ // the workflow boundary, checked in its own right and not only per step: a
257
+ // run cancelled before any step — or between a prerequisite and the workflow
258
+ // that needed it — must not go on to normalize and enter a workflow nobody
259
+ // is waiting for.
260
+ throwIfAborted(scope.signal, {
261
+ workflowId,
262
+ callStack: nested
263
+ });
264
+ const workflow = await this.#resolveWorkflow(locator, scope);
265
+ const state = new WorkflowExecutionState({
266
+ inputs
267
+ });
268
+ const invocation = {
269
+ workflowId,
270
+ document: locator.document,
271
+ state,
272
+ callStack: nested,
273
+ via
274
+ };
275
+
276
+ // validated before any prerequisite runs: a malformed `steps` is an
277
+ // authoring error, and discovering it only after the dependencies have
278
+ // fired would mean live side effects on the way to a throw.
279
+ const steps = this.#orderedSteps(workflow, workflowId);
280
+ // same reasoning extends to `outputs`: a workflow that cannot possibly
281
+ // produce them is unrunnable from the start, and only a shape check is
282
+ // needed to know that — no run state is required for it. Checking now, not
283
+ // at `#result` where it used to be resolved, means the throw lands before
284
+ // the first request rather than after the last.
285
+ this.#validateWorkflowOutputsShape(workflow, workflowId);
286
+ const dependencies = await this.#runDependencies(workflow, invocation, scope);
287
+ if (dependencies.some(dependency => this.#settledFailed(dependency))) {
288
+ // a declared prerequisite did not complete, so this workflow cannot be
289
+ // processed. That is a runtime failure like any failing step — a `failed`
290
+ // result carrying the dependency trace, not a throw — and none of its own
291
+ // steps run. Unless the run was cancelled, in which case the prerequisite
292
+ // did not fail on its own terms either.
293
+ throwIfAborted(scope.signal, {
294
+ workflowId,
295
+ callStack: nested
296
+ });
297
+ return this.#result(invocation, workflow, [], {
298
+ kind: 'settled',
299
+ status: 'failed'
300
+ }, dependencies, startedAt);
301
+ }
302
+ const trace = [];
303
+ let index = 0;
304
+ // named distinctly from each step's own `outcome` (a StepAttemptOutcome,
305
+ // destructured fresh every iteration below) — the two are unrelated
306
+ // values that happen to share a natural name.
307
+ let runOutcome = {
308
+ kind: 'settled',
309
+ status: 'completed'
310
+ };
311
+ stepLoop: while (index < steps.length) {
312
+ const step = steps[index];
313
+ const stepId = toValue(step.stepId);
314
+ const stepStartedAt = this.#now();
315
+ // the sub-workflow runs this step produces — one per attempt, so a retried
316
+ // sub-workflow step keeps every attempt's trace rather than only the last.
317
+ const subWorkflows = [];
318
+ // the retry reference runs this step's attempts produced, one per firing —
319
+ // separate from `subWorkflows` because a reference belongs to the retry
320
+ // that invoked it, not to the step's own target.
321
+ const retryReferences = [];
322
+ const attempt = this.#chargedAttempt(this.#stepAttempt(step, stepId, invocation, scope, subWorkflows), invocation, scope, stepId);
323
+
324
+ // the retry runner settles any `retry` actions, so `action` is the terminal
325
+ // action a retry chain resolved to and `attempts` is how many times the step
326
+ // ran.
327
+ const {
328
+ outcome,
329
+ action,
330
+ attempts
331
+ } = await this.#retryRunner.run(attempt, {
332
+ stepId,
333
+ workflowId,
334
+ signal: scope.signal,
335
+ runReference: this.#retryReference(steps, stepId, invocation, scope, retryReferences)
336
+ });
337
+ state.setStepOutputs(outcome.stepId, outcome.outputs);
338
+ trace.push({
339
+ stepId: outcome.stepId,
340
+ successful: outcome.successful,
341
+ action,
342
+ attempts,
343
+ durationMs: this.#now() - stepStartedAt,
344
+ ...(subWorkflows.length > 0 ? {
345
+ subWorkflows
346
+ } : {}),
347
+ ...(retryReferences.length > 0 ? {
348
+ retryReferences
349
+ } : {})
350
+ });
351
+ const transition = this.#interpreter.interpret(action, outcome.successful, {
352
+ workflowId,
353
+ stepId
354
+ });
355
+ switch (transition.kind) {
356
+ case 'next':
357
+ index += 1;
358
+ break;
359
+ case 'goto':
360
+ index = this.#interpreter.indexOfStep(steps, transition.stepId, workflowId, {
361
+ reason: 'goto-target-not-found',
362
+ label: 'goto target'
363
+ });
364
+ break;
365
+ case 'transfer':
366
+ {
367
+ // a one-way transfer: this run ends here, and the target's run is
368
+ // nested rather than spliced into this one. Checked for cancellation
369
+ // before running the target — same boundary convention as every
370
+ // other nested call — ahead of the reference helper's own
371
+ // resolution, so an aborted run reports `aborted` rather than
372
+ // whatever resolving the target (a malformed reference, a missing
373
+ // source) would have thrown.
374
+ throwIfAborted(scope.signal, {
375
+ workflowId,
376
+ stepId,
377
+ callStack: nested
378
+ });
379
+ const transferredTo = await this.#runReferencedWorkflow(transition.workflowId, stepId, invocation, scope, 'goto');
380
+ runOutcome = {
381
+ kind: 'transferred',
382
+ transferredTo
383
+ };
384
+ break stepLoop;
385
+ }
386
+ case 'end':
387
+ runOutcome = {
388
+ kind: 'settled',
389
+ status: 'ended'
390
+ };
391
+ break stepLoop;
392
+ case 'break':
393
+ runOutcome = {
394
+ kind: 'settled',
395
+ status: 'failed'
396
+ };
397
+ break stepLoop;
398
+ default:
399
+ {
400
+ // exhaustiveness guard: a Transition kind added above without a case
401
+ // here would otherwise fall through silently instead of failing
402
+ // loudly — `transition` is provably `never` once every kind above
403
+ // has its own case, which TypeScript checks at compile time.
404
+ const unreachable = transition;
405
+ throw new ExecutionError(`step "${stepId}" in workflow "${workflowId}" produced an unrecognized transition ${JSON.stringify(unreachable)}`, {
406
+ workflowId,
407
+ stepId,
408
+ reason: 'unknown-transition-kind'
409
+ });
410
+ }
411
+ }
412
+ }
413
+
414
+ // the closing boundary. Nothing follows the last step, so without a check
415
+ // here a run abandoned while that step was in flight would resolve as though
416
+ // the caller had waited for it — and whether it did would depend on the
417
+ // transport, since one honoring the signal fails the request instead.
418
+ // Cancellation must not be the one outcome that reads differently for the
419
+ // last step than for every step before it.
420
+ throwIfAborted(scope.signal, {
421
+ workflowId,
422
+ callStack: nested
423
+ });
424
+ return this.#result(invocation, workflow, trace, runOutcome, dependencies, startedAt);
425
+ }
426
+
427
+ /**
428
+ * Assembles the run's result from how it ended, resolving the workflow's
429
+ * `outputs` against the final state (unless it transferred) and stamping
430
+ * the elapsed time. `dependencies` is reported only when the workflow
431
+ * actually had some.
432
+ *
433
+ * `outcome` is the single source every derived field reads from — `status`,
434
+ * `settledStatus`, whether `outputs` gets resolved at all, whether
435
+ * `transferredTo` is present — so there is no second place any of them
436
+ * could drift from it. A `'transferred'` run never reaches its own end, so
437
+ * its `outputs` declaration is never evaluated — `{}` is the only honest
438
+ * value; the target's own outputs live at `transferredTo.outputs`. Skipping
439
+ * resolution loses no validation:
440
+ * {@link WorkflowExecutor.#validateWorkflowOutputsShape} already checked
441
+ * the shape up front, independent of resolving it.
442
+ */
443
+ #result(invocation, workflow, steps, outcome, dependencies, startedAt) {
444
+ const shared = {
445
+ workflowId: invocation.workflowId,
446
+ steps,
447
+ durationMs: this.#now() - startedAt,
448
+ ...(dependencies.length > 0 ? {
449
+ dependencies
450
+ } : {})
451
+ };
452
+ if (outcome.kind === 'transferred') {
453
+ return {
454
+ ...shared,
455
+ outputs: {},
456
+ status: 'transferred',
457
+ // computed once, here, rather than left for a caller to chase via
458
+ // `settledResult` — `transferredTo` already carries its own
459
+ // `settledStatus`, itself computed the same way when it was built,
460
+ // so this is O(1) per hop rather than a walk of the whole chain.
461
+ settledStatus: outcome.transferredTo.settledStatus,
462
+ transferredTo: outcome.transferredTo
463
+ };
464
+ }
465
+ return {
466
+ ...shared,
467
+ outputs: this.#resolveWorkflowOutputs(workflow, invocation),
468
+ status: outcome.status,
469
+ settledStatus: outcome.status
470
+ };
471
+ }
472
+
473
+ /**
474
+ * Whether a nested run's transfer chain settled on `'failed'` — reading
475
+ * {@link WorkflowExecutionResult.settledStatus} directly, which every
476
+ * result already carries, rather than walking `transferredTo` itself.
477
+ */
478
+ #settledFailed(result) {
479
+ return result.settledStatus === 'failed';
480
+ }
481
+
482
+ /**
483
+ * Charges one step attempt to the run-wide budget, throwing when it is spent.
484
+ *
485
+ * This is what bounds a runaway `retry`, a runaway `goto` loop, and a runaway
486
+ * tree of sub-workflow calls alike. The budget spans the whole call tree, so a
487
+ * sub-workflow cannot start afresh; the error carries the chain in progress,
488
+ * because the leaf that happened to spend the last unit is rarely the workflow
489
+ * that is looping.
490
+ */
491
+ #chargeBudget(scope, invocation, stepId) {
492
+ if (++scope.budget.spent <= this.#maxSteps) return;
493
+ throw new ExecutionError(`workflow "${invocation.workflowId}" exceeded its budget of ${this.#maxSteps} step attempts (a goto loop, excessive retries, or runaway sub-workflow calls)`, {
494
+ workflowId: invocation.workflowId,
495
+ stepId,
496
+ reason: 'step-budget',
497
+ path: invocation.callStack.path
498
+ });
499
+ }
500
+
501
+ /**
502
+ * Wraps a step-attempt thunk with the checks every attempt needs at its
503
+ * boundary: charge the run-wide budget, and check for cancellation both
504
+ * before the attempt runs and, if it throws, again on the way out — a
505
+ * cancellation noticed inside the attempt (StepExecutor is handed no
506
+ * workflow or call chain) is re-raised here with both attached, which keeps
507
+ * an abort surfacing mid-request as informative as one caught between two
508
+ * attempts.
509
+ *
510
+ * Shared by the step's own attempts and a `retry` action's `stepId`
511
+ * reference, which is itself one more step attempt against this same run.
512
+ */
513
+ #chargedAttempt(runAttempt, invocation, scope, stepId) {
514
+ const {
515
+ workflowId,
516
+ callStack
517
+ } = invocation;
518
+ return async () => {
519
+ throwIfAborted(scope.signal, {
520
+ workflowId,
521
+ stepId,
522
+ callStack
523
+ });
524
+ this.#chargeBudget(scope, invocation, stepId);
525
+ try {
526
+ return await runAttempt();
527
+ } catch (error) {
528
+ throwIfAborted(scope.signal, {
529
+ workflowId,
530
+ stepId,
531
+ callStack
532
+ });
533
+ throw error;
534
+ }
535
+ };
536
+ }
537
+
538
+ /**
539
+ * Builds the {@link StepRetryRunContext.runReference} callback a `retry`
540
+ * action's `stepId` / `workflowId` reference is run through.
541
+ *
542
+ * The runner that calls this back owns *when* — once per firing, after the
543
+ * `retryAfter` wait, before the step is re-run; this owns *how*. A retry
544
+ * action naming both fields is malformed — the same mutual-exclusivity
545
+ * `#subWorkflowId` enforces for a step's own target — rejected before either
546
+ * branch below runs, rather than silently preferring one.
547
+ *
548
+ * - a `workflowId` reference runs that workflow to completion through
549
+ * `#run`, on the same shared budget, call stack, and cycle/depth guards
550
+ * as a sub-workflow step — entered via `'retry'` so a loop closed through
551
+ * it is named a call cycle, not a dependency one, and charged one unit
552
+ * for entering the reference itself, matching the "1 for the step itself
553
+ * plus whatever its sub-run spends" convention a sub-workflow step
554
+ * already follows (without it, a reference to a workflow with few or no
555
+ * steps of its own would cost nothing to fire). It runs with no inputs:
556
+ * the specification gives a retry reference no input-mapping mechanism
557
+ * of its own (the same gap `dependsOn` has), and `{}` is the only
558
+ * spec-honest fallback — see issue `#62`. Its outputs are recorded via
559
+ * `state.setWorkflow`, which *is* "context transfers back" — but its
560
+ * `inputs` are preserved rather than overwritten with `{}` when this
561
+ * workflowId was already recorded (by an earlier `dependsOn` prerequisite
562
+ * or sub-workflow step sharing the id): this call's own lack of a real
563
+ * inputs value must not erase one a different, better-informed caller
564
+ * already gave.
565
+ * - a `stepId` reference must name a step of the current workflow (checked
566
+ * lazily, at fire time, via the same lookup a `goto` target uses) and
567
+ * runs as one more attempt at that step, through
568
+ * {@link WorkflowExecutor.#stepAttempt} and
569
+ * {@link WorkflowExecutor.#chargedAttempt} — so a step referencing a
570
+ * `workflowId` itself is handled the normal way, and the reference counts
571
+ * against the same budget. Its outputs are recorded via
572
+ * `state.setStepOutputs` — that recording *is* "context transfers back".
573
+ *
574
+ * Neither kind's own `onSuccess` / `onFailure` (or, for a step reference, its
575
+ * `successCriteria`-derived action) is acted on: "context transfers back upon
576
+ * completion" makes a reference a call, not a redirect, and a failed
577
+ * reference does not break the retry chain — the spec speaks of the
578
+ * reference's *completion*, not its success, and a retry that turns out
579
+ * futile is bounded by `retryLimit` regardless.
580
+ */
581
+ #retryReference(steps, stepId, invocation, scope, retryReferences) {
582
+ const {
583
+ workflowId,
584
+ state,
585
+ callStack
586
+ } = invocation;
587
+ return async action => {
588
+ throwIfAborted(scope.signal, {
589
+ workflowId,
590
+ stepId,
591
+ callStack
592
+ });
593
+ const {
594
+ hasStepId,
595
+ hasWorkflowId
596
+ } = actionTargets(action);
597
+ if (hasWorkflowId && hasStepId) {
598
+ throw new ExecutionError(`retry action on step "${stepId}" in workflow "${workflowId}" declares both a stepId and a workflowId (mutually exclusive)`, {
599
+ stepId,
600
+ workflowId,
601
+ reason: 'ambiguous-target'
602
+ });
603
+ }
604
+ if (hasWorkflowId) {
605
+ const reference = toValue(action.workflowId);
606
+ this.#chargeBudget(scope, invocation, stepId);
607
+ const result = await this.#runReferencedWorkflow(reference, stepId, invocation, scope, 'retry');
608
+ // the result's workflowId is the bare id the reference resolved to —
609
+ // the key run state is recorded under, whichever document owns it.
610
+ const refId = result.workflowId;
611
+ state.setWorkflow(refId, {
612
+ inputs: state.getWorkflow(refId)?.inputs ?? {},
613
+ outputs: result.outputs
614
+ });
615
+ retryReferences.push({
616
+ kind: 'workflow',
617
+ id: reference,
618
+ successful: !this.#settledFailed(result),
619
+ subWorkflow: result
620
+ });
621
+ return;
622
+ }
623
+ const refStepId = toValue(action.stepId);
624
+ const refIndex = this.#interpreter.indexOfStep(steps, refStepId, workflowId, {
625
+ reason: 'retry-target-not-found',
626
+ label: 'retry reference'
627
+ });
628
+ const refStep = steps[refIndex];
629
+ const refSubWorkflows = [];
630
+ const outcome = await this.#chargedAttempt(this.#stepAttempt(refStep, refStepId, invocation, scope, refSubWorkflows), invocation, scope, refStepId)();
631
+ state.setStepOutputs(outcome.stepId, outcome.outputs);
632
+ retryReferences.push({
633
+ kind: 'step',
634
+ id: refStepId,
635
+ successful: outcome.successful,
636
+ ...(refSubWorkflows.length > 0 ? {
637
+ subWorkflow: refSubWorkflows[0]
638
+ } : {})
639
+ });
640
+ };
641
+ }
642
+
643
+ /**
644
+ * Runs the workflows this workflow `dependsOn`, in declaration order, before
645
+ * any of its own steps — the Arazzo "MUST be completed before this workflow can
646
+ * be processed" precondition, satisfied on demand.
647
+ *
648
+ * Each dependency's outputs are recorded into the dependent's state so
649
+ * `$workflows.{id}.outputs` resolves; they are not merged into the dependent's
650
+ * own outputs. Runs stop at the first dependency that fails — the dependent
651
+ * cannot be processed, so running the rest would be pointless work with live
652
+ * side effects.
653
+ *
654
+ * Only workflows run *as prerequisites* are remembered as satisfying later
655
+ * ones. A workflow that already ran in this tree as a sub-workflow **step**
656
+ * target does not settle a later `dependsOn` on the same id, and runs again:
657
+ * a step call is an explicit invocation carrying that step's parameters as
658
+ * inputs, whereas a prerequisite runs with whatever the caller supplied in
659
+ * `dependencyInputs`. Treating one as having discharged the other would
660
+ * silently accept a run made with different inputs than the precondition asks
661
+ * for.
662
+ */
663
+ async #runDependencies(workflow, invocation, scope) {
664
+ const {
665
+ state,
666
+ callStack,
667
+ via
668
+ } = invocation;
669
+ // `runDependencies: false` is the caller vouching for the prerequisites of
670
+ // the workflow they named. It stops there: a sub-workflow's own `dependsOn`
671
+ // is an implementation detail the caller may not know exists, so skipping it
672
+ // on their behalf would run that workflow against state nobody claimed to
673
+ // have prepared — silently producing a wrong-but-`completed` result.
674
+ if (via === 'root' && !scope.runDependencies) return [];
675
+ if (!workflow.hasKey('dependsOn')) return [];
676
+
677
+ // every entry is validated before the first one runs: an authoring error
678
+ // found halfway down the list would otherwise be raised only after earlier
679
+ // prerequisites had already fired live requests.
680
+ const dependencyRefs = await this.#dependencyRefs(workflow, invocation);
681
+ const results = [];
682
+ for (const dependency of dependencyRefs) {
683
+ const qualifiedId = this.#qualifiedId(dependency.locator);
684
+ // a dependency already completed in this run is satisfied, not repeated: a
685
+ // precondition holds once met, and a diamond (two dependents sharing one
686
+ // dependency) must not duplicate its live side effects. Every dependent
687
+ // still reports and reads that one run — including the inputs the run
688
+ // was actually given, not whatever this dependent's own
689
+ // `dependencyInputs` entry would have supplied had it run it.
690
+ const memoized = scope.dependencyRuns.get(qualifiedId);
691
+ const inputs = memoized?.inputs ?? scope.dependencyInputs[dependency.inputsKey] ?? {};
692
+ const result = memoized?.result ?? (await this.#run(dependency.locator, inputs, scope, callStack, 'dependsOn'));
693
+ results.push(result);
694
+ // recorded whether or not it completed: a failed prerequisite still
695
+ // resolved (possibly partial) outputs, and the parent resolves its own
696
+ // outputs against this state on its way out, so `$workflows.{id}` is
697
+ // uniformly readable for every dependency that ran. `outputs` is the
698
+ // dependency's own (`{}` when it transferred) — never the settled
699
+ // workflow's, which would misattribute values to a declaration that
700
+ // never ran. Keyed by the bare id even for a cross-document dependency:
701
+ // `$workflows.{id}` is the only form an expression can read it back by.
702
+ state.setWorkflow(dependency.locator.workflowId, {
703
+ inputs,
704
+ outputs: result.outputs
705
+ });
706
+ if (this.#settledFailed(result)) return results;
707
+
708
+ // only a dependency whose chain settled non-`failed` is remembered as
709
+ // satisfied — a failure, transferred or not, must never let a later
710
+ // dependent skip running it.
711
+ scope.dependencyRuns.set(qualifiedId, {
712
+ result,
713
+ inputs
714
+ });
715
+ }
716
+ return results;
717
+ }
718
+
719
+ /**
720
+ * The workflows a workflow declares in `dependsOn`, each resolved to its
721
+ * locator, with every entry checked before any of them is run: the list
722
+ * must be a list of strings, and each entry — a plain id or a
723
+ * cross-document `$sourceDescriptions.{name}.{workflowId}` reference —
724
+ * must name a workflow its document defines.
725
+ *
726
+ * Checking the whole list up front is the point. These are authoring errors,
727
+ * and validating them one at a time as they are run would let a bad entry
728
+ * halfway down the list throw only after the entries before it had already
729
+ * made live requests.
730
+ *
731
+ * `inputsKey` is the entry as written, the key
732
+ * {@link WorkflowExecuteOptions.dependencyInputs} addresses the dependency
733
+ * by.
734
+ */
735
+ async #dependencyRefs(workflow, invocation) {
736
+ const {
737
+ workflowId,
738
+ document
739
+ } = invocation;
740
+ const dependsOn = workflow.dependsOn;
741
+ if (!isArrayElement(dependsOn)) {
742
+ throw new ExecutionError(`workflow "${workflowId}" has a non-list "dependsOn"`, {
743
+ workflowId,
744
+ reason: 'malformed-dependsOn'
745
+ });
746
+ }
747
+ const references = [...dependsOn].map((entry, index) => {
748
+ if (!isStringElement(entry)) {
749
+ throw new ExecutionError(`workflow "${workflowId}" has a non-string entry at dependsOn[${index}]`, {
750
+ workflowId,
751
+ reason: 'malformed-dependsOn'
752
+ });
753
+ }
754
+ return toValue(entry);
755
+ });
756
+ const refs = [];
757
+ for (const reference of references) {
758
+ const locator = await this.#workflowLocatorNormalizer.normalize(reference, document, {
759
+ workflowId
760
+ });
761
+ if (!locator.document.workflowIndex.has(locator.workflowId)) {
762
+ throw new ExecutionError(`workflow "${workflowId}" depends on "${reference}", which the Arazzo document at "${locator.document.uri}" does not define`, {
763
+ workflowId,
764
+ reason: 'workflow-not-found'
765
+ });
766
+ }
767
+ refs.push({
768
+ locator,
769
+ inputsKey: reference
770
+ });
771
+ }
772
+ return refs;
773
+ }
774
+
775
+ /**
776
+ * Builds the thunk that runs one attempt at a step, hiding which kind of step
777
+ * it is from the retry loop.
778
+ *
779
+ * An operation step is delegated to {@link StepExecutor}. A step targeting a
780
+ * `workflowId` is a sub-workflow call this executor runs itself — the case
781
+ * StepExecutor refuses — mapping the step's `parameters` to the sub-workflow's
782
+ * inputs, recording the sub-run under `$workflows`, then resolving the step's
783
+ * own `outputs` and selecting its actions against that updated state. Because
784
+ * both reduce to a {@link StepAttemptOutcome}, `retry` on a sub-workflow step
785
+ * works as it does on an operation step: each attempt re-runs the sub-workflow
786
+ * — its steps, that is — charged against the same budget. Prerequisites it
787
+ * already completed stay satisfied and are not run again, since a completed
788
+ * `dependsOn` workflow is memoized for the whole run; a retry re-runs the
789
+ * work, not the preconditions.
790
+ */
791
+ #stepAttempt(step, stepId, invocation, scope, subWorkflows) {
792
+ const {
793
+ workflowId,
794
+ document,
795
+ state,
796
+ callStack
797
+ } = invocation;
798
+ if (!isStringElement(step.workflowId)) {
799
+ return () => this.#stepExecutor.forDocument(document).execute(step, state, scope.executeOptions);
800
+ }
801
+ const reference = this.#subWorkflowReference(step, stepId, workflowId);
802
+ const stepScope = {
803
+ stepId
804
+ };
805
+ // the reference resolves to the same locator on every attempt, so it is
806
+ // resolved once for the whole retry chain, on the first attempt; that
807
+ // same first-attempt moment — before this call has written anything —
808
+ // is when this call's own $workflows entry is pinned (typically absent,
809
+ // but a legitimate value when an earlier, different step already called
810
+ // the same workflowId), held fixed across every attempt below so a retry
811
+ // never sees this call's *own* prior-attempt output. Everything else is
812
+ // read live per attempt.
813
+ let locator;
814
+ let priorWorkflowEntry;
815
+ return async () => {
816
+ if (locator === undefined) {
817
+ locator = await this.#workflowLocatorNormalizer.normalize(reference, document, {
818
+ workflowId,
819
+ stepId
820
+ });
821
+ priorWorkflowEntry = state.getWorkflow(locator.workflowId);
822
+ }
823
+ const subWorkflowId = locator.workflowId;
824
+
825
+ // the sub-workflow's inputs come from the step's parameters, mapped by
826
+ // bare name — per the specification, "all parameters map to workflow
827
+ // inputs" for such a step. That includes an inherited parameter that
828
+ // carries an `in` (the step's own carry none): it still arrives as an
829
+ // input under its name, not under the '{in}.{name}' key an operation
830
+ // step would deliver it by.
831
+ //
832
+ // Resolved fresh on every attempt, not once outside this thunk: a
833
+ // `retry` action's reference can now mutate `$steps` / `$workflows`
834
+ // between attempts (a repair a retried step's own parameters should
835
+ // see), and freezing inputs once would hide that. The one thing that
836
+ // must NOT drift attempt to attempt is this call's own target —
837
+ // `#inputContext` pins `subWorkflowId`'s entry back to
838
+ // `priorWorkflowEntry` so a parameter reading `$workflows.{subWorkflowId}`
839
+ // sees the same (usually absent) value on every attempt, not the
840
+ // previous attempt's own result.
841
+ const preContext = this.#inputContext(state, subWorkflowId, priorWorkflowEntry);
842
+ const inputs = this.#parameterResolver.resolve(step.parameters, expression => this.#evaluate(preContext, expression, document), stepScope);
843
+ const result = await this.#run(locator, inputs, scope, callStack, 'step');
844
+ subWorkflows.push(result);
845
+ state.setWorkflow(subWorkflowId, {
846
+ inputs,
847
+ outputs: result.outputs
848
+ });
849
+
850
+ // resolved after the sub-run is recorded, so the step's outputs can map
851
+ // out of `$workflows.{subWorkflowId}.outputs`. The called workflow is the
852
+ // step's result: `$outputs` names its outputs — the enclosing workflow's
853
+ // own outputs do not exist until it ends, so that reading could never
854
+ // resolve here — and, the step having made no HTTP call, `calledWorkflow`
855
+ // makes `$statusCode`, `$response` and the other HTTP-exchange roots
856
+ // authoring errors rather than silent misses.
857
+ const context = {
858
+ ...state.toContext(),
859
+ outputs: result.outputs,
860
+ calledWorkflow: {
861
+ stepId,
862
+ workflowId: subWorkflowId
863
+ }
864
+ };
865
+ const outputs = this.#outputResolver.resolve(step.outputs, expression => this.#evaluate(context, expression, document), stepScope);
866
+ // an `end`ed sub-workflow returned to its caller with outputs, so it took
867
+ // the success path like a completed one; only a chain settling `failed` is
868
+ // a failure — a sub-workflow that transferred is judged by where its
869
+ // chain settled, via `#settledFailed`, not by the intermediate
870
+ // `'transferred'` status itself. The step's own `successCriteria` still
871
+ // apply on top — they are the author's assertion about this step, and
872
+ // dropping them because the step happens to target a workflow would
873
+ // silently discard it. They see the sub-run's own outputs (`{}` when it
874
+ // transferred) through `$outputs` and `$workflows`.
875
+ // the criteria and actions are the *calling* step's, so they are
876
+ // evaluated by the parent document's step executor — only the sub-run
877
+ // itself switched documents.
878
+ const stepExecutor = this.#stepExecutor.forDocument(document);
879
+ const successful = !this.#settledFailed(result) && stepExecutor.evaluateCriteria(step, context);
880
+ const matchedActions = stepExecutor.selectActions(step, successful, context);
881
+ return {
882
+ stepId,
883
+ successful,
884
+ outputs,
885
+ action: matchedActions[0],
886
+ matchedActions
887
+ };
888
+ };
889
+ }
890
+
891
+ /**
892
+ * The live run context, with `workflowId`'s own `$workflows` entry pinned to
893
+ * `entry` rather than whatever `state` holds right now.
894
+ *
895
+ * Used to resolve a sub-workflow step's inputs on a retry: every other path
896
+ * (`$steps`, every *other* `$workflows` entry, `$inputs`) must be read live,
897
+ * so a `retry` reference's repair between attempts is visible — but this
898
+ * call's own target must not, or a retried attempt would read its own
899
+ * previous attempt's output instead of the value it had before this step's
900
+ * retry chain began. `toContext` builds a fresh plain object per call, so
901
+ * overriding one key here never touches `state` itself.
902
+ */
903
+ #inputContext(state, workflowId, entry) {
904
+ const context = state.toContext();
905
+ const {
906
+ [workflowId]: _current,
907
+ ...workflows
908
+ } = context.workflows ?? {};
909
+ return {
910
+ ...context,
911
+ workflows: entry === undefined ? workflows : {
912
+ ...workflows,
913
+ [workflowId]: entry
914
+ }
915
+ };
916
+ }
917
+
918
+ /**
919
+ * The reference a sub-workflow step targets, as written — a plain
920
+ * workflowId of the current document, or a cross-document
921
+ * `$sourceDescriptions.{name}.{workflowId}` runtime expression.
922
+ */
923
+ #subWorkflowReference(step, stepId, workflowId) {
924
+ // a step names its target once: declaring any other target alongside a
925
+ // workflow is malformed and has no defined resolution. StepExecutor makes
926
+ // the same check, but a sub-workflow step never reaches it — so both read
927
+ // the one list of target fields, and a target added to the specification
928
+ // cannot be rejected by one and silently accepted by the other.
929
+ const conflicting = STEP_TARGET_FIELDS.filter(field => field !== 'workflowId' && isStringElement(step[field]));
930
+ if (conflicting.length > 0) {
931
+ throw new ExecutionError(`step "${stepId}" in workflow "${workflowId}" declares a workflowId alongside ${conflicting.join(', ')} (mutually exclusive)`, {
932
+ stepId,
933
+ workflowId,
934
+ reason: 'ambiguous-target'
935
+ });
936
+ }
937
+ return toValue(step.workflowId);
938
+ }
939
+
940
+ /**
941
+ * Runs a workflow an action references directly — a retry's `workflowId`
942
+ * reference or a goto's transfer, plain id or cross-document expression —
943
+ * with `{}` inputs, since neither has an input-mapping mechanism of its own
944
+ * (issue `#62`).
945
+ *
946
+ * Shared rather than duplicated by its two callers, which otherwise repeat
947
+ * the identical "resolve the reference, then run with no inputs" sequence.
948
+ * Rejects a target its document does not define with the calling `stepId`
949
+ * attached, unlike {@link WorkflowExecutor.#resolveWorkflow}'s own
950
+ * `workflow-not-found` (reached only once `#run` itself resolves the
951
+ * target) — which carries no caller context at all, since it is reached
952
+ * generically from every kind of nested call.
953
+ */
954
+ async #runReferencedWorkflow(reference, stepId, invocation, scope, via) {
955
+ const locator = await this.#workflowLocatorNormalizer.normalize(reference, invocation.document, {
956
+ workflowId: invocation.workflowId,
957
+ stepId
958
+ });
959
+ if (!locator.document.workflowIndex.has(locator.workflowId)) {
960
+ throw new ExecutionError(`workflow "${locator.workflowId}" not found in Arazzo document at "${locator.document.uri}"`, {
961
+ workflowId: locator.workflowId,
962
+ stepId,
963
+ reason: 'workflow-not-found'
964
+ });
965
+ }
966
+ return this.#run(locator, {}, scope, invocation.callStack, via);
967
+ }
968
+
969
+ /**
970
+ * Extracts and normalizes the workflow a locator names. A workflow its
971
+ * document does not define is the executor-level `workflow-not-found`
972
+ * authoring error, raised here rather than leaking the extractor's
973
+ * `ExtractionError`.
974
+ */
975
+ async #resolveWorkflow(locator, scope) {
976
+ const {
977
+ document,
978
+ workflowId
979
+ } = locator;
980
+ const qualifiedId = this.#qualifiedId(locator);
981
+ const cached = scope.workflows.get(qualifiedId);
982
+ if (cached !== undefined) return cached;
983
+ if (!document.workflowIndex.has(workflowId)) {
984
+ throw new ExecutionError(`workflow "${workflowId}" not found in Arazzo document at "${document.uri}"`, {
985
+ workflowId,
986
+ reason: 'workflow-not-found'
987
+ });
988
+ }
989
+ const workflow = await this.#normalizer.normalize(this.#extractor.extract(document, workflowId), document);
990
+ scope.workflows.set(qualifiedId, workflow);
991
+ return workflow;
992
+ }
993
+
994
+ /**
995
+ * The run-wide identity of the workflow a locator names —
996
+ * `{documentURI}#{workflowId}`, the key the normalized-workflow cache and
997
+ * the dependency ledger share (the call stack compares the same pair,
998
+ * unformatted). Qualified because a bare workflowId is only unique within
999
+ * its own document.
1000
+ */
1001
+ #qualifiedId(locator) {
1002
+ return `${locator.document.uri}#${locator.workflowId}`;
1003
+ }
1004
+
1005
+ /**
1006
+ * The workflow's steps as an array in list order. An absent `steps` is a
1007
+ * workflow with no steps — an empty list, a completed no-op run. A present but
1008
+ * malformed `steps` (not a list, or holding a non-step entry) is an authoring
1009
+ * error and throws rather than being silently treated as empty or partial.
1010
+ */
1011
+ #orderedSteps(workflow, workflowId) {
1012
+ if (!workflow.hasKey('steps')) return [];
1013
+ const steps = workflow.steps;
1014
+ if (!isArrayElement(steps)) {
1015
+ throw new ExecutionError(`workflow "${workflowId}" has a non-list "steps"`, {
1016
+ workflowId,
1017
+ reason: 'malformed-steps'
1018
+ });
1019
+ }
1020
+ return [...steps].map((step, index) => {
1021
+ if (!isStepElement(step)) {
1022
+ throw new ExecutionError(`workflow "${workflowId}" has a non-step entry at steps[${index}]`, {
1023
+ workflowId,
1024
+ reason: 'malformed-steps'
1025
+ });
1026
+ }
1027
+ return step;
1028
+ });
1029
+ }
1030
+
1031
+ /**
1032
+ * Checks that the workflow's `outputs`, if present, is a map — without
1033
+ * resolving any of its values. The same check {@link OutputResolver.resolve}
1034
+ * makes on its way to actually resolving them, run here early instead: the
1035
+ * shape needs no run state, so this can — and, to keep a malformed `outputs`
1036
+ * from costing the run its live side effects, must — happen alongside
1037
+ * {@link WorkflowExecutor.#orderedSteps}, before any prerequisite or step of
1038
+ * the workflow runs.
1039
+ */
1040
+ #validateWorkflowOutputsShape(workflow, workflowId) {
1041
+ if (!workflow.hasKey('outputs')) return;
1042
+ const outputs = workflow.outputs;
1043
+ if (isObjectElement(outputs)) return;
1044
+ throw new ResolverError('`outputs` is present but is not a map', {
1045
+ workflowId,
1046
+ target: 'outputs',
1047
+ reason: 'malformed-outputs'
1048
+ });
1049
+ }
1050
+
1051
+ /**
1052
+ * Resolves the workflow's `outputs` declaration against the final run state,
1053
+ * mirroring how {@link StepExecutor} resolves a step's outputs.
1054
+ */
1055
+ #resolveWorkflowOutputs(workflow, invocation) {
1056
+ const context = invocation.state.toContext();
1057
+ return this.#outputResolver.resolve(workflow.outputs, expression => this.#evaluate(context, expression, invocation.document), {
1058
+ workflowId: toValue(workflow.workflowId)
1059
+ });
1060
+ }
1061
+
1062
+ /**
1063
+ * Resolves a runtime expression leniently against a context, forwarding
1064
+ * `$components` / `$sourceDescriptions` resolution to the given document —
1065
+ * the invocation's, so a foreign workflow's expressions resolve against
1066
+ * the document that owns it — and the registry; the workflow-scoped
1067
+ * counterpart of {@link StepExecutor}'s bridge.
1068
+ */
1069
+ #evaluate(context, expression, document) {
1070
+ return new RuntimeExpressionEvaluator(context, {
1071
+ strict: false,
1072
+ document,
1073
+ registry: this.#registry
1074
+ }).evaluate(expression);
1075
+ }
1076
+ }
1077
+ export default WorkflowExecutor;