ai-workflows 2.1.3 → 2.4.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 +14 -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,976 @@
1
+ /**
2
+ * DurableStep - Wrapper for Cloudflare Workflows step semantics
3
+ *
4
+ * Provides durable execution, retries, sleep, and step metadata
5
+ * by wrapping Cloudflare Workflows' step.do() primitive. This is the
6
+ * foundation for building reliable, resumable workflow steps.
7
+ *
8
+ * ## Features
9
+ *
10
+ * - **Durable Execution**: Steps are persisted and can resume after failures
11
+ * - **Automatic Retries**: Configure retry behavior with backoff strategies
12
+ * - **Timeout Support**: Set per-step timeouts for long-running operations
13
+ * - **Step Context**: Access to nested durable operations and sleep
14
+ * - **Type Safety**: Full TypeScript generics for input/output types
15
+ * - **Cascade Pattern**: Tiered execution with code -> generative -> agentic -> human escalation
16
+ *
17
+ * ## Basic Usage
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * import { DurableStep } from 'ai-workflows/worker'
22
+ *
23
+ * // Simple step with type inference
24
+ * const fetchData = new DurableStep('fetch-data', async (input: { url: string }) => {
25
+ * const response = await fetch(input.url)
26
+ * return response.json()
27
+ * })
28
+ *
29
+ * // With retry configuration
30
+ * const processPayment = new DurableStep(
31
+ * 'process-payment',
32
+ * { retries: { limit: 3, delay: '1 second', backoff: 'exponential' } },
33
+ * async (input: { amount: number }) => {
34
+ * return { success: true }
35
+ * }
36
+ * )
37
+ *
38
+ * // Run with workflow step
39
+ * const result = await fetchData.run(step, { url: 'https://api.example.com' })
40
+ * ```
41
+ *
42
+ * ## Using StepContext
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const complexStep = new DurableStep('complex', async (input, ctx) => {
47
+ * // Access step metadata
48
+ * console.log(`Attempt ${ctx.metadata.attempt} of step ${ctx.metadata.id}`)
49
+ *
50
+ * // Nested durable operation
51
+ * const result = await ctx.do('fetch-api', async () => {
52
+ * return fetch('https://api.example.com').then(r => r.json())
53
+ * })
54
+ *
55
+ * return result
56
+ * })
57
+ * ```
58
+ *
59
+ * ## Cascade Pattern
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * const processRefund = DurableStep.cascade('process-refund', {
64
+ * code: async (input) => {
65
+ * if (input.amount < 100) return { approved: true }
66
+ * throw new Error('Needs AI review')
67
+ * },
68
+ * generative: async (input, ctx) => {
69
+ * const result = await ctx.ai.run('@cf/meta/llama-3-8b-instruct', {
70
+ * messages: [{ role: 'user', content: 'Approve refund?' }]
71
+ * })
72
+ * return { approved: result.response.includes('yes') }
73
+ * },
74
+ * human: async (input, ctx) => {
75
+ * return ctx.requestHumanReview({ type: 'refund', data: input })
76
+ * }
77
+ * })
78
+ * ```
79
+ *
80
+ * @packageDocumentation
81
+ */
82
+
83
+ import {
84
+ createCascadeContext,
85
+ recordStep,
86
+ type CascadeContext as BaseCascadeContext,
87
+ type FiveWHEvent,
88
+ } from '../cascade-context.js'
89
+
90
+ /**
91
+ * Configuration for retry behavior
92
+ */
93
+ export interface RetryConfig {
94
+ /** Maximum number of retry attempts */
95
+ limit: number
96
+ /** Delay between retries (string like '1 second' or number in ms) */
97
+ delay?: string | number
98
+ /** Backoff strategy */
99
+ backoff?: 'constant' | 'linear' | 'exponential'
100
+ }
101
+
102
+ /**
103
+ * Configuration for a step, matching Cloudflare WorkflowStepConfig
104
+ */
105
+ export interface StepConfig {
106
+ /** Retry configuration */
107
+ retries?: RetryConfig
108
+ /** Timeout for the step (string like '30 seconds' or number in ms) */
109
+ timeout?: string | number
110
+ }
111
+
112
+ /**
113
+ * Metadata about the current step execution
114
+ */
115
+ export interface StepMetadata {
116
+ /** Step identifier */
117
+ id: string
118
+ /** Current attempt number (1-based) */
119
+ attempt: number
120
+ /** Configured retry limit (0 if no retries configured) */
121
+ retries: number
122
+ }
123
+
124
+ /**
125
+ * Interface for Cloudflare Workflows step object
126
+ */
127
+ export interface WorkflowStep {
128
+ do<T>(name: string, callback: () => Promise<T>): Promise<T>
129
+ do<T>(name: string, config: StepConfig, callback: () => Promise<T>): Promise<T>
130
+ sleep(name: string, duration: string | number): Promise<void>
131
+ sleepUntil(name: string, timestamp: Date | number): Promise<void>
132
+ }
133
+
134
+ /**
135
+ * Context provided to step functions for additional operations
136
+ */
137
+ export class StepContext {
138
+ private workflowStep: WorkflowStep
139
+ private stepName: string
140
+ private stepConfig: StepConfig | undefined
141
+ private currentAttempt: number
142
+
143
+ /**
144
+ * Create a new StepContext
145
+ *
146
+ * @param workflowStep - The underlying Cloudflare workflow step
147
+ * @param stepName - The name of the parent step
148
+ * @param stepConfig - Optional step configuration
149
+ * @param attempt - Current attempt number
150
+ */
151
+ constructor(
152
+ workflowStep: WorkflowStep,
153
+ stepName: string,
154
+ stepConfig?: StepConfig,
155
+ attempt: number = 1
156
+ ) {
157
+ this.workflowStep = workflowStep
158
+ this.stepName = stepName
159
+ this.stepConfig = stepConfig
160
+ this.currentAttempt = attempt
161
+ }
162
+
163
+ /**
164
+ * Metadata about the current step execution
165
+ */
166
+ get metadata(): StepMetadata {
167
+ return {
168
+ id: this.stepName,
169
+ attempt: this.currentAttempt,
170
+ retries: this.stepConfig?.retries?.limit ?? 0,
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Execute a named side effect durably
176
+ *
177
+ * @param name - Unique name for this side effect
178
+ * @param callback - Function to execute
179
+ * @returns Result of the callback
180
+ */
181
+ do<T>(name: string, callback: () => Promise<T>): Promise<T>
182
+
183
+ /**
184
+ * Execute a named side effect durably with configuration
185
+ *
186
+ * @param name - Unique name for this side effect
187
+ * @param config - Step configuration (retries, timeout)
188
+ * @param callback - Function to execute
189
+ * @returns Result of the callback
190
+ */
191
+ do<T>(name: string, config: StepConfig, callback: () => Promise<T>): Promise<T>
192
+
193
+ /**
194
+ * Implementation of do() overloads
195
+ */
196
+ do<T>(
197
+ name: string,
198
+ configOrCallback: StepConfig | (() => Promise<T>),
199
+ maybeCallback?: () => Promise<T>
200
+ ): Promise<T> {
201
+ if (typeof configOrCallback === 'function') {
202
+ return this.workflowStep.do(name, configOrCallback)
203
+ } else {
204
+ return this.workflowStep.do(name, configOrCallback, maybeCallback!)
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Sleep for a specified duration
210
+ *
211
+ * @param name - Unique name for this sleep operation
212
+ * @param duration - Duration to sleep (string like '5 seconds' or number in ms)
213
+ */
214
+ sleep(name: string, duration: string | number): Promise<void> {
215
+ return this.workflowStep.sleep(name, duration)
216
+ }
217
+
218
+ /**
219
+ * Sleep until a specified timestamp
220
+ *
221
+ * @param name - Unique name for this sleep operation
222
+ * @param timestamp - Date or unix timestamp to sleep until
223
+ */
224
+ sleepUntil(name: string, timestamp: Date | number): Promise<void> {
225
+ return this.workflowStep.sleepUntil(name, timestamp)
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Type for the step function
231
+ */
232
+ export type StepFunction<TInput, TOutput> = (input: TInput, ctx: StepContext) => Promise<TOutput>
233
+
234
+ /**
235
+ * DurableStep - Wrapper for durable function execution
236
+ *
237
+ * Wraps a function for durable execution using Cloudflare Workflows'
238
+ * step.do() primitive. Provides retry configuration, timeout, and
239
+ * access to step context.
240
+ *
241
+ * @typeParam TInput - Input type for the step function
242
+ * @typeParam TOutput - Output type for the step function
243
+ */
244
+ export class DurableStep<TInput = void, TOutput = void> {
245
+ /** Step name */
246
+ readonly name: string
247
+
248
+ /** Step configuration (retries, timeout) */
249
+ readonly config?: StepConfig
250
+
251
+ /** The wrapped function */
252
+ readonly fn: (input: TInput, ctx: StepContext) => Promise<TOutput>
253
+
254
+ /**
255
+ * Create a DurableStep with just a name and function
256
+ *
257
+ * @param name - Unique name for this step
258
+ * @param fn - Function to execute
259
+ */
260
+ constructor(name: string, fn: (input: TInput, ctx?: StepContext) => Promise<TOutput>)
261
+
262
+ /**
263
+ * Create a DurableStep with name, config, and function
264
+ *
265
+ * @param name - Unique name for this step
266
+ * @param config - Step configuration (retries, timeout)
267
+ * @param fn - Function to execute
268
+ */
269
+ constructor(
270
+ name: string,
271
+ config: StepConfig,
272
+ fn: (input: TInput, ctx?: StepContext) => Promise<TOutput>
273
+ )
274
+
275
+ /**
276
+ * Implementation of constructor overloads
277
+ */
278
+ constructor(
279
+ name: string,
280
+ configOrFn: StepConfig | ((input: TInput, ctx?: StepContext) => Promise<TOutput>),
281
+ maybeFn?: (input: TInput, ctx?: StepContext) => Promise<TOutput>
282
+ ) {
283
+ this.name = name
284
+
285
+ if (typeof configOrFn === 'function') {
286
+ this.fn = configOrFn as (input: TInput, ctx: StepContext) => Promise<TOutput>
287
+ } else {
288
+ this.config = configOrFn
289
+ this.fn = maybeFn as (input: TInput, ctx: StepContext) => Promise<TOutput>
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Run the step with durable execution
295
+ *
296
+ * @param workflowStep - The Cloudflare workflow step object
297
+ * @param input - Input for the step function
298
+ * @returns Result of the step function
299
+ */
300
+ async run(workflowStep: WorkflowStep, input: TInput): Promise<TOutput> {
301
+ const ctx = new StepContext(workflowStep, this.name, this.config)
302
+
303
+ if (this.config) {
304
+ return workflowStep.do(this.name, this.config, async () => {
305
+ return this.fn(input, ctx)
306
+ })
307
+ } else {
308
+ return workflowStep.do(this.name, async () => {
309
+ return this.fn(input, ctx)
310
+ })
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Create a durable cascade step with tiered execution
316
+ *
317
+ * The cascade pattern executes tiers in order: code -> generative -> agentic -> human
318
+ * Each tier can short-circuit on success, or escalate to the next tier on failure.
319
+ *
320
+ * @param name - Unique name for this cascade step
321
+ * @param config - Cascade configuration with tier handlers
322
+ * @returns DurableCascadeStep instance
323
+ *
324
+ * @example
325
+ * ```typescript
326
+ * const processRefund = DurableStep.cascade('process-refund', {
327
+ * code: async (input) => {
328
+ * if (input.amount < 100) return { approved: true }
329
+ * throw new Error('Needs AI review')
330
+ * },
331
+ * generative: async (input, ctx) => {
332
+ * const result = await ctx.ai.run('@cf/meta/llama-3-8b-instruct', {
333
+ * messages: [{ role: 'user', content: 'Approve refund?' }]
334
+ * })
335
+ * return { approved: result.response.includes('yes') }
336
+ * },
337
+ * human: async (input, ctx) => {
338
+ * return ctx.requestHumanReview({ type: 'refund', data: input })
339
+ * }
340
+ * })
341
+ * ```
342
+ */
343
+ static cascade<TInput = unknown, TOutput = unknown>(
344
+ name: string,
345
+ config: CascadeConfig<TInput, TOutput>
346
+ ): DurableCascadeStep<TInput, TOutput> {
347
+ return new DurableCascadeStep(name, config)
348
+ }
349
+ }
350
+
351
+ // ============================================================================
352
+ // Cascade Types
353
+ // ============================================================================
354
+
355
+ /**
356
+ * Tier names in cascade order
357
+ */
358
+ export type CascadeTier = 'code' | 'generative' | 'agentic' | 'human'
359
+
360
+ /**
361
+ * Default timeouts per tier in milliseconds
362
+ */
363
+ export const DEFAULT_CASCADE_TIMEOUTS: Record<CascadeTier, number> = {
364
+ code: 5000, // 5 seconds
365
+ generative: 30000, // 30 seconds
366
+ agentic: 300000, // 5 minutes
367
+ human: 86400000, // 24 hours
368
+ }
369
+
370
+ /**
371
+ * Ordered list of cascade tiers
372
+ */
373
+ export const CASCADE_TIER_ORDER: CascadeTier[] = ['code', 'generative', 'agentic', 'human']
374
+
375
+ /**
376
+ * Human review request
377
+ */
378
+ export interface HumanReviewRequest {
379
+ type: string
380
+ data: unknown
381
+ assignee?: string
382
+ }
383
+
384
+ /**
385
+ * AI binding interface (matches Cloudflare AI binding)
386
+ */
387
+ export interface AiBinding {
388
+ run<T = unknown>(
389
+ model: string,
390
+ options: { messages: Array<{ role: string; content: string }> }
391
+ ): Promise<T & { response?: string }>
392
+ }
393
+
394
+ /**
395
+ * Context provided to cascade tier handlers
396
+ */
397
+ export interface CascadeTierContext<TInput = unknown> {
398
+ /** AI binding for generative/agentic tiers */
399
+ ai: AiBinding
400
+ /** Previous tier errors (for context) */
401
+ previousErrors: Array<{ tier: CascadeTier; error: string; attempt: number }>
402
+ /** Request human review */
403
+ requestHumanReview: (request: HumanReviewRequest) => Promise<{ reviewId: string; status: string }>
404
+ /** Cascade context for tracing */
405
+ cascadeContext: CascadeContext
406
+ /** Input data */
407
+ input: TInput
408
+ /** Current tier */
409
+ tier: CascadeTier
410
+ }
411
+
412
+ /**
413
+ * Configuration for a specific tier
414
+ */
415
+ export interface CascadeTierConfig {
416
+ /** Timeout in milliseconds */
417
+ timeout?: number
418
+ /** Retry configuration */
419
+ retries?: {
420
+ limit: number
421
+ delay?: number
422
+ backoff?: 'constant' | 'linear' | 'exponential'
423
+ }
424
+ /** Custom success condition */
425
+ successCondition?: (result: unknown) => boolean
426
+ /** Custom error handler */
427
+ onError?: (error: Error, tier: CascadeTier) => void
428
+ }
429
+
430
+ /**
431
+ * Result from a single tier execution
432
+ */
433
+ export interface CascadeTierResult<T = unknown> {
434
+ /** Tier that was executed */
435
+ tier: CascadeTier
436
+ /** Whether the tier succeeded */
437
+ success: boolean
438
+ /** Result value (if success) */
439
+ value?: T
440
+ /** Error (if failure) */
441
+ error?: Error
442
+ /** Whether the tier timed out */
443
+ timedOut?: boolean
444
+ /** Duration in milliseconds */
445
+ duration: number
446
+ /** Number of attempts */
447
+ attempts?: number
448
+ }
449
+
450
+ /**
451
+ * Cascade execution context with tracing
452
+ * Note: This interface extends BaseCascadeContext with no changes.
453
+ * The 'steps' property is inherited from BaseCascadeContext.
454
+ */
455
+ export interface CascadeContext extends BaseCascadeContext {
456
+ // Inherits all properties from BaseCascadeContext including correlationId and steps
457
+ }
458
+
459
+ /**
460
+ * Result from cascade execution
461
+ */
462
+ export interface CascadeResult<T = unknown> {
463
+ /** Final result value */
464
+ value: T
465
+ /** Tier that produced the result */
466
+ tier: CascadeTier
467
+ /** History of all tier executions */
468
+ history: CascadeTierResult<T>[]
469
+ /** Tiers that were skipped */
470
+ skippedTiers: CascadeTier[]
471
+ /** Cascade context with tracing info */
472
+ context: CascadeContext
473
+ /** Execution metrics */
474
+ metrics: {
475
+ totalDuration: number
476
+ tierDurations: Record<string, number>
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Handler for code tier (synchronous/deterministic)
482
+ */
483
+ export type CodeTierHandler<TInput, TOutput> = (input: TInput) => Promise<TOutput>
484
+
485
+ /**
486
+ * Handler for AI tiers (generative, agentic)
487
+ */
488
+ export type AiTierHandler<TInput, TOutput> = (
489
+ input: TInput,
490
+ ctx: CascadeTierContext<TInput>
491
+ ) => Promise<TOutput>
492
+
493
+ /**
494
+ * Handler for human tier
495
+ */
496
+ export type HumanTierHandler<TInput, TOutput> = (
497
+ input: TInput,
498
+ ctx: CascadeTierContext<TInput>
499
+ ) => Promise<TOutput>
500
+
501
+ /**
502
+ * Configuration for DurableStep.cascade()
503
+ */
504
+ export interface CascadeConfig<TInput = unknown, TOutput = unknown> {
505
+ /** Code tier handler (deterministic, fast) */
506
+ code?: CodeTierHandler<TInput, TOutput>
507
+ /** Generative AI tier handler */
508
+ generative?: AiTierHandler<TInput, TOutput>
509
+ /** Agentic AI tier handler (multi-step reasoning) */
510
+ agentic?: AiTierHandler<TInput, TOutput>
511
+ /** Human tier handler (human-in-the-loop) */
512
+ human?: HumanTierHandler<TInput, TOutput>
513
+ /** Per-tier timeout configuration */
514
+ timeouts?: Partial<Record<CascadeTier, number>>
515
+ /** Total cascade timeout */
516
+ totalTimeout?: number
517
+ /** Per-tier configuration */
518
+ tierConfig?: Partial<Record<CascadeTier, CascadeTierConfig>>
519
+ /** Event callback for 5W+H audit events */
520
+ onEvent?: (event: FiveWHEvent) => void
521
+ /** Custom result merger */
522
+ resultMerger?: (results: CascadeTierResult<TOutput>[]) => TOutput
523
+ /** Actor identifier for audit trail */
524
+ actor?: string
525
+ }
526
+
527
+ /**
528
+ * Error thrown when all cascade tiers fail
529
+ */
530
+ export class AllTiersFailed extends Error {
531
+ public readonly history: CascadeTierResult[]
532
+
533
+ constructor(history: CascadeTierResult[]) {
534
+ super('All cascade tiers failed')
535
+ this.name = 'AllTiersFailed'
536
+ this.history = history
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Error thrown when cascade times out
542
+ */
543
+ export class CascadeTimeout extends Error {
544
+ public readonly timeout: number
545
+ public readonly elapsed: number
546
+
547
+ constructor(timeout: number, elapsed: number) {
548
+ super(`Cascade timed out after ${elapsed}ms (limit: ${timeout}ms)`)
549
+ this.name = 'CascadeTimeout'
550
+ this.timeout = timeout
551
+ this.elapsed = elapsed
552
+ }
553
+ }
554
+
555
+ // ============================================================================
556
+ // DurableCascadeStep
557
+ // ============================================================================
558
+
559
+ /**
560
+ * DurableCascadeStep - Durable tiered execution with cascade pattern
561
+ *
562
+ * Combines the cascade pattern (code -> generative -> agentic -> human)
563
+ * with Cloudflare Workflows durability guarantees.
564
+ */
565
+ export class DurableCascadeStep<TInput = unknown, TOutput = unknown> {
566
+ /** Cascade name */
567
+ readonly name: string
568
+
569
+ /** Cascade configuration */
570
+ readonly config: CascadeConfig<TInput, TOutput>
571
+
572
+ /** AI binding (injected at runtime) */
573
+ private ai?: AiBinding
574
+
575
+ /** Human review handler */
576
+ private humanReviewHandler?: (
577
+ request: HumanReviewRequest
578
+ ) => Promise<{ reviewId: string; status: string }>
579
+
580
+ constructor(name: string, config: CascadeConfig<TInput, TOutput>) {
581
+ this.name = name
582
+ this.config = config
583
+ }
584
+
585
+ /**
586
+ * Set the AI binding for generative/agentic tiers
587
+ */
588
+ setAi(ai: AiBinding): this {
589
+ this.ai = ai
590
+ return this
591
+ }
592
+
593
+ /**
594
+ * Set the human review handler
595
+ */
596
+ setHumanReviewHandler(
597
+ handler: (request: HumanReviewRequest) => Promise<{ reviewId: string; status: string }>
598
+ ): this {
599
+ this.humanReviewHandler = handler
600
+ return this
601
+ }
602
+
603
+ /**
604
+ * Run the cascade with durable execution
605
+ */
606
+ async run(
607
+ workflowStep: WorkflowStep,
608
+ input: TInput,
609
+ options?: {
610
+ ai?: AiBinding
611
+ humanReviewHandler?: (
612
+ request: HumanReviewRequest
613
+ ) => Promise<{ reviewId: string; status: string }>
614
+ }
615
+ ): Promise<CascadeResult<TOutput>> {
616
+ const ai = options?.ai ?? this.ai
617
+ const humanReviewHandler = options?.humanReviewHandler ?? this.humanReviewHandler
618
+
619
+ // Create cascade context for tracing
620
+ const cascadeContext = createCascadeContext({ name: this.name }) as CascadeContext
621
+
622
+ const startTime = Date.now()
623
+ const history: CascadeTierResult<TOutput>[] = []
624
+ const skippedTiers: CascadeTier[] = []
625
+ const tierDurations: Record<string, number> = {}
626
+ const previousErrors: Array<{ tier: CascadeTier; error: string; attempt: number }> = []
627
+ let checkpointsCreated = 0
628
+ const checkpointIds: string[] = []
629
+
630
+ // Emit cascade start event
631
+ this.emitEvent({
632
+ who: this.config.actor ?? 'system',
633
+ what: 'cascade-start',
634
+ when: startTime,
635
+ where: this.name,
636
+ how: {
637
+ status: 'running',
638
+ metadata: { input },
639
+ },
640
+ })
641
+
642
+ // Execute tiers in order
643
+ for (const tier of CASCADE_TIER_ORDER) {
644
+ const handler = this.getTierHandler(tier)
645
+
646
+ // Skip unconfigured tiers
647
+ if (!handler) {
648
+ skippedTiers.push(tier)
649
+ continue
650
+ }
651
+
652
+ // Check total timeout
653
+ if (this.config.totalTimeout) {
654
+ const elapsed = Date.now() - startTime
655
+ if (elapsed >= this.config.totalTimeout) {
656
+ throw new CascadeTimeout(this.config.totalTimeout, elapsed)
657
+ }
658
+ }
659
+
660
+ // Create tier context
661
+ const tierContext: CascadeTierContext<TInput> = {
662
+ ai: ai ?? this.createMockAi(),
663
+ previousErrors: [...previousErrors],
664
+ requestHumanReview: async (request) => {
665
+ if (humanReviewHandler) {
666
+ return humanReviewHandler(request)
667
+ }
668
+ const reviewId = `review-${Date.now()}-${Math.random().toString(36).slice(2)}`
669
+ return { reviewId, status: 'pending-human' }
670
+ },
671
+ cascadeContext,
672
+ input,
673
+ tier,
674
+ }
675
+
676
+ // Execute tier with durability
677
+ const tierResult = await this.executeTier(
678
+ workflowStep,
679
+ tier,
680
+ handler,
681
+ input,
682
+ tierContext,
683
+ cascadeContext
684
+ )
685
+
686
+ // Record checkpoint
687
+ checkpointsCreated++
688
+ checkpointIds.push(`${this.name}-${tier}-${Date.now()}`)
689
+
690
+ history.push(tierResult)
691
+ tierDurations[tier] = tierResult.duration
692
+
693
+ // If tier succeeded, we're done
694
+ if (tierResult.success && tierResult.value !== undefined) {
695
+ // Check custom success condition
696
+ const tierConfig = this.config.tierConfig?.[tier]
697
+ if (tierConfig?.successCondition && !tierConfig.successCondition(tierResult.value)) {
698
+ // Success condition not met, treat as failure and continue
699
+ previousErrors.push({
700
+ tier,
701
+ error: 'Success condition not met',
702
+ attempt: tierResult.attempts ?? 1,
703
+ })
704
+ continue
705
+ }
706
+
707
+ const endTime = Date.now()
708
+ this.emitEvent({
709
+ who: this.config.actor ?? 'system',
710
+ what: 'cascade-complete',
711
+ when: endTime,
712
+ where: this.name,
713
+ how: {
714
+ status: 'completed',
715
+ duration: endTime - startTime,
716
+ metadata: { tier, value: tierResult.value },
717
+ },
718
+ })
719
+
720
+ return {
721
+ value: tierResult.value as TOutput,
722
+ tier,
723
+ history,
724
+ skippedTiers,
725
+ context: cascadeContext,
726
+ metrics: {
727
+ totalDuration: endTime - startTime,
728
+ tierDurations,
729
+ },
730
+ }
731
+ }
732
+
733
+ // Record error for next tier
734
+ if (tierResult.error) {
735
+ previousErrors.push({
736
+ tier,
737
+ error: tierResult.error.message,
738
+ attempt: tierResult.attempts ?? 1,
739
+ })
740
+
741
+ // Emit escalation event
742
+ const nextTier = this.getNextConfiguredTier(tier)
743
+ if (nextTier) {
744
+ this.emitEvent({
745
+ who: this.config.actor ?? 'system',
746
+ what: `escalate-to-${nextTier}`,
747
+ when: Date.now(),
748
+ where: this.name,
749
+ why: tierResult.error.message,
750
+ how: {
751
+ status: 'running',
752
+ metadata: { fromTier: tier, toTier: nextTier },
753
+ },
754
+ })
755
+ }
756
+ }
757
+ }
758
+
759
+ // All tiers failed
760
+ const endTime = Date.now()
761
+ this.emitEvent({
762
+ who: this.config.actor ?? 'system',
763
+ what: 'cascade-failed',
764
+ when: endTime,
765
+ where: this.name,
766
+ why: 'All tiers failed',
767
+ how: {
768
+ status: 'failed',
769
+ duration: endTime - startTime,
770
+ },
771
+ })
772
+
773
+ throw new AllTiersFailed(history)
774
+ }
775
+
776
+ /**
777
+ * Execute a single tier with durability
778
+ */
779
+ private async executeTier(
780
+ workflowStep: WorkflowStep,
781
+ tier: CascadeTier,
782
+ handler: CodeTierHandler<TInput, TOutput> | AiTierHandler<TInput, TOutput>,
783
+ input: TInput,
784
+ tierContext: CascadeTierContext<TInput>,
785
+ cascadeContext: CascadeContext
786
+ ): Promise<CascadeTierResult<TOutput>> {
787
+ const tierStartTime = Date.now()
788
+ const tierConfig = this.config.tierConfig?.[tier]
789
+ const timeout = this.config.timeouts?.[tier] ?? DEFAULT_CASCADE_TIMEOUTS[tier]
790
+ const maxRetries = tierConfig?.retries?.limit ?? 0
791
+
792
+ // Record step in cascade context
793
+ const step = recordStep(cascadeContext, tier, {
794
+ actor: this.config.actor ?? 'system',
795
+ action: `execute-${tier}`,
796
+ })
797
+
798
+ // Emit tier start event
799
+ this.emitEvent({
800
+ who: this.config.actor ?? 'system',
801
+ what: `tier-${tier}-execute`,
802
+ when: tierStartTime,
803
+ where: this.name,
804
+ how: {
805
+ status: 'running',
806
+ metadata: { tier },
807
+ },
808
+ })
809
+
810
+ let lastError: Error | undefined
811
+ let attempts = 0
812
+
813
+ while (attempts <= maxRetries) {
814
+ attempts++
815
+
816
+ try {
817
+ // Execute with timeout using workflow step
818
+ const result = await workflowStep.do(
819
+ `${this.name}-${tier}-attempt-${attempts}`,
820
+ timeout !== undefined ? { timeout: `${timeout} milliseconds` } : {},
821
+ async () => {
822
+ if (tier === 'code') {
823
+ return (handler as CodeTierHandler<TInput, TOutput>)(input)
824
+ } else {
825
+ return (handler as AiTierHandler<TInput, TOutput>)(input, tierContext)
826
+ }
827
+ }
828
+ )
829
+
830
+ const duration = Date.now() - tierStartTime
831
+
832
+ // Mark step complete
833
+ step.complete()
834
+
835
+ // Emit tier success event
836
+ this.emitEvent({
837
+ who: this.config.actor ?? 'system',
838
+ what: `tier-${tier}-execute`,
839
+ when: Date.now(),
840
+ where: this.name,
841
+ how: {
842
+ status: 'completed',
843
+ duration,
844
+ metadata: { tier, result, attempts },
845
+ },
846
+ })
847
+
848
+ return {
849
+ tier,
850
+ success: true,
851
+ value: result as TOutput,
852
+ duration,
853
+ attempts,
854
+ }
855
+ } catch (error) {
856
+ lastError = error instanceof Error ? error : new Error(String(error))
857
+
858
+ // Check if it's a timeout error
859
+ const isTimeout =
860
+ lastError.message.includes('timed out') ||
861
+ lastError.message.includes('timeout') ||
862
+ lastError.name === 'TimeoutError'
863
+
864
+ // Call custom error handler if provided
865
+ if (tierConfig?.onError) {
866
+ tierConfig.onError(lastError, tier)
867
+ }
868
+
869
+ // If we've exhausted retries, stop
870
+ if (attempts > maxRetries) {
871
+ break
872
+ }
873
+
874
+ // Wait before retry with backoff
875
+ if (tierConfig?.retries?.delay) {
876
+ const backoff = tierConfig.retries.backoff ?? 'constant'
877
+ let delay = tierConfig.retries.delay
878
+
879
+ if (backoff === 'exponential') {
880
+ delay = tierConfig.retries.delay * Math.pow(2, attempts - 1)
881
+ } else if (backoff === 'linear') {
882
+ delay = tierConfig.retries.delay * attempts
883
+ }
884
+
885
+ await workflowStep.sleep(`${this.name}-${tier}-retry-wait-${attempts}`, delay)
886
+ }
887
+ }
888
+ }
889
+
890
+ const duration = Date.now() - tierStartTime
891
+ const isTimeout =
892
+ lastError?.message.includes('timed out') ||
893
+ lastError?.message.includes('timeout') ||
894
+ lastError?.name === 'TimeoutError'
895
+
896
+ // Mark step failed
897
+ if (lastError) {
898
+ step.fail(lastError)
899
+ }
900
+
901
+ // Emit tier failure event
902
+ this.emitEvent({
903
+ who: this.config.actor ?? 'system',
904
+ what: `tier-${tier}-execute`,
905
+ when: Date.now(),
906
+ where: this.name,
907
+ ...(lastError?.message !== undefined && { why: lastError.message }),
908
+ how: {
909
+ status: 'failed',
910
+ duration,
911
+ metadata: { tier, error: lastError?.message, attempts },
912
+ },
913
+ })
914
+
915
+ return {
916
+ tier,
917
+ success: false,
918
+ ...(lastError !== undefined && { error: lastError }),
919
+ timedOut: isTimeout,
920
+ duration,
921
+ attempts,
922
+ }
923
+ }
924
+
925
+ /**
926
+ * Get the handler for a tier
927
+ */
928
+ private getTierHandler(
929
+ tier: CascadeTier
930
+ ): CodeTierHandler<TInput, TOutput> | AiTierHandler<TInput, TOutput> | undefined {
931
+ switch (tier) {
932
+ case 'code':
933
+ return this.config.code
934
+ case 'generative':
935
+ return this.config.generative
936
+ case 'agentic':
937
+ return this.config.agentic
938
+ case 'human':
939
+ return this.config.human
940
+ default:
941
+ return undefined
942
+ }
943
+ }
944
+
945
+ /**
946
+ * Get the next configured tier
947
+ */
948
+ private getNextConfiguredTier(currentTier: CascadeTier): CascadeTier | undefined {
949
+ const currentIndex = CASCADE_TIER_ORDER.indexOf(currentTier)
950
+ for (let i = currentIndex + 1; i < CASCADE_TIER_ORDER.length; i++) {
951
+ const tier = CASCADE_TIER_ORDER[i]
952
+ if (tier && this.getTierHandler(tier)) {
953
+ return tier
954
+ }
955
+ }
956
+ return undefined
957
+ }
958
+
959
+ /**
960
+ * Emit a 5W+H event
961
+ */
962
+ private emitEvent(event: FiveWHEvent): void {
963
+ if (this.config.onEvent) {
964
+ this.config.onEvent(event)
965
+ }
966
+ }
967
+
968
+ /**
969
+ * Create a mock AI binding for testing
970
+ */
971
+ private createMockAi(): AiBinding {
972
+ return {
973
+ run: async <T>(): Promise<T> => ({ response: 'mock response' } as unknown as T),
974
+ }
975
+ }
976
+ }