ai-workflows 2.1.3 → 2.3.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 (188) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +8 -1
  3. package/README.md +2 -0
  4. package/dist/barrier.d.ts +6 -0
  5. package/dist/barrier.d.ts.map +1 -1
  6. package/dist/barrier.js +45 -7
  7. package/dist/barrier.js.map +1 -1
  8. package/dist/cascade-context.d.ts.map +1 -1
  9. package/dist/cascade-context.js +25 -25
  10. package/dist/cascade-context.js.map +1 -1
  11. package/dist/cascade-executor.d.ts.map +1 -1
  12. package/dist/cascade-executor.js +1 -1
  13. package/dist/cascade-executor.js.map +1 -1
  14. package/dist/context.d.ts.map +1 -1
  15. package/dist/context.js +23 -7
  16. package/dist/context.js.map +1 -1
  17. package/dist/cron-parser.d.ts +65 -0
  18. package/dist/cron-parser.d.ts.map +1 -0
  19. package/dist/cron-parser.js +294 -0
  20. package/dist/cron-parser.js.map +1 -0
  21. package/dist/cron-scheduler.d.ts +117 -0
  22. package/dist/cron-scheduler.d.ts.map +1 -0
  23. package/dist/cron-scheduler.js +176 -0
  24. package/dist/cron-scheduler.js.map +1 -0
  25. package/dist/database-context.d.ts +184 -0
  26. package/dist/database-context.d.ts.map +1 -0
  27. package/dist/database-context.js +428 -0
  28. package/dist/database-context.js.map +1 -0
  29. package/dist/digital-objects-adapter.d.ts +159 -0
  30. package/dist/digital-objects-adapter.d.ts.map +1 -0
  31. package/dist/digital-objects-adapter.js +229 -0
  32. package/dist/digital-objects-adapter.js.map +1 -0
  33. package/dist/durable-execution-cloudflare.d.ts +427 -0
  34. package/dist/durable-execution-cloudflare.d.ts.map +1 -0
  35. package/dist/durable-execution-cloudflare.js +510 -0
  36. package/dist/durable-execution-cloudflare.js.map +1 -0
  37. package/dist/durable-execution.d.ts +482 -0
  38. package/dist/durable-execution.d.ts.map +1 -0
  39. package/dist/durable-execution.js +594 -0
  40. package/dist/durable-execution.js.map +1 -0
  41. package/dist/durable-workflow.d.ts +176 -0
  42. package/dist/durable-workflow.d.ts.map +1 -0
  43. package/dist/durable-workflow.js +552 -0
  44. package/dist/durable-workflow.js.map +1 -0
  45. package/dist/graph/topological-sort.d.ts.map +1 -1
  46. package/dist/graph/topological-sort.js +5 -5
  47. package/dist/graph/topological-sort.js.map +1 -1
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +15 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/logger.d.ts +101 -0
  53. package/dist/logger.d.ts.map +1 -0
  54. package/dist/logger.js +115 -0
  55. package/dist/logger.js.map +1 -0
  56. package/dist/on.d.ts.map +1 -1
  57. package/dist/on.js +3 -3
  58. package/dist/on.js.map +1 -1
  59. package/dist/runtime.d.ts +169 -0
  60. package/dist/runtime.d.ts.map +1 -0
  61. package/dist/runtime.js +275 -0
  62. package/dist/runtime.js.map +1 -0
  63. package/dist/send.d.ts.map +1 -1
  64. package/dist/send.js +4 -3
  65. package/dist/send.js.map +1 -1
  66. package/dist/telemetry.d.ts +150 -0
  67. package/dist/telemetry.d.ts.map +1 -0
  68. package/dist/telemetry.js +388 -0
  69. package/dist/telemetry.js.map +1 -0
  70. package/dist/timer-registry.d.ts +25 -0
  71. package/dist/timer-registry.d.ts.map +1 -1
  72. package/dist/timer-registry.js +42 -8
  73. package/dist/timer-registry.js.map +1 -1
  74. package/dist/types.d.ts +17 -6
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js +1 -1
  77. package/dist/types.js.map +1 -1
  78. package/dist/worker/durable-step.d.ts +481 -0
  79. package/dist/worker/durable-step.d.ts.map +1 -0
  80. package/dist/worker/durable-step.js +606 -0
  81. package/dist/worker/durable-step.js.map +1 -0
  82. package/dist/worker/index.d.ts +106 -0
  83. package/dist/worker/index.d.ts.map +1 -0
  84. package/dist/worker/index.js +124 -0
  85. package/dist/worker/index.js.map +1 -0
  86. package/dist/worker/state-adapter.d.ts +230 -0
  87. package/dist/worker/state-adapter.d.ts.map +1 -0
  88. package/dist/worker/state-adapter.js +409 -0
  89. package/dist/worker/state-adapter.js.map +1 -0
  90. package/dist/worker/topological-executor.d.ts +282 -0
  91. package/dist/worker/topological-executor.d.ts.map +1 -0
  92. package/dist/worker/topological-executor.js +396 -0
  93. package/dist/worker/topological-executor.js.map +1 -0
  94. package/dist/worker/workflow-builder.d.ts +286 -0
  95. package/dist/worker/workflow-builder.d.ts.map +1 -0
  96. package/dist/worker/workflow-builder.js +565 -0
  97. package/dist/worker/workflow-builder.js.map +1 -0
  98. package/dist/worker.d.ts +800 -0
  99. package/dist/worker.d.ts.map +1 -0
  100. package/dist/worker.js +2428 -0
  101. package/dist/worker.js.map +1 -0
  102. package/dist/workflow-builder.d.ts +287 -0
  103. package/dist/workflow-builder.d.ts.map +1 -0
  104. package/dist/workflow-builder.js +762 -0
  105. package/dist/workflow-builder.js.map +1 -0
  106. package/dist/workflow.d.ts +14 -30
  107. package/dist/workflow.d.ts.map +1 -1
  108. package/dist/workflow.js +132 -292
  109. package/dist/workflow.js.map +1 -1
  110. package/examples/01-ecommerce-order-pipeline.ts +358 -0
  111. package/examples/02-content-moderation-cascade.ts +454 -0
  112. package/examples/03-scheduled-reporting-dependencies.ts +479 -0
  113. package/examples/04-database-persistence.ts +518 -0
  114. package/examples/README.md +173 -0
  115. package/package.json +30 -13
  116. package/src/__tests__/digital-objects-adapter.test.ts +274 -0
  117. package/src/__tests__/durable-workflow.test.ts +297 -0
  118. package/src/barrier.ts +48 -7
  119. package/src/cascade-context.ts +36 -29
  120. package/src/cascade-executor.ts +3 -2
  121. package/src/context.ts +41 -12
  122. package/src/cron-parser.ts +347 -0
  123. package/src/cron-scheduler.ts +239 -0
  124. package/src/database-context.ts +658 -0
  125. package/src/digital-objects-adapter.ts +351 -0
  126. package/src/durable-execution-cloudflare.ts +855 -0
  127. package/src/durable-execution.ts +1042 -0
  128. package/src/durable-workflow.ts +717 -0
  129. package/src/graph/topological-sort.ts +6 -8
  130. package/src/index.ts +69 -0
  131. package/src/logger.ts +148 -0
  132. package/src/on.ts +8 -9
  133. package/src/runtime.ts +436 -0
  134. package/src/send.ts +4 -5
  135. package/src/telemetry.ts +577 -0
  136. package/src/timer-registry.ts +44 -10
  137. package/src/types.ts +32 -17
  138. package/src/worker/durable-step.ts +976 -0
  139. package/src/worker/index.ts +216 -0
  140. package/src/worker/state-adapter.ts +589 -0
  141. package/src/worker/topological-executor.ts +625 -0
  142. package/src/worker/workflow-builder.ts +871 -0
  143. package/src/worker.ts +2906 -0
  144. package/src/workflow-builder.ts +1068 -0
  145. package/src/workflow.ts +188 -351
  146. package/test/barrier-join.test.ts +32 -24
  147. package/test/cascade-executor.test.ts +9 -16
  148. package/test/cron-parser.test.ts +314 -0
  149. package/test/cron-scheduler.test.ts +291 -0
  150. package/test/database-context.test.ts +770 -0
  151. package/test/db-provider-adapter.test.ts +862 -0
  152. package/test/durable-execution-cloudflare.test.ts +606 -0
  153. package/test/durable-execution-in-process.test.ts +286 -0
  154. package/test/durable-execution.test.ts +247 -0
  155. package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
  156. package/test/integration.test.ts +442 -0
  157. package/test/rpc-surface.test.ts +946 -0
  158. package/test/runtime.test.ts +262 -0
  159. package/test/schedule-timer-cleanup.test.ts +30 -21
  160. package/test/send-race-conditions.test.ts +30 -40
  161. package/test/worker/durable-cascade.test.ts +1117 -0
  162. package/test/worker/durable-step.test.ts +723 -0
  163. package/test/worker/topological-executor.test.ts +1240 -0
  164. package/test/worker/workflow-builder.test.ts +1067 -0
  165. package/test/worker.test.ts +608 -0
  166. package/test/workflow-builder.test.ts +1670 -0
  167. package/test/workflow-cron.test.ts +256 -0
  168. package/test/workflow-state-adapter.test.ts +923 -0
  169. package/test/workflow.test.ts +25 -22
  170. package/tsconfig.json +3 -1
  171. package/vitest.config.ts +38 -1
  172. package/vitest.workers.config.ts +44 -0
  173. package/wrangler.jsonc +22 -0
  174. package/.turbo/turbo-test.log +0 -169
  175. package/LICENSE +0 -21
  176. package/src/context.js +0 -83
  177. package/src/every.js +0 -267
  178. package/src/index.js +0 -71
  179. package/src/on.js +0 -79
  180. package/src/send.js +0 -111
  181. package/src/types.js +0 -4
  182. package/src/workflow.js +0 -455
  183. package/test/context.test.js +0 -116
  184. package/test/every.test.js +0 -282
  185. package/test/on.test.js +0 -80
  186. package/test/send.test.js +0 -89
  187. package/test/workflow.test.js +0 -224
  188. package/vitest.config.js +0 -7
