@relayflows/sdk 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/dist/authored-flow-error.d.ts +9 -0
  2. package/dist/authored-flow-error.d.ts.map +1 -0
  3. package/dist/authored-flow-error.js +13 -0
  4. package/dist/authored-flow-error.js.map +1 -0
  5. package/dist/authored-flow-executor.d.ts +28 -0
  6. package/dist/authored-flow-executor.d.ts.map +1 -0
  7. package/dist/authored-flow-executor.js +203 -0
  8. package/dist/authored-flow-executor.js.map +1 -0
  9. package/dist/authored-flow-lifecycle.d.ts +61 -0
  10. package/dist/authored-flow-lifecycle.d.ts.map +1 -0
  11. package/dist/authored-flow-lifecycle.js +284 -0
  12. package/dist/authored-flow-lifecycle.js.map +1 -0
  13. package/dist/authored-flow-loader.d.ts +7 -0
  14. package/dist/authored-flow-loader.d.ts.map +1 -0
  15. package/dist/authored-flow-loader.js +39 -0
  16. package/dist/authored-flow-loader.js.map +1 -0
  17. package/dist/authored-flow-operation.d.ts +43 -0
  18. package/dist/authored-flow-operation.d.ts.map +1 -0
  19. package/dist/authored-flow-operation.js +176 -0
  20. package/dist/authored-flow-operation.js.map +1 -0
  21. package/dist/authored-flow.d.ts +12 -0
  22. package/dist/authored-flow.d.ts.map +1 -0
  23. package/dist/authored-flow.js +13 -0
  24. package/dist/authored-flow.js.map +1 -0
  25. package/dist/authored-promise-graph.d.ts +67 -0
  26. package/dist/authored-promise-graph.d.ts.map +1 -0
  27. package/dist/authored-promise-graph.js +206 -0
  28. package/dist/authored-promise-graph.js.map +1 -0
  29. package/dist/backlog-picker.d.ts +55 -0
  30. package/dist/backlog-picker.d.ts.map +1 -0
  31. package/dist/backlog-picker.js +117 -0
  32. package/dist/backlog-picker.js.map +1 -0
  33. package/dist/canonical.d.ts +18 -0
  34. package/dist/canonical.d.ts.map +1 -0
  35. package/dist/canonical.js +55 -0
  36. package/dist/canonical.js.map +1 -0
  37. package/dist/cli/check.d.ts +30 -0
  38. package/dist/cli/check.d.ts.map +1 -0
  39. package/dist/cli/check.js +324 -0
  40. package/dist/cli/check.js.map +1 -0
  41. package/dist/cli/direct-run.d.ts +3 -0
  42. package/dist/cli/direct-run.d.ts.map +1 -0
  43. package/dist/cli/direct-run.js +69 -0
  44. package/dist/cli/direct-run.js.map +1 -0
  45. package/dist/cli/hn-monitor.d.ts +107 -0
  46. package/dist/cli/hn-monitor.d.ts.map +1 -0
  47. package/dist/cli/hn-monitor.js +220 -0
  48. package/dist/cli/hn-monitor.js.map +1 -0
  49. package/dist/cli/interruptible-sleep.d.ts +13 -0
  50. package/dist/cli/interruptible-sleep.d.ts.map +1 -0
  51. package/dist/cli/interruptible-sleep.js +31 -0
  52. package/dist/cli/interruptible-sleep.js.map +1 -0
  53. package/dist/cli/run.d.ts +54 -0
  54. package/dist/cli/run.d.ts.map +1 -0
  55. package/dist/cli/run.js +316 -0
  56. package/dist/cli/run.js.map +1 -0
  57. package/dist/cli/tick-runner.d.ts +151 -0
  58. package/dist/cli/tick-runner.d.ts.map +1 -0
  59. package/dist/cli/tick-runner.js +258 -0
  60. package/dist/cli/tick-runner.js.map +1 -0
  61. package/dist/cli-adapter.d.ts +29 -0
  62. package/dist/cli-adapter.d.ts.map +1 -0
  63. package/dist/cli-adapter.js +92 -0
  64. package/dist/cli-adapter.js.map +1 -0
  65. package/dist/cli-executable.d.ts +3 -0
  66. package/dist/cli-executable.d.ts.map +1 -0
  67. package/dist/cli-executable.js +10 -0
  68. package/dist/cli-executable.js.map +1 -0
  69. package/dist/cli.d.ts +9 -0
  70. package/dist/cli.d.ts.map +1 -0
  71. package/dist/cli.js +348 -0
  72. package/dist/cli.js.map +1 -0
  73. package/dist/compile.d.ts +42 -0
  74. package/dist/compile.d.ts.map +1 -0
  75. package/dist/compile.js +515 -0
  76. package/dist/compile.js.map +1 -0
  77. package/dist/demo-hn-monitor.d.ts +2 -0
  78. package/dist/demo-hn-monitor.d.ts.map +1 -0
  79. package/dist/demo-hn-monitor.js +118 -0
  80. package/dist/demo-hn-monitor.js.map +1 -0
  81. package/dist/dir-watcher-poller.d.ts +73 -0
  82. package/dist/dir-watcher-poller.d.ts.map +1 -0
  83. package/dist/dir-watcher-poller.js +80 -0
  84. package/dist/dir-watcher-poller.js.map +1 -0
  85. package/dist/direct-input.d.ts +10 -0
  86. package/dist/direct-input.d.ts.map +1 -0
  87. package/dist/direct-input.js +61 -0
  88. package/dist/direct-input.js.map +1 -0
  89. package/dist/failure-kinds.d.ts +25 -0
  90. package/dist/failure-kinds.d.ts.map +1 -0
  91. package/dist/failure-kinds.js +64 -0
  92. package/dist/failure-kinds.js.map +1 -0
  93. package/dist/gate-contract.d.ts +24 -0
  94. package/dist/gate-contract.d.ts.map +1 -0
  95. package/dist/gate-contract.js +50 -0
  96. package/dist/gate-contract.js.map +1 -0
  97. package/dist/hn-poller.d.ts +50 -0
  98. package/dist/hn-poller.d.ts.map +1 -0
  99. package/dist/hn-poller.js +79 -0
  100. package/dist/hn-poller.js.map +1 -0
  101. package/dist/index.d.ts +21 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +30 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/journal-client.d.ts +122 -0
  106. package/dist/journal-client.d.ts.map +1 -0
  107. package/dist/journal-client.js +277 -0
  108. package/dist/journal-client.js.map +1 -0
  109. package/dist/json-schema-bound.d.ts +7 -0
  110. package/dist/json-schema-bound.d.ts.map +1 -0
  111. package/dist/json-schema-bound.js +353 -0
  112. package/dist/json-schema-bound.js.map +1 -0
  113. package/dist/json-schema.d.ts +5 -0
  114. package/dist/json-schema.d.ts.map +1 -0
  115. package/dist/json-schema.js +72 -0
  116. package/dist/json-schema.js.map +1 -0
  117. package/dist/json-value.d.ts +6 -0
  118. package/dist/json-value.d.ts.map +1 -0
  119. package/dist/json-value.js +94 -0
  120. package/dist/json-value.js.map +1 -0
  121. package/dist/model-name.d.ts +7 -0
  122. package/dist/model-name.d.ts.map +1 -0
  123. package/dist/model-name.js +21 -0
  124. package/dist/model-name.js.map +1 -0
  125. package/dist/output-schema.d.ts +8 -0
  126. package/dist/output-schema.d.ts.map +1 -0
  127. package/dist/output-schema.js +28 -0
  128. package/dist/output-schema.js.map +1 -0
  129. package/dist/preflight.d.ts +80 -0
  130. package/dist/preflight.d.ts.map +1 -0
  131. package/dist/preflight.js +361 -0
  132. package/dist/preflight.js.map +1 -0
  133. package/dist/protocol.d.ts +359 -0
  134. package/dist/protocol.d.ts.map +1 -0
  135. package/dist/protocol.js +15 -0
  136. package/dist/protocol.js.map +1 -0
  137. package/dist/spec.d.ts +294 -0
  138. package/dist/spec.d.ts.map +1 -0
  139. package/dist/spec.js +13 -0
  140. package/dist/spec.js.map +1 -0
  141. package/dist/step-dependencies.d.ts +2 -0
  142. package/dist/step-dependencies.d.ts.map +1 -0
  143. package/dist/step-dependencies.js +88 -0
  144. package/dist/step-dependencies.js.map +1 -0
  145. package/dist/step-fields.d.ts +17 -0
  146. package/dist/step-fields.d.ts.map +1 -0
  147. package/dist/step-fields.js +32 -0
  148. package/dist/step-fields.js.map +1 -0
  149. package/dist/tick-source.d.ts +205 -0
  150. package/dist/tick-source.d.ts.map +1 -0
  151. package/dist/tick-source.js +234 -0
  152. package/dist/tick-source.js.map +1 -0
  153. package/dist/unknown-keys.d.ts +7 -0
  154. package/dist/unknown-keys.d.ts.map +1 -0
  155. package/dist/unknown-keys.js +47 -0
  156. package/dist/unknown-keys.js.map +1 -0
  157. package/dist/validate.d.ts +7 -0
  158. package/dist/validate.d.ts.map +1 -0
  159. package/dist/validate.js +458 -0
  160. package/dist/validate.js.map +1 -0
  161. package/dist/work-package-consumer.d.ts +33 -0
  162. package/dist/work-package-consumer.d.ts.map +1 -0
  163. package/dist/work-package-consumer.js +44 -0
  164. package/dist/work-package-consumer.js.map +1 -0
  165. package/dist/work-package-validator.d.ts +15 -0
  166. package/dist/work-package-validator.d.ts.map +1 -0
  167. package/dist/work-package-validator.js +72 -0
  168. package/dist/work-package-validator.js.map +1 -0
  169. package/dist/worker-cli.d.ts +16 -0
  170. package/dist/worker-cli.d.ts.map +1 -0
  171. package/dist/worker-cli.js +77 -0
  172. package/dist/worker-cli.js.map +1 -0
  173. package/dist/worker.d.ts +57 -0
  174. package/dist/worker.d.ts.map +1 -0
  175. package/dist/worker.js +129 -0
  176. package/dist/worker.js.map +1 -0
  177. package/dist/wrapper-runtime.d.ts +10 -0
  178. package/dist/wrapper-runtime.d.ts.map +1 -0
  179. package/dist/wrapper-runtime.js +83 -0
  180. package/dist/wrapper-runtime.js.map +1 -0
  181. package/dist/wrapper-session.d.ts +16 -0
  182. package/dist/wrapper-session.d.ts.map +1 -0
  183. package/dist/wrapper-session.js +244 -0
  184. package/dist/wrapper-session.js.map +1 -0
  185. package/package.json +50 -0
  186. package/src/authored-flow-error.ts +31 -0
  187. package/src/authored-flow-executor.ts +365 -0
  188. package/src/authored-flow-lifecycle.ts +344 -0
  189. package/src/authored-flow-loader.ts +44 -0
  190. package/src/authored-flow-operation.ts +252 -0
  191. package/src/authored-flow.ts +21 -0
  192. package/src/authored-promise-graph.ts +196 -0
  193. package/src/backlog-picker.ts +153 -0
  194. package/src/canonical.ts +57 -0
  195. package/src/cli/check.ts +391 -0
  196. package/src/cli/direct-run.ts +86 -0
  197. package/src/cli/hn-monitor.ts +286 -0
  198. package/src/cli/interruptible-sleep.ts +27 -0
  199. package/src/cli/run.ts +439 -0
  200. package/src/cli/tick-runner.ts +378 -0
  201. package/src/cli-adapter.ts +115 -0
  202. package/src/cli-executable.ts +12 -0
  203. package/src/cli.ts +364 -0
  204. package/src/compile.ts +573 -0
  205. package/src/demo-hn-monitor.ts +132 -0
  206. package/src/dir-watcher-poller.ts +122 -0
  207. package/src/direct-input.ts +69 -0
  208. package/src/failure-kinds.ts +77 -0
  209. package/src/gate-contract.ts +72 -0
  210. package/src/hn-poller.ts +104 -0
  211. package/src/index.ts +195 -0
  212. package/src/journal-client.ts +379 -0
  213. package/src/json-schema-bound.ts +357 -0
  214. package/src/json-schema.ts +65 -0
  215. package/src/json-value.ts +110 -0
  216. package/src/model-name.ts +20 -0
  217. package/src/output-schema.ts +33 -0
  218. package/src/preflight.ts +482 -0
  219. package/src/protocol.ts +364 -0
  220. package/src/spec.ts +343 -0
  221. package/src/step-dependencies.ts +100 -0
  222. package/src/step-fields.ts +36 -0
  223. package/src/tick-source.ts +334 -0
  224. package/src/unknown-keys.ts +52 -0
  225. package/src/validate.ts +503 -0
  226. package/src/work-package-consumer.ts +73 -0
  227. package/src/work-package-validator.ts +90 -0
  228. package/src/worker-cli.ts +110 -0
  229. package/src/worker.ts +150 -0
  230. package/src/wrapper-runtime.ts +93 -0
  231. package/src/wrapper-session.ts +287 -0