@@ -0,0 +1,1042 @@
1
+ /**
2
+ * DurableExecutionAdapter - Port for durable workflow execution backends.
3
+ *
4
+ * Defines a small, backend-agnostic interface that callers (notably
5
+ * `ai-database`'s cascade orchestration in ADR-0003) can program against
6
+ * without knowing which durable execution backend is wired underneath.
7
+ *
8
+ * ## Why this port exists
9
+ *
10
+ * `ai-workflows`'s {@link createWorkflowRuntime} owns the `$` runtime
11
+ * contract — handlers, dispatch, cascade-context, database-context. It runs
12
+ * in-process and lives only as long as the host process. For long-running
13
+ * cascades, scheduled jobs, and orchestration spanning hours-to-days, an
14
+ * external durable execution backend is required.
15
+ *
16
+ * Two production candidates (per ADR-0004):
17
+ *
18
+ * - **Cloudflare Workflows** (default backend; Workers-only). Hibernation
19
+ * while waiting; no per-step billing; 25K steps default; 365-day max
20
+ * sleep. State must flow through step returns — in-memory variables do
21
+ * not survive hibernation.
22
+ *
23
+ * - **Vercel Workflow Development Kit (WDK)** (alternate backend; Vercel
24
+ * or self-hosted). Apache-2.0; pluggable "Worlds"; per-step billing;
25
+ * event-sourced replay; unlimited sleep.
26
+ *
27
+ * Plus an in-process stub (this file's {@link createInMemoryDurableExecution})
28
+ * for tests and local development that does not need durability across
29
+ * process restarts. Phase 1 (bead `aip-fgq5`) will land a richer in-process
30
+ * adapter that bridges this port to the existing
31
+ * {@link createWorkflowRuntime}.
32
+ *
33
+ * The port is modeled after WDK's "Worlds" abstraction — the prior art for
34
+ * exactly this pattern: a small, backend-portable surface for `step`,
35
+ * `sleep`, `waitForEvent`, and scheduling, with each adapter translating to
36
+ * its native primitive (CF's hibernation, WDK's event-sourced replay,
37
+ * in-process's plain async).
38
+ *
39
+ * ## Programming model: Rules of Workflows
40
+ *
41
+ * These rules are universal to durable execution and are not specific to
42
+ * any one backend. They originate in CF's "Rules of Workflows" guidance and
43
+ * apply equally to WDK's deterministic replay model.
44
+ *
45
+ * 1. **Steps must be idempotent.** A step body may be re-executed across
46
+ * a hibernation/restart boundary or after a transient failure. Wrap
47
+ * external side-effects (writes, payments, emails) so a duplicate
48
+ * invocation is observably equivalent to a single invocation.
49
+ *
50
+ * 2. **Step names must be deterministic.** Adapters use the step name as
51
+ * a stable identity for memoization and event-sourced replay. Do not
52
+ * include random ids, timestamps, or run-specific values in the name.
53
+ *
54
+ * 3. **State flows through step returns.** Variables defined inside a
55
+ * workflow body but outside a step are not guaranteed to survive a
56
+ * hibernation boundary. Read inputs at the top of a step; return only
57
+ * what you need; pass results forward through subsequent steps.
58
+ *
59
+ * 4. **Workflow bodies must be deterministic.** Two replays of the same
60
+ * input must take the same control-flow path. Push non-determinism
61
+ * (clocks, randomness, network reads) into steps so the adapter can
62
+ * memoize the result.
63
+ *
64
+ * 5. **Sleeps and waits are first-class.** Use
65
+ * {@link DurableExecutionAdapter.sleep},
66
+ * {@link DurableExecutionAdapter.sleepUntil}, and
67
+ * {@link DurableExecutionAdapter.waitForEvent} rather than `setTimeout`
68
+ * or polling — only the adapter knows how to suspend and resume the
69
+ * workflow correctly.
70
+ *
71
+ * ## Error semantics
72
+ *
73
+ * - {@link DurableStepError} (and its subclasses) are thrown by step
74
+ * execution failures. The {@link DurableStepError.retryable} flag tells
75
+ * the caller whether the adapter considered the failure transient. Real
76
+ * adapters surface their backend's retry decisions through this flag.
77
+ *
78
+ * - {@link WaitForEventTimeoutError} is thrown by
79
+ * {@link DurableExecutionAdapter.waitForEvent} when a timeout elapses
80
+ * without the named event arriving. Callers should catch this explicitly
81
+ * to model "happens within N" branches.
82
+ *
83
+ * - All other thrown errors propagate as-is. Callers must classify
84
+ * non-retryable business errors themselves; adapters do not introspect
85
+ * user errors.
86
+ *
87
+ * ## Subpath export
88
+ *
89
+ * This module ships at the `ai-workflows/durable-execution` subpath so the
90
+ * cascade orchestrator (in `ai-database`) can depend on the port without
91
+ * pulling in the full `ai-workflows` runtime — keeping the static dependency
92
+ * graph one-way (`ai-database` → `ai-workflows/durable-execution`).
93
+ *
94
+ * @example In-memory test usage
95
+ * ```ts
96
+ * import { createInMemoryDurableExecution } from 'ai-workflows/durable-execution'
97
+ *
98
+ * const dx = createInMemoryDurableExecution()
99
+ * const result = await dx.run('charge-customer', async (ctx) => {
100
+ * const charged = await ctx.step('charge-card', async () => {
101
+ * return chargeStripe(ctx.input)
102
+ * })
103
+ * await ctx.sleep('1 second')
104
+ * return { id: charged.id }
105
+ * }, { customerId: 'c-1', amount: 1000 })
106
+ * ```
107
+ *
108
+ * @example Wiring a real adapter (future Phase 1; bead aip-fgq5/aip-i456)
109
+ * ```ts
110
+ * import type { DurableExecutionAdapter } from 'ai-workflows/durable-execution'
111
+ *
112
+ * function makeCascadeRunner(dx: DurableExecutionAdapter) {
113
+ * return (input: CascadeInput) => dx.run('cascade', async (ctx) => {
114
+ * const plan = await ctx.step('plan', () => generatePlan(ctx.input))
115
+ * return ctx.step('write', () => writeAll(plan))
116
+ * }, input)
117
+ * }
118
+ * ```
119
+ *
120
+ * @packageDocumentation
121
+ */
122
+
123
+ import { recordStep } from './cascade-context.js'
124
+ import { getLogger } from './logger.js'
125
+ import type { WorkflowRuntime } from './runtime.js'
126
+
127
+ // =============================================================================
128
+ // Workflow function shape
129
+ // =============================================================================
130
+
131
+ /**
132
+ * Context passed to a {@link WorkflowFn}. Provides the same step / sleep /
133
+ * wait primitives as the top-level {@link DurableExecutionAdapter}, plus the
134
+ * input the workflow was invoked with.
135
+ *
136
+ * The context is bound to one workflow run; calls on it are routed to the
137
+ * adapter that started the run.
138
+ */
139
+ export interface WorkflowContext<TInput = unknown> {
140
+ /** The input value the workflow was invoked with. */
141
+ readonly input: TInput
142
+
143
+ /** Stable instance id assigned by the adapter at run start. */
144
+ readonly instanceId: string
145
+
146
+ /** Logical workflow name supplied at {@link DurableExecutionAdapter.run}. */
147
+ readonly name: string
148
+
149
+ /**
150
+ * Execute a named, durable step. See
151
+ * {@link DurableExecutionAdapter.step} for semantics.
152
+ */
153
+ step<T>(name: string, fn: () => Promise<T>): Promise<T>
154
+
155
+ /**
156
+ * Execute a named, durable step with explicit configuration. See
157
+ * {@link DurableExecutionAdapter.step}.
158
+ */
159
+ step<T>(name: string, config: StepConfig, fn: () => Promise<T>): Promise<T>
160
+
161
+ /** Suspend the workflow for {@link DurableExecutionAdapter.sleep}. */
162
+ sleep(duration: string | number): Promise<void>
163
+
164
+ /** Suspend until {@link DurableExecutionAdapter.sleepUntil}. */
165
+ sleepUntil(date: Date): Promise<void>
166
+
167
+ /** Wait for a named event. See {@link DurableExecutionAdapter.waitForEvent}. */
168
+ waitForEvent<T = unknown>(name: string, timeout?: number | string): Promise<T>
169
+ }
170
+
171
+ /**
172
+ * A user-authored workflow body.
173
+ *
174
+ * The body must be deterministic across replays — see the "Rules of
175
+ * Workflows" in the module-level docs. The function is invoked once per
176
+ * run-or-replay; the adapter memoizes step results by step name so repeated
177
+ * executions converge on the same outcome.
178
+ */
179
+ export type WorkflowFn<TResult = unknown, TInput = unknown> = (
180
+ ctx: WorkflowContext<TInput>
181
+ ) => Promise<TResult>
182
+
183
+ // =============================================================================
184
+ // Step configuration
185
+ // =============================================================================
186
+
187
+ /**
188
+ * Per-step configuration. Adapters that do not support a given option treat
189
+ * it as advisory. CF Workflows and WDK both support timeout and retries;
190
+ * the in-memory stub honours retries and ignores timeout (since steps are
191
+ * synchronous from the adapter's perspective).
192
+ */
193
+ export interface StepConfig {
194
+ /** Maximum total attempts including the first. Default: adapter-specific (typically 1). */
195
+ retries?: {
196
+ /** Total attempts (>= 1). */
197
+ limit: number
198
+ /** Delay between attempts. String forms: '1 second', '500ms'. */
199
+ delay?: string | number
200
+ /** Backoff curve. Default: 'constant'. */
201
+ backoff?: 'constant' | 'linear' | 'exponential'
202
+ }
203
+ /** Per-attempt timeout. String or ms. */
204
+ timeout?: string | number
205
+ }
206
+
207
+ // =============================================================================
208
+ // Subscription / scheduling
209
+ // =============================================================================
210
+
211
+ /**
212
+ * Returned by {@link DurableExecutionAdapter.schedule}. Calling
213
+ * {@link Subscription.unsubscribe} cancels the schedule; idempotent.
214
+ */
215
+ export interface Subscription {
216
+ /** Stable id for the schedule (adapter-defined format). */
217
+ readonly id: string
218
+ /** Cancel the schedule. Safe to call more than once. */
219
+ unsubscribe(): void
220
+ }
221
+
222
+ // =============================================================================
223
+ // Error types
224
+ // =============================================================================
225
+
226
+ /**
227
+ * Thrown when a step's body fails after all retries (or immediately, when
228
+ * retries are not configured).
229
+ *
230
+ * The {@link DurableStepError.retryable} flag reflects the adapter's
231
+ * judgement about the underlying failure: `true` means the adapter believed
232
+ * the failure was transient (network, rate-limit, server error) and would
233
+ * have retried again given more attempts; `false` means the adapter
234
+ * classified it as terminal (programmer error, invalid input). Callers can
235
+ * use this to decide whether to escalate or fall through to a different
236
+ * code path.
237
+ */
238
+ export class DurableStepError extends Error {
239
+ /** Step name that failed. */
240
+ readonly stepName: string
241
+ /** Number of attempts the adapter actually made (>= 1). */
242
+ readonly attempts: number
243
+ /** Whether the adapter considered the cause transient. */
244
+ readonly retryable: boolean
245
+ /** The error that ultimately caused the failure. */
246
+ override readonly cause: unknown
247
+
248
+ constructor(
249
+ message: string,
250
+ options: {
251
+ stepName: string
252
+ attempts: number
253
+ retryable: boolean
254
+ cause: unknown
255
+ }
256
+ ) {
257
+ super(message)
258
+ this.name = 'DurableStepError'
259
+ this.stepName = options.stepName
260
+ this.attempts = options.attempts
261
+ this.retryable = options.retryable
262
+ this.cause = options.cause
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Thrown when {@link DurableExecutionAdapter.waitForEvent} elapses without
268
+ * the named event arriving.
269
+ */
270
+ export class WaitForEventTimeoutError extends Error {
271
+ readonly eventName: string
272
+ readonly timeout: number | string
273
+ constructor(eventName: string, timeout: number | string) {
274
+ super(`Timed out waiting for event "${eventName}" after ${String(timeout)}`)
275
+ this.name = 'WaitForEventTimeoutError'
276
+ this.eventName = eventName
277
+ this.timeout = timeout
278
+ }
279
+ }
280
+
281
+ // =============================================================================
282
+ // The port
283
+ // =============================================================================
284
+
285
+ /**
286
+ * Concrete adapter kind. Used only as a discriminant for callers that want
287
+ * to log or branch on the active backend; should never be relied on for
288
+ * correctness.
289
+ */
290
+ export type DurableExecutionKind = 'in-process' | 'cloudflare' | 'vercel-wdk'
291
+
292
+ /**
293
+ * The port. A small, backend-agnostic interface over `run`, `step`, `sleep`,
294
+ * `waitForEvent`, and `schedule`.
295
+ *
296
+ * Real adapters live alongside this file but ship behind feature flags or
297
+ * separate subpath modules so importing the port itself does not pull in
298
+ * any backend dependencies.
299
+ */
300
+ export interface DurableExecutionAdapter {
301
+ /** Discriminant tag for the active backend. */
302
+ readonly kind: DurableExecutionKind
303
+
304
+ /**
305
+ * Run a workflow body to completion. The adapter assigns the run a
306
+ * stable `instanceId`, persists it (where applicable), and invokes `fn`
307
+ * with a {@link WorkflowContext}.
308
+ *
309
+ * Resolves with the workflow's return value. Rejects with whatever the
310
+ * body or its steps reject with — callers must handle both
311
+ * {@link DurableStepError} and arbitrary user errors.
312
+ *
313
+ * @param name Logical name of the workflow (used for telemetry and, in
314
+ * some backends, for routing). Should be stable across deployments.
315
+ * @param fn The deterministic workflow body. See module docs for rules.
316
+ * @param input Arbitrary input value passed through to `ctx.input`. Must
317
+ * be JSON-serializable for adapters that persist state (CF, WDK).
318
+ */
319
+ run<TResult = unknown, TInput = unknown>(
320
+ name: string,
321
+ fn: WorkflowFn<TResult, TInput>,
322
+ input: TInput
323
+ ): Promise<TResult>
324
+
325
+ /**
326
+ * Execute a named, idempotent step.
327
+ *
328
+ * Step names must be stable across replays — the adapter uses the name
329
+ * as the memoization key. On replay, a step that has already succeeded
330
+ * returns its memoized result without re-invoking `fn`. On a transient
331
+ * failure, the adapter retries per {@link StepConfig.retries} (or its
332
+ * default policy).
333
+ *
334
+ * Outside of `run()` (e.g. when the adapter is called directly without a
335
+ * surrounding workflow body) implementations may either: (a) treat
336
+ * `step` as a thin wrapper that calls `fn` once with no replay, or (b)
337
+ * throw. The in-memory stub picks (a) because it is convenient for tests.
338
+ *
339
+ * Step bodies must be idempotent — see the Rules of Workflows.
340
+ */
341
+ step<T>(name: string, fn: () => Promise<T>): Promise<T>
342
+ step<T>(name: string, config: StepConfig, fn: () => Promise<T>): Promise<T>
343
+
344
+ /**
345
+ * Suspend execution for a duration. The adapter's backend may hibernate
346
+ * the workflow during the sleep window; in-memory the stub merely awaits.
347
+ *
348
+ * @param duration Number of milliseconds, or a human string accepted by
349
+ * the backend (e.g. `'1 second'`, `'5 minutes'`, `'1 day'`). Adapters
350
+ * that do not understand a string form must throw.
351
+ */
352
+ sleep(duration: string | number): Promise<void>
353
+
354
+ /** Suspend execution until the given absolute timestamp. */
355
+ sleepUntil(date: Date): Promise<void>
356
+
357
+ /**
358
+ * Wait for an externally-signalled event by name.
359
+ *
360
+ * @param name Event name. Convention: `Noun.event` (matching
361
+ * `ai-workflows`'s dispatch shape) but adapters do not enforce this.
362
+ * @param timeout Optional max wait. Number = milliseconds. String = a
363
+ * backend-recognised duration. When `undefined` the adapter waits
364
+ * forever (or until its hard backend limit, e.g. 365 days for CF).
365
+ *
366
+ * Rejects with {@link WaitForEventTimeoutError} if the timeout elapses.
367
+ */
368
+ waitForEvent<T = unknown>(name: string, timeout?: number | string): Promise<T>
369
+
370
+ /**
371
+ * Register a workflow body to run on a cron schedule.
372
+ *
373
+ * The schedule itself is durable — the adapter is responsible for firing
374
+ * at the given cron expression even across process restarts. The
375
+ * returned {@link Subscription} cancels the schedule when unsubscribed.
376
+ *
377
+ * Fired runs receive an empty input (`undefined`). To pass per-run data,
378
+ * read it from external state inside the workflow body.
379
+ */
380
+ schedule<TResult = unknown>(
381
+ name: string,
382
+ cron: string,
383
+ fn: WorkflowFn<TResult, undefined>
384
+ ): Subscription
385
+ }
386
+
387
+ // =============================================================================
388
+ // In-memory stub for tests
389
+ // =============================================================================
390
+
391
+ /**
392
+ * Options for {@link createInMemoryDurableExecution}.
393
+ */
394
+ export interface InMemoryDurableExecutionOptions {
395
+ /**
396
+ * Override the clock. Used by tests to control sleep without waiting in
397
+ * real time. When provided, `sleep`/`sleepUntil` resolve as soon as the
398
+ * clock returns a value `>= deadline`.
399
+ */
400
+ now?: () => number
401
+
402
+ /**
403
+ * Override the sleep primitive. Defaults to a real `setTimeout`. Tests
404
+ * that want to fast-forward time can replace this with a no-op.
405
+ */
406
+ delay?: (ms: number) => Promise<void>
407
+ }
408
+
409
+ /**
410
+ * Internal: parse human-readable durations like `'1 second'`, `'500ms'`,
411
+ * `'5 minutes'` into milliseconds. The supported grammar is small on
412
+ * purpose; real backends define their own. Numbers pass through.
413
+ */
414
+ function parseDuration(input: string | number): number {
415
+ if (typeof input === 'number') return input
416
+ const trimmed = input.trim().toLowerCase()
417
+ const match = trimmed.match(
418
+ /^(\d+(?:\.\d+)?)\s*(ms|millisecond|milliseconds|s|sec|second|seconds|m|min|minute|minutes|h|hr|hour|hours|d|day|days)$/
419
+ )
420
+ if (!match) {
421
+ throw new Error(`Unrecognised duration: "${input}"`)
422
+ }
423
+ const value = parseFloat(match[1]!)
424
+ const unit = match[2]!
425
+ switch (unit) {
426
+ case 'ms':
427
+ case 'millisecond':
428
+ case 'milliseconds':
429
+ return value
430
+ case 's':
431
+ case 'sec':
432
+ case 'second':
433
+ case 'seconds':
434
+ return value * 1000
435
+ case 'm':
436
+ case 'min':
437
+ case 'minute':
438
+ case 'minutes':
439
+ return value * 60_000
440
+ case 'h':
441
+ case 'hr':
442
+ case 'hour':
443
+ case 'hours':
444
+ return value * 3_600_000
445
+ case 'd':
446
+ case 'day':
447
+ case 'days':
448
+ return value * 86_400_000
449
+ /* istanbul ignore next */
450
+ default:
451
+ throw new Error(`Unrecognised duration unit: "${unit}"`)
452
+ }
453
+ }
454
+
455
+ /** Internal: compute backoff delay for retry attempt n (1-based). */
456
+ function computeBackoff(config: NonNullable<StepConfig['retries']>, attempt: number): number {
457
+ const baseMs = config.delay !== undefined ? parseDuration(config.delay) : 0
458
+ const strategy = config.backoff ?? 'constant'
459
+ switch (strategy) {
460
+ case 'constant':
461
+ return baseMs
462
+ case 'linear':
463
+ return baseMs * attempt
464
+ case 'exponential':
465
+ return baseMs * Math.pow(2, attempt - 1)
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Construct an in-memory {@link DurableExecutionAdapter} suitable for tests
471
+ * and validating the port shape. **Not durable** — state lives only in
472
+ * closure for the lifetime of the process.
473
+ *
474
+ * Behaviour:
475
+ *
476
+ * - `run`: invokes the body once with a fresh context. No replay.
477
+ * - `step`: memoizes the result by step name within a single `run`
478
+ * invocation; outside a `run` it executes once and returns. Honours
479
+ * retries (constant/linear/exponential backoff).
480
+ * - `sleep` / `sleepUntil`: awaits real time by default, overridable via
481
+ * {@link InMemoryDurableExecutionOptions.delay}.
482
+ * - `waitForEvent`: pairs with {@link InMemoryDurableExecution.emit} on the
483
+ * returned object. If `timeout` elapses first, rejects with
484
+ * {@link WaitForEventTimeoutError}.
485
+ * - `schedule`: installs a `setInterval` that fires the workflow body each
486
+ * `cron` tick. The cron expression itself is **not** parsed in the stub —
487
+ * it is treated as opaque metadata. Tests that want time-driven schedules
488
+ * should drive emissions manually via the returned helpers.
489
+ *
490
+ * The stub satisfies the full {@link DurableExecutionAdapter} surface so
491
+ * tests can validate the port shape without writing a real adapter.
492
+ */
493
+ export interface InMemoryDurableExecution extends DurableExecutionAdapter {
494
+ /**
495
+ * Manually deliver an event to the next pending {@link waitForEvent}
496
+ * call with the matching name. Returns `true` if a waiter was resolved,
497
+ * `false` otherwise.
498
+ */
499
+ emit<T = unknown>(name: string, value: T): boolean
500
+ }
501
+
502
+ interface PendingWaiter {
503
+ resolve: (value: unknown) => void
504
+ reject: (reason: unknown) => void
505
+ }
506
+
507
+ /**
508
+ * @see InMemoryDurableExecution
509
+ */
510
+ export function createInMemoryDurableExecution(
511
+ options: InMemoryDurableExecutionOptions = {}
512
+ ): InMemoryDurableExecution {
513
+ const now = options.now ?? (() => Date.now())
514
+ const delay =
515
+ options.delay ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))
516
+
517
+ // Per-run memoization is keyed by the surrounding run's instanceId. Calls
518
+ // outside of a run get an `undefined` key (single shared bucket).
519
+ const runMemo = new Map<string | undefined, Map<string, unknown>>()
520
+
521
+ // Active event waiters, FIFO per event name.
522
+ const waiters = new Map<string, PendingWaiter[]>()
523
+
524
+ // Active scheduled intervals, by subscription id.
525
+ const schedules = new Map<string, ReturnType<typeof setInterval>>()
526
+ let scheduleSeq = 0
527
+ let runSeq = 0
528
+
529
+ // Tracks the active run so step() can attribute results to the right
530
+ // memo bucket. We only support a single concurrent run for memo purposes;
531
+ // concurrent runs do not share state regardless.
532
+ type RunFrame = { instanceId: string }
533
+ const runStack: RunFrame[] = []
534
+ const currentRun = (): RunFrame | undefined => runStack[runStack.length - 1]
535
+
536
+ async function executeStep<T>(
537
+ name: string,
538
+ config: StepConfig | undefined,
539
+ fn: () => Promise<T>
540
+ ): Promise<T> {
541
+ const frame = currentRun()
542
+ const bucketKey = frame?.instanceId
543
+ let bucket = runMemo.get(bucketKey)
544
+ if (!bucket) {
545
+ bucket = new Map()
546
+ runMemo.set(bucketKey, bucket)
547
+ }
548
+ if (bucket.has(name)) {
549
+ return bucket.get(name) as T
550
+ }
551
+
552
+ const retries = config?.retries
553
+ const maxAttempts = retries?.limit ?? 1
554
+ let lastErr: unknown
555
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
556
+ try {
557
+ const result = await fn()
558
+ bucket.set(name, result)
559
+ return result
560
+ } catch (err) {
561
+ lastErr = err
562
+ if (attempt < maxAttempts && retries) {
563
+ const wait = computeBackoff(retries, attempt)
564
+ if (wait > 0) await delay(wait)
565
+ }
566
+ }
567
+ }
568
+ throw new DurableStepError(`Step "${name}" failed after ${maxAttempts} attempt(s)`, {
569
+ stepName: name,
570
+ attempts: maxAttempts,
571
+ retryable: true,
572
+ cause: lastErr,
573
+ })
574
+ }
575
+
576
+ async function sleep(duration: string | number): Promise<void> {
577
+ const ms = parseDuration(duration)
578
+ if (ms > 0) await delay(ms)
579
+ }
580
+
581
+ async function sleepUntil(date: Date): Promise<void> {
582
+ const ms = date.getTime() - now()
583
+ if (ms > 0) await delay(ms)
584
+ }
585
+
586
+ function waitForEvent<T = unknown>(name: string, timeout?: number | string): Promise<T> {
587
+ return new Promise<T>((resolve, reject) => {
588
+ const queue = waiters.get(name) ?? []
589
+ const waiter: PendingWaiter = {
590
+ resolve: (v) => resolve(v as T),
591
+ reject,
592
+ }
593
+ queue.push(waiter)
594
+ waiters.set(name, queue)
595
+
596
+ if (timeout !== undefined) {
597
+ const ms = parseDuration(timeout)
598
+ const timer = setTimeout(() => {
599
+ const q = waiters.get(name)
600
+ if (!q) return
601
+ const idx = q.indexOf(waiter)
602
+ if (idx >= 0) {
603
+ q.splice(idx, 1)
604
+ if (q.length === 0) waiters.delete(name)
605
+ reject(new WaitForEventTimeoutError(name, timeout))
606
+ }
607
+ }, ms)
608
+ // Don't keep the process alive solely for a wait timeout.
609
+ if (typeof (timer as { unref?: () => void }).unref === 'function') {
610
+ ;(timer as { unref?: () => void }).unref!()
611
+ }
612
+ }
613
+ })
614
+ }
615
+
616
+ function emit<T = unknown>(name: string, value: T): boolean {
617
+ const queue = waiters.get(name)
618
+ if (!queue || queue.length === 0) return false
619
+ const waiter = queue.shift()!
620
+ if (queue.length === 0) waiters.delete(name)
621
+ waiter.resolve(value)
622
+ return true
623
+ }
624
+
625
+ function schedule<TResult = unknown>(
626
+ name: string,
627
+ cron: string,
628
+ fn: WorkflowFn<TResult, undefined>
629
+ ): Subscription {
630
+ const id = `sched-${++scheduleSeq}`
631
+ // The stub does not parse cron — it fires every second. Tests that
632
+ // need precise timing should drive runs manually rather than rely on
633
+ // the stub's scheduler. Real adapters honour the cron expression.
634
+ void cron
635
+ void name
636
+ const handle = setInterval(() => {
637
+ void adapter.run(name, fn, undefined).catch(() => {
638
+ // Schedule errors are swallowed; the stub has nowhere to surface
639
+ // them. Real adapters log/persist failures.
640
+ })
641
+ }, 1000)
642
+ if (typeof (handle as unknown as { unref?: () => void }).unref === 'function') {
643
+ ;(handle as unknown as { unref: () => void }).unref()
644
+ }
645
+ schedules.set(id, handle)
646
+ return {
647
+ id,
648
+ unsubscribe(): void {
649
+ const h = schedules.get(id)
650
+ if (h) {
651
+ clearInterval(h)
652
+ schedules.delete(id)
653
+ }
654
+ },
655
+ }
656
+ }
657
+
658
+ async function run<TResult = unknown, TInput = unknown>(
659
+ name: string,
660
+ fn: WorkflowFn<TResult, TInput>,
661
+ input: TInput
662
+ ): Promise<TResult> {
663
+ const instanceId = `run-${++runSeq}`
664
+ const frame: RunFrame = { instanceId }
665
+ runStack.push(frame)
666
+ const ctx: WorkflowContext<TInput> = {
667
+ input,
668
+ instanceId,
669
+ name,
670
+ step: ((
671
+ stepName: string,
672
+ configOrFn: StepConfig | (() => Promise<unknown>),
673
+ maybeFn?: () => Promise<unknown>
674
+ ) => {
675
+ if (typeof configOrFn === 'function') {
676
+ return executeStep(stepName, undefined, configOrFn) as Promise<unknown>
677
+ }
678
+ return executeStep(stepName, configOrFn, maybeFn!) as Promise<unknown>
679
+ }) as WorkflowContext<TInput>['step'],
680
+ sleep,
681
+ sleepUntil,
682
+ waitForEvent,
683
+ }
684
+ try {
685
+ return await fn(ctx)
686
+ } finally {
687
+ runStack.pop()
688
+ runMemo.delete(instanceId)
689
+ }
690
+ }
691
+
692
+ const adapter: InMemoryDurableExecution = {
693
+ kind: 'in-process',
694
+ run,
695
+ step: ((
696
+ name: string,
697
+ configOrFn: StepConfig | (() => Promise<unknown>),
698
+ maybeFn?: () => Promise<unknown>
699
+ ) => {
700
+ if (typeof configOrFn === 'function') {
701
+ return executeStep(name, undefined, configOrFn) as Promise<unknown>
702
+ }
703
+ return executeStep(name, configOrFn, maybeFn!) as Promise<unknown>
704
+ }) as DurableExecutionAdapter['step'],
705
+ sleep,
706
+ sleepUntil,
707
+ waitForEvent,
708
+ schedule,
709
+ emit,
710
+ }
711
+
712
+ return adapter
713
+ }
714
+
715
+ // =============================================================================
716
+ // In-process production adapter (wraps WorkflowRuntime)
717
+ // =============================================================================
718
+
719
+ /**
720
+ * Options for {@link createInProcessDurableExecution}.
721
+ *
722
+ * When a `runtime` is supplied the adapter records each durable run/step into
723
+ * the runtime's cascade context, history, and (when configured) database. When
724
+ * no runtime is supplied the adapter behaves identically to
725
+ * {@link createInMemoryDurableExecution} but uses production logging instead of
726
+ * silently swallowing schedule errors — making it suitable for local dev and
727
+ * single-process callers that want durable-execution semantics without
728
+ * crash-recovery.
729
+ *
730
+ * `now` and `delay` are forwarded to the underlying machinery for tests that
731
+ * need to fast-forward time. Real callers should leave them at the defaults.
732
+ */
733
+ export interface InProcessDurableExecutionOptions extends InMemoryDurableExecutionOptions {
734
+ /**
735
+ * Optional {@link WorkflowRuntime} to integrate with. When supplied:
736
+ *
737
+ * - Each durable `run` pushes an `event`-typed history entry on start and
738
+ * completion through `runtime.state.history`.
739
+ * - Each durable `step` records a {@link CascadeStep} on
740
+ * `runtime.cascade` via {@link recordStep}; the step is marked
741
+ * completed/failed when the body resolves/rejects.
742
+ * - When `runtime.$.db` is present, each durable step creates an action
743
+ * via `db.createAction({ actor: 'workflow', object: name, action: 'step' })`
744
+ * before invoking the body, and completes (or records failure) on resolution.
745
+ * - The workflow context handed to the body exposes the runtime's `$` as
746
+ * {@link InProcessWorkflowContext.runtime} so handlers can dispatch
747
+ * events through the same registry the rest of the app uses.
748
+ *
749
+ * Pass `null`/omitted to opt out — the adapter still satisfies the port and
750
+ * runs durably in-process, just without the runtime integration.
751
+ */
752
+ runtime?: WorkflowRuntime
753
+ }
754
+
755
+ /**
756
+ * Workflow context handed to bodies invoked through
757
+ * {@link createInProcessDurableExecution}. Identical to {@link WorkflowContext}
758
+ * but with an optional `runtime` reference exposing the wrapping
759
+ * {@link WorkflowRuntime} (when one was supplied at construction time).
760
+ *
761
+ * `runtime` is `undefined` when the adapter was constructed without a runtime.
762
+ */
763
+ export interface InProcessWorkflowContext<TInput = unknown> extends WorkflowContext<TInput> {
764
+ /** The `WorkflowRuntime` wrapped by the adapter, if any. */
765
+ readonly runtime?: WorkflowRuntime
766
+ }
767
+
768
+ /**
769
+ * The adapter returned by {@link createInProcessDurableExecution}.
770
+ *
771
+ * Extends {@link DurableExecutionAdapter} with `emit` (for paired
772
+ * `waitForEvent` testing) and exposes the wrapped runtime when one was
773
+ * supplied. The `runtime` reference is the same object passed in at
774
+ * construction; callers can use it to register handlers, inspect history, or
775
+ * read cascade traces emitted during durable runs.
776
+ */
777
+ export interface InProcessDurableExecution extends DurableExecutionAdapter {
778
+ /**
779
+ * Manually deliver an event to the next pending {@link waitForEvent} call
780
+ * with the matching name. Mirrors {@link InMemoryDurableExecution.emit}.
781
+ */
782
+ emit<T = unknown>(name: string, value: T): boolean
783
+ /** The runtime wrapped by the adapter, if one was supplied. */
784
+ readonly runtime?: WorkflowRuntime
785
+ }
786
+
787
+ /**
788
+ * Construct the production in-process {@link DurableExecutionAdapter}.
789
+ *
790
+ * Differs from {@link createInMemoryDurableExecution} in three ways:
791
+ *
792
+ * 1. **Runtime integration.** When a {@link WorkflowRuntime} is supplied,
793
+ * each run/step is reflected in the runtime's cascade context, history,
794
+ * and (when `runtime.$.db` is wired) database adapter — making durable
795
+ * runs observable through the same surfaces as event-driven dispatch.
796
+ *
797
+ * 2. **Production-shape logging.** Schedule failures and step failures with
798
+ * a configured runtime are routed through the package logger
799
+ * ({@link getLogger}). The in-memory stub silently swallows these because
800
+ * tests do not want noise in the console.
801
+ *
802
+ * 3. **Workflow context exposes the runtime.** Bodies receive an
803
+ * {@link InProcessWorkflowContext} with `ctx.runtime` set, so durable
804
+ * workflows can dispatch events through the same `$.on`/`$.send`
805
+ * registry the rest of the app uses.
806
+ *
807
+ * The adapter is *not* durable across process restarts — that is the
808
+ * Cloudflare Workflows adapter's job (separate bead `aip-i456`). It exists
809
+ * for test environments, local development, and single-process callers that
810
+ * want the port semantics without a real backend.
811
+ *
812
+ * @example No runtime — equivalent to the in-memory stub but production-shape
813
+ * ```ts
814
+ * const dx = createInProcessDurableExecution()
815
+ * await dx.run('hello', async (ctx) => ctx.input.name, { name: 'world' })
816
+ * ```
817
+ *
818
+ * @example With runtime — durable runs participate in the `$` ecosystem
819
+ * ```ts
820
+ * const runtime = createWorkflowRuntime({ db: myDb })
821
+ * const dx = createInProcessDurableExecution({ runtime })
822
+ *
823
+ * await dx.run('cascade', async (ctx) => {
824
+ * const plan = await ctx.step('plan', async () => generatePlan(ctx.input))
825
+ * // The runtime's $.send delivers to handlers registered on the same runtime.
826
+ * ctx.runtime!.$.send('Plan.generated', plan)
827
+ * return plan
828
+ * }, { customerId: 'c-1' })
829
+ *
830
+ * // Cascade trace, history, and db actions all reflect the run.
831
+ * console.log(runtime.cascade.steps.length) // >= 1 (the 'plan' step)
832
+ * ```
833
+ */
834
+ export function createInProcessDurableExecution(
835
+ options: InProcessDurableExecutionOptions = {}
836
+ ): InProcessDurableExecution {
837
+ // The in-process adapter delegates step/sleep/wait/schedule mechanics to
838
+ // the in-memory factory and wraps run/step with runtime-integration hooks.
839
+ // This keeps the two factories from drifting in their core behaviour and
840
+ // lets the in-memory stub remain the canonical reference for the port's
841
+ // baseline semantics.
842
+ const inner = createInMemoryDurableExecution({
843
+ ...(options.now !== undefined && { now: options.now }),
844
+ ...(options.delay !== undefined && { delay: options.delay }),
845
+ })
846
+ const runtime = options.runtime
847
+
848
+ function recordRunHistory(
849
+ name: string,
850
+ phase: 'start' | 'finish' | 'error',
851
+ data?: unknown
852
+ ): void {
853
+ if (!runtime) return
854
+ runtime.state.history.push({
855
+ timestamp: Date.now(),
856
+ type: 'event',
857
+ name: `durable-run:${phase}:${name}`,
858
+ ...(data !== undefined && { data }),
859
+ })
860
+ }
861
+
862
+ async function instrumentedStep<T>(
863
+ stepName: string,
864
+ config: StepConfig | undefined,
865
+ fn: () => Promise<T>,
866
+ innerStep: DurableExecutionAdapter['step']
867
+ ): Promise<T> {
868
+ if (!runtime) {
869
+ // No runtime — defer to the inner mechanics unchanged.
870
+ return config !== undefined ? innerStep<T>(stepName, config, fn) : innerStep<T>(stepName, fn)
871
+ }
872
+
873
+ const cascadeStep = recordStep(runtime.cascade, stepName)
874
+ const db = runtime.$.db
875
+ let actionRecorded = false
876
+ if (db) {
877
+ try {
878
+ await db.createAction({
879
+ actor: 'workflow',
880
+ object: stepName,
881
+ action: 'step',
882
+ status: 'active',
883
+ })
884
+ actionRecorded = true
885
+ } catch (err) {
886
+ // Do not fail the step because action recording failed; surface the
887
+ // error through the package logger so it is observable.
888
+ getLogger().error(
889
+ `[durable-execution] Failed to record action for step "${stepName}":`,
890
+ err
891
+ )
892
+ }
893
+ }
894
+
895
+ const wrapped = async (): Promise<T> => fn()
896
+ try {
897
+ const result = await (config !== undefined
898
+ ? innerStep<T>(stepName, config, wrapped)
899
+ : innerStep<T>(stepName, wrapped))
900
+ cascadeStep.complete()
901
+ if (actionRecorded && db) {
902
+ try {
903
+ await db.completeAction(stepName, result as unknown)
904
+ } catch (err) {
905
+ getLogger().error(
906
+ `[durable-execution] Failed to complete action for step "${stepName}":`,
907
+ err
908
+ )
909
+ }
910
+ }
911
+ return result
912
+ } catch (err) {
913
+ cascadeStep.fail(err instanceof Error ? err : new Error(String(err)))
914
+ getLogger().error(`[durable-execution] Step "${stepName}" failed:`, err)
915
+ throw err
916
+ }
917
+ }
918
+
919
+ async function run<TResult = unknown, TInput = unknown>(
920
+ name: string,
921
+ fn: (ctx: InProcessWorkflowContext<TInput>) => Promise<TResult>,
922
+ input: TInput
923
+ ): Promise<TResult> {
924
+ recordRunHistory(name, 'start', { input })
925
+ try {
926
+ const result = await inner.run<TResult, TInput>(
927
+ name,
928
+ async (innerCtx) => {
929
+ const ctx: InProcessWorkflowContext<TInput> = {
930
+ input: innerCtx.input,
931
+ instanceId: innerCtx.instanceId,
932
+ name: innerCtx.name,
933
+ sleep: innerCtx.sleep,
934
+ sleepUntil: innerCtx.sleepUntil,
935
+ waitForEvent: innerCtx.waitForEvent,
936
+ step: ((
937
+ stepName: string,
938
+ configOrFn: StepConfig | (() => Promise<unknown>),
939
+ maybeFn?: () => Promise<unknown>
940
+ ) => {
941
+ if (typeof configOrFn === 'function') {
942
+ return instrumentedStep(
943
+ stepName,
944
+ undefined,
945
+ configOrFn,
946
+ innerCtx.step
947
+ ) as Promise<unknown>
948
+ }
949
+ return instrumentedStep(
950
+ stepName,
951
+ configOrFn,
952
+ maybeFn!,
953
+ innerCtx.step
954
+ ) as Promise<unknown>
955
+ }) as InProcessWorkflowContext<TInput>['step'],
956
+ ...(runtime !== undefined && { runtime }),
957
+ }
958
+ return fn(ctx)
959
+ },
960
+ input
961
+ )
962
+ recordRunHistory(name, 'finish', { result })
963
+ return result
964
+ } catch (err) {
965
+ recordRunHistory(name, 'error', { error: err instanceof Error ? err.message : String(err) })
966
+ throw err
967
+ }
968
+ }
969
+
970
+ function topLevelStep<T>(
971
+ name: string,
972
+ configOrFn: StepConfig | (() => Promise<T>),
973
+ maybeFn?: () => Promise<T>
974
+ ): Promise<T> {
975
+ if (typeof configOrFn === 'function') {
976
+ return instrumentedStep(name, undefined, configOrFn, inner.step)
977
+ }
978
+ return instrumentedStep(name, configOrFn, maybeFn!, inner.step)
979
+ }
980
+
981
+ function instrumentedSchedule<TResult = unknown>(
982
+ name: string,
983
+ cron: string,
984
+ fn: WorkflowFn<TResult, undefined>
985
+ ): Subscription {
986
+ if (!runtime) {
987
+ return inner.schedule(name, cron, fn)
988
+ }
989
+ // When a runtime is wired, surface schedule failures through the logger
990
+ // (the in-memory stub silently swallows them). We do this by wrapping the
991
+ // body to log; the inner schedule itself continues to swallow rejections
992
+ // so the interval keeps firing.
993
+ const wrapped: WorkflowFn<TResult, undefined> = async (ctx) => {
994
+ try {
995
+ return await fn(ctx)
996
+ } catch (err) {
997
+ getLogger().error(`[durable-execution] Scheduled run "${name}" failed:`, err)
998
+ throw err
999
+ }
1000
+ }
1001
+ return inner.schedule(name, cron, wrapped)
1002
+ }
1003
+
1004
+ const adapter: InProcessDurableExecution = {
1005
+ kind: 'in-process',
1006
+ run: run as DurableExecutionAdapter['run'],
1007
+ step: topLevelStep as DurableExecutionAdapter['step'],
1008
+ sleep: inner.sleep,
1009
+ sleepUntil: inner.sleepUntil,
1010
+ waitForEvent: inner.waitForEvent,
1011
+ schedule: instrumentedSchedule,
1012
+ emit: inner.emit,
1013
+ ...(runtime !== undefined && { runtime }),
1014
+ }
1015
+
1016
+ return adapter
1017
+ }
1018
+
1019
+ // =============================================================================
1020
+ // Cloudflare Workflows adapter (re-export)
1021
+ // =============================================================================
1022
+
1023
+ // Re-export the Cloudflare Workflows adapter so callers using the
1024
+ // `ai-workflows/durable-execution` subpath can reach both the port and its CF
1025
+ // implementation through a single import. See `./durable-execution-cloudflare.ts`
1026
+ // for the bridge logic and `docs/adr/0004-durable-execution-cf-workflows-default.md`
1027
+ // for the rationale.
1028
+ export {
1029
+ createCloudflareWorkflowsDurableExecution,
1030
+ createWorkflowEntrypoint,
1031
+ type CloudflareWorkflowsDurableExecution,
1032
+ type CloudflareWorkflowsDurableExecutionOptions,
1033
+ type CloudflareWorkflowsLimits,
1034
+ type WorkflowEnvelope,
1035
+ type WorkflowEntrypointConstructor,
1036
+ type WorkflowEntrypointLike,
1037
+ type WorkflowEventLike,
1038
+ type WorkflowInstanceLike,
1039
+ type WorkflowsBindingLike,
1040
+ type WorkflowStepConfigLike,
1041
+ type WorkflowStepLike,
1042
+ } from './durable-execution-cloudflare.js'