@@ -0,0 +1,365 @@
1
+ import {
2
+ COMPLETION_REASONS,
3
+ RUN_COMPLETION_REASONS,
4
+ type AgentResult,
5
+ type CloudHelper,
6
+ type CompletionReason as SurfaceCompletionReason,
7
+ type Ctx,
8
+ type RunCompletionReason as SurfaceRunCompletionReason,
9
+ type Step,
10
+ } from '@relayflows/surface';
11
+ import type { FlowHandle } from '@relayflows/surface/runtime';
12
+ import { compileSpec, toKernelSpec } from './compile.js';
13
+ import { getAuthoredFlowDefinition } from './authored-flow.js';
14
+ import {
15
+ AuthoredFlowExecutionError,
16
+ type AuthoredFlowExecutionErrorCode,
17
+ } from './authored-flow-error.js';
18
+ import {
19
+ AuthoredFlowOperation,
20
+ stopAuthoredOperations,
21
+ verifyAuthoredOperations,
22
+ } from './authored-flow-operation.js';
23
+ import { AuthoredFlowLifecycle } from './authored-flow-lifecycle.js';
24
+ import { JournalClient } from './journal-client.js';
25
+ import type {
26
+ CompletionReason as ProtocolCompletionReason,
27
+ RunCompletionReason as ProtocolRunCompletionReason,
28
+ RunOutcome,
29
+ } from './protocol.js';
30
+ import { SPEC_SCHEMA_VERSION } from './spec.js';
31
+
32
+ type Assert<T extends true> = T;
33
+ type Equal<A, B> = [A] extends [B]
34
+ ? ([B] extends [A] ? true : false)
35
+ : false;
36
+ type CompletionVocabularyMatchesProtocol = Assert<
37
+ Equal<SurfaceCompletionReason, ProtocolCompletionReason>
38
+ >;
39
+ type RunCompletionVocabularyMatchesProtocol = Assert<
40
+ Equal<SurfaceRunCompletionReason, ProtocolRunCompletionReason>
41
+ >;
42
+ type DoneCompletionReason = Parameters<Ctx['done']>[0];
43
+ type DoneAcceptsOnlyRunCompletionReasons = Assert<
44
+ DoneCompletionReason extends SurfaceRunCompletionReason ? true : false
45
+ >;
46
+ type EveryRunCompletionReasonIsAcceptedByDone = Assert<
47
+ SurfaceRunCompletionReason extends DoneCompletionReason ? true : false
48
+ >;
49
+
50
+ export { AuthoredFlowExecutionError, type AuthoredFlowExecutionErrorCode };
51
+
52
+ export interface AuthoredFlowJournalStep {
53
+ readonly id: string;
54
+ readonly runId: string;
55
+ readonly completionReason: ProtocolCompletionReason;
56
+ }
57
+
58
+ export interface AuthoredFlowExecutionResult {
59
+ readonly name: string;
60
+ readonly completionReason: ProtocolRunCompletionReason;
61
+ readonly journalSteps: readonly AuthoredFlowJournalStep[];
62
+ }
63
+
64
+ type ExecutionResultUsesRunCompletionReason = Assert<
65
+ Equal<AuthoredFlowExecutionResult['completionReason'], ProtocolRunCompletionReason>
66
+ >;
67
+ type JournalStepUsesStepCompletionReason = Assert<
68
+ Equal<AuthoredFlowJournalStep['completionReason'], ProtocolCompletionReason>
69
+ >;
70
+
71
+ /**
72
+ * Exercise the internal authored-flow lowering seam through protocol v0.
73
+ *
74
+ * This is deliberately not exported by the SDK package: without a durable
75
+ * authored root, it is not a resumable public runner. The seam is narrow: an
76
+ * empty-header flow may await plain `f.run(...)` steps and must finish with
77
+ * `f.done("success")`. Every run and
78
+ * the terminal marker is a compiled deterministic spec submitted through
79
+ * `JournalClient`; values are read back from `step.completed` journal entries.
80
+ * Unsupported headers, verbs, gates, or completion lowering fail closed.
81
+ */
82
+ export async function executeAuthoredFlow<Input = undefined>(
83
+ handle: FlowHandle,
84
+ journal: JournalClient,
85
+ input?: Input,
86
+ ): Promise<AuthoredFlowExecutionResult> {
87
+ const definition = getAuthoredFlowDefinition<Input>(handle);
88
+ const headerFields = Object.keys(definition.header);
89
+ if (headerFields.length > 0) {
90
+ throw new AuthoredFlowExecutionError(
91
+ 'unsupported_header',
92
+ `flow "${definition.name}" uses unsupported header fields: ${headerFields.join(', ')}`,
93
+ );
94
+ }
95
+
96
+ const journalSteps: AuthoredFlowJournalStep[] = [];
97
+ const authoredSteps: AuthoredFlowOperation<unknown>[] = [];
98
+ const lifecycle = new AuthoredFlowLifecycle();
99
+ let nextStep = 1;
100
+ let requestedCompletion: SurfaceRunCompletionReason | undefined;
101
+
102
+ const lowerDeterministic = async (
103
+ id: string,
104
+ command: string,
105
+ ): Promise<string> => {
106
+ const spec = toKernelSpec(compileSpec({
107
+ version: SPEC_SCHEMA_VERSION,
108
+ name: `${definition.name}/${id}`,
109
+ steps: [{ id, type: 'deterministic', command }],
110
+ }));
111
+ const outcome = await journal.runStart(spec);
112
+ return readSuccessfulOutput(journal, outcome, id, journalSteps);
113
+ };
114
+
115
+ const context: Ctx = {
116
+ run(command) {
117
+ assertOperationAllowed('run', definition.name, requestedCompletion);
118
+ const id = `run-${nextStep++}`;
119
+ return trackStep(authoredSteps, new AuthoredFlowOperation(
120
+ id,
121
+ 'run',
122
+ () => assertOperationAllowed('run', definition.name, requestedCompletion),
123
+ () => lowerDeterministic(id, command),
124
+ lifecycle,
125
+ ));
126
+ },
127
+ llm() {
128
+ assertOperationAllowed('llm', definition.name, requestedCompletion);
129
+ const id = `llm-${nextStep++}`;
130
+ return trackStep(authoredSteps, unsupportedStep(
131
+ id,
132
+ 'llm',
133
+ () => assertOperationAllowed('llm', definition.name, requestedCompletion),
134
+ lifecycle,
135
+ ));
136
+ },
137
+ agent() {
138
+ assertOperationAllowed('agent', definition.name, requestedCompletion);
139
+ const id = `agent-${nextStep++}`;
140
+ return trackStep(authoredSteps, unsupportedStep<AgentResult>(
141
+ id,
142
+ 'agent',
143
+ () => assertOperationAllowed('agent', definition.name, requestedCompletion),
144
+ lifecycle,
145
+ ));
146
+ },
147
+ human() {
148
+ assertOperationAllowed('human', definition.name, requestedCompletion);
149
+ throw unsupportedVerb('human');
150
+ },
151
+ dispatch<T>() {
152
+ assertOperationAllowed('dispatch', definition.name, requestedCompletion);
153
+ throw unsupportedVerb('dispatch');
154
+ },
155
+ done(reason) {
156
+ if (!isSurfaceRunCompletionReason(reason)) {
157
+ throw new AuthoredFlowExecutionError(
158
+ 'unsupported_completion',
159
+ `unknown completion reason: ${String(reason)}`,
160
+ );
161
+ }
162
+ if (requestedCompletion !== undefined) {
163
+ throw new AuthoredFlowExecutionError(
164
+ 'duplicate_completion',
165
+ `flow "${definition.name}" called done() more than once`,
166
+ );
167
+ }
168
+ if (reason !== 'success') {
169
+ throw new AuthoredFlowExecutionError(
170
+ 'unsupported_completion',
171
+ `the initial authored executor cannot lower done("${reason}")`,
172
+ reason,
173
+ );
174
+ }
175
+ lifecycle.markCompletion();
176
+ requestedCompletion = reason;
177
+ },
178
+ cloud: unsupportedCloud(
179
+ () => assertOperationAllowed('cloud', definition.name, requestedCompletion),
180
+ ),
181
+ };
182
+
183
+ let bodyFailed = false;
184
+ let bodyFailure: unknown;
185
+ try {
186
+ const bodyPromise = lifecycle.runBody(() => definition.body(context, input as Input));
187
+ await bodyPromise;
188
+ } catch (error) {
189
+ bodyFailed = true;
190
+ bodyFailure = error;
191
+ }
192
+ if (bodyFailed) {
193
+ try {
194
+ await stopAuthoredOperations(authoredSteps, bodyFailure);
195
+ } finally {
196
+ lifecycle.close();
197
+ }
198
+ throw bodyFailure;
199
+ }
200
+ // The completion requirement is checked BEFORE operation verification,
201
+ // because without a completion the verification cannot answer its own
202
+ // question. `AuthoredFlowLifecycle.isHandled` decides whether an operation
203
+ // was consumed by asking whether the COMPLETION depends on it; with no
204
+ // completion there is no async id to trace from, so it returns false for
205
+ // every operation. Verifying first therefore reported correctly-awaited
206
+ // steps as `unawaited_step`, naming the step the author had awaited and
207
+ // saying nothing about the `done()` they forgot (#183).
208
+ //
209
+ // A body that both forgets `done()` and leaves a step unawaited now reports
210
+ // the missing completion. That is the honest order: the unawaited-step
211
+ // verdict is not computable until there is a completion to compute it
212
+ // against, and once the author adds `done()` the verification runs normally
213
+ // and will catch it.
214
+ if (requestedCompletion === undefined) {
215
+ const missingCompletion = new AuthoredFlowExecutionError(
216
+ 'missing_completion',
217
+ `flow "${definition.name}" returned without done()`,
218
+ );
219
+ try {
220
+ await stopAuthoredOperations(authoredSteps, missingCompletion);
221
+ } finally {
222
+ lifecycle.close();
223
+ }
224
+ throw missingCompletion;
225
+ }
226
+ try {
227
+ await verifyAuthoredOperations(definition.name, authoredSteps, lifecycle);
228
+ } finally {
229
+ lifecycle.close();
230
+ }
231
+
232
+ await lowerDeterministic(`complete-${nextStep}`, ':');
233
+ return Object.freeze({
234
+ name: definition.name,
235
+ completionReason: requestedCompletion,
236
+ journalSteps: Object.freeze([...journalSteps]),
237
+ });
238
+ }
239
+
240
+ function trackStep<T>(
241
+ tracked: AuthoredFlowOperation<unknown>[],
242
+ operation: AuthoredFlowOperation<T>,
243
+ ): Step<T> {
244
+ tracked.push(operation as AuthoredFlowOperation<unknown>);
245
+ return operation.step;
246
+ }
247
+
248
+ function unsupportedStep<T>(
249
+ id: string,
250
+ verb: string,
251
+ assertCanStart: () => void,
252
+ lifecycle: AuthoredFlowLifecycle,
253
+ ): AuthoredFlowOperation<T> {
254
+ return new AuthoredFlowOperation<T>(id, verb, assertCanStart, async () => {
255
+ throw unsupportedVerb(verb);
256
+ }, lifecycle);
257
+ }
258
+
259
+ function unsupportedVerb(verb: string): AuthoredFlowExecutionError {
260
+ return new AuthoredFlowExecutionError(
261
+ 'unsupported_verb',
262
+ `the initial authored executor does not lower f.${verb}`,
263
+ );
264
+ }
265
+
266
+ function assertOperationAllowed(
267
+ verb: string,
268
+ flowName: string,
269
+ completion: SurfaceRunCompletionReason | undefined,
270
+ ): void {
271
+ if (completion !== undefined) {
272
+ throw new AuthoredFlowExecutionError(
273
+ 'operation_after_completion',
274
+ `flow "${flowName}" called f.${verb} after done()`,
275
+ completion,
276
+ );
277
+ }
278
+ }
279
+
280
+ function unsupportedCloud(assertOpen: () => void): CloudHelper {
281
+ return new Proxy({}, {
282
+ get() {
283
+ assertOpen();
284
+ throw unsupportedVerb('cloud');
285
+ },
286
+ }) as CloudHelper;
287
+ }
288
+
289
+ async function readSuccessfulOutput(
290
+ journal: JournalClient,
291
+ outcome: RunOutcome,
292
+ stepId: string,
293
+ journalSteps: AuthoredFlowJournalStep[],
294
+ ): Promise<string> {
295
+ const entries = (await journal.journalRead(outcome.run_id, 1)).entries;
296
+ const completed = entries.find((entry) => isStepCompleted(entry, stepId));
297
+ if (!isStepCompleted(completed, stepId)) {
298
+ throw protocolViolation(outcome.run_id, `journal has no step.completed for "${stepId}"`);
299
+ }
300
+
301
+ const reason = completed.payload.completionReason;
302
+ journalSteps.push(Object.freeze({ id: stepId, runId: outcome.run_id, completionReason: reason }));
303
+ if (reason !== 'success') {
304
+ throw new AuthoredFlowExecutionError(
305
+ 'step_failed',
306
+ `journal step "${stepId}" completed with ${reason}`,
307
+ reason,
308
+ outcome.run_id,
309
+ );
310
+ }
311
+ if (outcome.status !== 'completed' || outcome.completion_reason !== 'success') {
312
+ throw protocolViolation(
313
+ outcome.run_id,
314
+ `successful step entry conflicts with run outcome ${outcome.status}/${String(outcome.completion_reason)}`,
315
+ );
316
+ }
317
+
318
+ const output = completed.payload.output;
319
+ if (!isRecord(output) || typeof output['stdout_tail'] !== 'string') {
320
+ throw protocolViolation(outcome.run_id, `step "${stepId}" has no string stdout_tail`);
321
+ }
322
+ return output['stdout_tail'];
323
+ }
324
+
325
+ interface StepCompletedEntry {
326
+ entry_type: 'step.completed';
327
+ step_id: string;
328
+ payload: {
329
+ completionReason: ProtocolCompletionReason;
330
+ output: unknown;
331
+ };
332
+ }
333
+
334
+ function isStepCompleted(value: unknown, stepId: string): value is StepCompletedEntry {
335
+ if (!isRecord(value) || value['entry_type'] !== 'step.completed' || value['step_id'] !== stepId) {
336
+ return false;
337
+ }
338
+ const payload = value['payload'];
339
+ return isRecord(payload)
340
+ && isSurfaceCompletionReason(payload['completionReason'])
341
+ && 'output' in payload;
342
+ }
343
+
344
+ function isSurfaceCompletionReason(value: unknown): value is ProtocolCompletionReason {
345
+ return typeof value === 'string'
346
+ && (COMPLETION_REASONS as readonly string[]).includes(value);
347
+ }
348
+
349
+ function isSurfaceRunCompletionReason(value: unknown): value is ProtocolRunCompletionReason {
350
+ return typeof value === 'string'
351
+ && (RUN_COMPLETION_REASONS as readonly string[]).includes(value);
352
+ }
353
+
354
+ function isRecord(value: unknown): value is Record<string, unknown> {
355
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
356
+ }
357
+
358
+ function protocolViolation(runId: string, detail: string): AuthoredFlowExecutionError {
359
+ return new AuthoredFlowExecutionError(
360
+ 'journal_protocol_violation',
361
+ detail,
362
+ undefined,
363
+ runId,
364
+ );
365
+ }
@@ -0,0 +1,344 @@
1
+ import { AsyncLocalStorage, executionAsyncId } from 'node:async_hooks';
2
+ import { AuthoredFlowExecutionError } from './authored-flow-error.js';
3
+ import { AuthoredPromiseGraph } from './authored-promise-graph.js';
4
+
5
+ type OperationToken = object;
6
+ type ResolverProbe = Set<number>;
7
+ interface PromiseAllGroup {
8
+ aggregate: number;
9
+ members: ReadonlySet<number>;
10
+ }
11
+ export interface AuthoredOperationInvocation {
12
+ readonly asyncId: number;
13
+ bound: boolean;
14
+ }
15
+
16
+ const nativePromiseThen = Promise.prototype.then;
17
+ const activeLifecycle = new AsyncLocalStorage<AuthoredFlowLifecycle>();
18
+ const stepOwners = new WeakMap<object, {
19
+ lifecycle: AuthoredFlowLifecycle;
20
+ operation: OperationToken;
21
+ }>();
22
+
23
+ /**
24
+ * The four intrinsic combinators, intercepted so that an aggregate's membership
25
+ * is recorded exactly.
26
+ *
27
+ * **Why interception, and why all four.** An aggregate is derived from *every*
28
+ * member, but the runtime supplies an edge to only *one* of them: the aggregate
29
+ * is resolved inside the reaction of whichever member settled last (`all`,
30
+ * `allSettled`) or first (`race`, `any`). Inferring membership from that edge is
31
+ * a sufficient rule, never a necessary one, and it fails in both directions —
32
+ * `Promise.allSettled([step, slowerUnrelated])` hid a rejected derived chain
33
+ * behind an aggregate an unrelated promise resolved, and
34
+ * `await Promise.allSettled([a, b])` refused every member except the last to
35
+ * settle. Membership cannot be recovered from the promise graph, so it is
36
+ * recorded here, where the combinator is called and the member list is in hand.
37
+ *
38
+ * A previous revision registered `Promise.all` only, and claimed a
39
+ * resolution-context rule covered "every combinator, present and future". That
40
+ * claim was wrong: it covered whichever member happened to resolve the
41
+ * aggregate. What is true is narrower and is what the code now implements —
42
+ * these four are exact, and `adoptFromResolvingContext` in the promise graph is
43
+ * a best-effort fallback for aggregates built by hand.
44
+ *
45
+ * The interception is disclosed to authors in `docs/SURFACE.md`. It is spec
46
+ * transparent: `Symbol.iterator` is read exactly once (as the intrinsic does),
47
+ * a non-iterable is handed to the intrinsic so it produces the specified
48
+ * rejected promise, `this` is honoured for subclasses, and `name`/`length`
49
+ * match.
50
+ */
51
+ const COMBINATORS = ['all', 'allSettled', 'any', 'race'] as const;
52
+ type CombinatorName = (typeof COMBINATORS)[number];
53
+ type Combinator = (this: PromiseConstructor, values: Iterable<unknown>) => Promise<unknown>;
54
+
55
+ const nativeCombinators = Object.freeze(
56
+ Object.fromEntries(COMBINATORS.map((name) => [name, Promise[name] as unknown as Combinator])),
57
+ ) as Readonly<Record<CombinatorName, Combinator>>;
58
+
59
+ const observedCombinators: Record<CombinatorName, Combinator> = Object.fromEntries(
60
+ COMBINATORS.map((name) => {
61
+ const native = nativeCombinators[name];
62
+ const observed = function (
63
+ this: PromiseConstructor,
64
+ values: Iterable<unknown>,
65
+ ): Promise<unknown> {
66
+ const members = collectMembers(values);
67
+ if (members === undefined) return native.call(this, values);
68
+ const aggregate = native.call(this, members);
69
+ activeLifecycle.getStore()?.registerCombinator(members, aggregate);
70
+ return aggregate;
71
+ };
72
+ Object.defineProperty(observed, 'name', { value: name, configurable: true });
73
+ Object.defineProperty(observed, 'length', { value: 1, configurable: true });
74
+ return [name, observed];
75
+ }),
76
+ ) as Record<CombinatorName, Combinator>;
77
+
78
+ let combinatorObservers = 0;
79
+
80
+ /**
81
+ * Drain an iterable into an array, reading `Symbol.iterator` exactly once.
82
+ *
83
+ * Returns `undefined` when the argument is not iterable or iteration threw, so
84
+ * the caller hands the original value to the intrinsic and the author sees the
85
+ * intrinsic's own behaviour. A previous revision used `isIterable()` followed by
86
+ * `Array.from()`, which invoked a `Symbol.iterator` getter twice where the
87
+ * intrinsic invokes it once.
88
+ */
89
+ function collectMembers(values: Iterable<unknown>): unknown[] | undefined {
90
+ if (values === null || values === undefined) return undefined;
91
+ let iteratorMethod: unknown;
92
+ try {
93
+ iteratorMethod = (values as { [Symbol.iterator]?: unknown })[Symbol.iterator];
94
+ } catch {
95
+ return undefined;
96
+ }
97
+ if (typeof iteratorMethod !== 'function') return undefined;
98
+ try {
99
+ return [...(values as Iterable<unknown>)];
100
+ } catch {
101
+ return undefined;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Runtime proof that a root operation participates in the continuation which
107
+ * reaches done(). Promise resolver identity is never inferred from callback
108
+ * source: a resolver counts only when invoking it settles the promise job that
109
+ * called the operation's then method.
110
+ */
111
+ export class AuthoredFlowLifecycle {
112
+ private readonly graph: AuthoredPromiseGraph;
113
+ private readonly activeResolverProbes: ResolverProbe[] = [];
114
+ private readonly invocations = new Map<OperationToken, AuthoredOperationInvocation[]>();
115
+ private readonly promiseAllAggregates = new Map<OperationToken, Set<number>>();
116
+ private readonly promiseAllGroups: PromiseAllGroup[] = [];
117
+ private readonly callbackFailures = new Map<OperationToken, unknown>();
118
+ private completionAsyncId: number | undefined;
119
+ private closed = false;
120
+
121
+ constructor() {
122
+ this.graph = new AuthoredPromiseGraph(
123
+ () => activeLifecycle.getStore() === this,
124
+ (asyncId) => {
125
+ for (const probe of this.activeResolverProbes) probe.add(asyncId);
126
+ },
127
+ );
128
+ installPromiseAllObserver();
129
+ try {
130
+ this.graph.enable();
131
+ } catch (error) {
132
+ uninstallPromiseAllObserver();
133
+ throw error;
134
+ }
135
+ }
136
+
137
+ runBody<T>(body: () => T): T {
138
+ return activeLifecycle.run(this, body);
139
+ }
140
+
141
+ registerStep(step: object, operation: OperationToken): void {
142
+ stepOwners.set(step, { lifecycle: this, operation });
143
+ }
144
+
145
+ registerCombinator(values: readonly unknown[], aggregate: Promise<unknown>): void {
146
+ const aggregateId = this.graph.idOf(aggregate);
147
+ if (aggregateId === undefined) return;
148
+ this.graph.registerRoot(aggregateId);
149
+ const memberIds = new Set<number>();
150
+ for (const value of values) {
151
+ if ((typeof value !== 'object' && typeof value !== 'function') || value === null) continue;
152
+ const memberId = this.graph.idOf(value);
153
+ if (memberId !== undefined) memberIds.add(memberId);
154
+ const owner = stepOwners.get(value);
155
+ if (owner?.lifecycle !== this) continue;
156
+ let operationAggregates = this.promiseAllAggregates.get(owner.operation);
157
+ if (operationAggregates === undefined) {
158
+ operationAggregates = new Set();
159
+ this.promiseAllAggregates.set(owner.operation, operationAggregates);
160
+ }
161
+ operationAggregates.add(aggregateId);
162
+ }
163
+ this.promiseAllGroups.push({ aggregate: aggregateId, members: memberIds });
164
+ }
165
+
166
+ registerInvocation(
167
+ operation: OperationToken,
168
+ asyncId: number,
169
+ ): AuthoredOperationInvocation {
170
+ const invocation = { asyncId, bound: false };
171
+ const operationInvocations = this.invocations.get(operation);
172
+ if (operationInvocations === undefined) this.invocations.set(operation, [invocation]);
173
+ else operationInvocations.push(invocation);
174
+ this.graph.registerRoot(asyncId);
175
+ return invocation;
176
+ }
177
+
178
+ markCompletion(): void {
179
+ if (activeLifecycle.getStore() !== this) {
180
+ throw new AuthoredFlowExecutionError(
181
+ 'unsupported_promise_lifecycle',
182
+ 'done() escaped its authored flow lifecycle scope',
183
+ );
184
+ }
185
+ this.completionAsyncId = executionAsyncId();
186
+ }
187
+
188
+ invokeResolver<T>(
189
+ invocation: AuthoredOperationInvocation,
190
+ callback: () => T,
191
+ ): T {
192
+ const probe: ResolverProbe = new Set();
193
+ this.activeResolverProbes.push(probe);
194
+ try {
195
+ return callback();
196
+ } finally {
197
+ this.activeResolverProbes.pop();
198
+ if (invocation.asyncId > 0 && probe.has(invocation.asyncId)) {
199
+ invocation.bound = true;
200
+ }
201
+ }
202
+ }
203
+
204
+ hasBoundConsumer(operation: OperationToken): boolean {
205
+ return this.invocations.get(operation)?.some((invocation) => invocation.bound) ?? false;
206
+ }
207
+
208
+ isHandled(operation: OperationToken): boolean {
209
+ const completion = this.completionAsyncId;
210
+ const operationInvocations = this.invocations.get(operation);
211
+ if (completion === undefined || operationInvocations === undefined) return false;
212
+ const aggregates = this.aggregatesFor(operation);
213
+ return operationInvocations.length > 0 && operationInvocations.every((invocation) =>
214
+ invocation.bound && (
215
+ this.graph.dependsOn(completion, invocation.asyncId)
216
+ || [...aggregates].some((aggregate) => this.graph.dependsOn(completion, aggregate))
217
+ ));
218
+ }
219
+
220
+ /**
221
+ * Operations that still had derived work running when the body returned.
222
+ *
223
+ * This is the question the gate used to get wrong. It previously asked which
224
+ * derived failures had *already landed*, and skipped every promise that had
225
+ * not settled — so the same program passed or failed on how many microtask
226
+ * ticks the failure took. Ten `await null`s, or any real derived I/O, cleared
227
+ * the window.
228
+ *
229
+ * Whether derived work is still in flight when the body returns is not a
230
+ * timing fact, it is a causal one: work the author awaited is settled at that
231
+ * instant in every timing, and work the author did not await is pending in
232
+ * every timing. Refusing on pending work therefore closes the race rather
233
+ * than widening it — and it makes the *settled* set complete, so reading the
234
+ * outcomes of the settled promises stops being a sample and becomes a total
235
+ * answer over a closed set.
236
+ *
237
+ * Must be called before the gate awaits anything.
238
+ */
239
+ derivedWorkInFlight<T extends OperationToken>(operations: readonly T[]): T[] {
240
+ return operations.filter((operation) =>
241
+ this.graph.inFlightFrom(this.rootsFor(operation)).length > 0);
242
+ }
243
+
244
+ async observeCallbackFailures(operations: readonly OperationToken[]): Promise<void> {
245
+ const observations: Promise<unknown>[] = [];
246
+ for (const operation of operations) {
247
+ for (const promise of this.graph.settledFrom(this.rootsFor(operation))) {
248
+ observations.push(nativePromiseThen.call(
249
+ promise,
250
+ () => undefined,
251
+ (error: unknown) => { this.recordCallbackFailure(operation, error); },
252
+ ));
253
+ }
254
+ }
255
+ await Promise.all(observations);
256
+ }
257
+
258
+ callbackFailure(operation: OperationToken): {
259
+ readonly recorded: boolean;
260
+ readonly value: unknown;
261
+ } {
262
+ return {
263
+ recorded: this.callbackFailures.has(operation),
264
+ value: this.callbackFailures.get(operation),
265
+ };
266
+ }
267
+
268
+ close(): void {
269
+ if (this.closed) return;
270
+ this.closed = true;
271
+ this.graph.disable();
272
+ this.graph.clear();
273
+ this.invocations.clear();
274
+ this.promiseAllAggregates.clear();
275
+ this.promiseAllGroups.length = 0;
276
+ this.callbackFailures.clear();
277
+ this.activeResolverProbes.length = 0;
278
+ uninstallPromiseAllObserver();
279
+ }
280
+
281
+ private rootsFor(operation: OperationToken): ReadonlySet<number> {
282
+ const roots = new Set(
283
+ (this.invocations.get(operation) ?? [])
284
+ .filter((invocation) => invocation.bound)
285
+ .map((invocation) => invocation.asyncId),
286
+ );
287
+ for (const aggregate of this.aggregatesFor(operation)) roots.add(aggregate);
288
+ return roots;
289
+ }
290
+
291
+ private recordCallbackFailure(operation: OperationToken, error: unknown): void {
292
+ if (!this.callbackFailures.has(operation)) this.callbackFailures.set(operation, error);
293
+ }
294
+
295
+ private aggregatesFor(operation: OperationToken): Set<number> {
296
+ const aggregates = new Set(this.promiseAllAggregates.get(operation) ?? []);
297
+ for (const invocation of this.invocations.get(operation) ?? []) {
298
+ for (const group of this.promiseAllGroups) {
299
+ if (
300
+ group.members.size > 0
301
+ && [...group.members].some((member) => this.graph.dependsOn(member, invocation.asyncId))
302
+ ) {
303
+ aggregates.add(group.aggregate);
304
+ }
305
+ }
306
+ }
307
+ return aggregates;
308
+ }
309
+ }
310
+
311
+ function installPromiseAllObserver(): void {
312
+ if (combinatorObservers === 0) {
313
+ for (const name of COMBINATORS) {
314
+ if (Promise[name] !== (nativeCombinators[name] as unknown)) {
315
+ throw new AuthoredFlowExecutionError(
316
+ 'unsupported_promise_lifecycle',
317
+ `authored flow execution requires the intrinsic Promise.${name}`,
318
+ );
319
+ }
320
+ }
321
+ for (const name of COMBINATORS) {
322
+ (Promise as unknown as Record<string, unknown>)[name] = observedCombinators[name];
323
+ }
324
+ } else {
325
+ for (const name of COMBINATORS) {
326
+ if (Promise[name] !== (observedCombinators[name] as unknown)) {
327
+ throw new AuthoredFlowExecutionError(
328
+ 'unsupported_promise_lifecycle',
329
+ `the authored flow Promise.${name} lifecycle contract was replaced`,
330
+ );
331
+ }
332
+ }
333
+ }
334
+ combinatorObservers += 1;
335
+ }
336
+
337
+ function uninstallPromiseAllObserver(): void {
338
+ combinatorObservers -= 1;
339
+ if (combinatorObservers > 0) return;
340
+ combinatorObservers = 0;
341
+ for (const name of COMBINATORS) {
342
+ (Promise as unknown as Record<string, unknown>)[name] = nativeCombinators[name];
343
+ }
344
+ }