ai-props 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 (105) hide show
  1. package/.dev.vars +2 -0
  2. package/.turbo/turbo-build.log +1 -1
  3. package/CHANGELOG.md +20 -0
  4. package/README.md +2 -0
  5. package/dist/ai.d.ts.map +1 -1
  6. package/dist/ai.js +4 -4
  7. package/dist/ai.js.map +1 -1
  8. package/dist/cascade.d.ts +329 -0
  9. package/dist/cascade.d.ts.map +1 -0
  10. package/dist/cascade.js +522 -0
  11. package/dist/cascade.js.map +1 -0
  12. package/dist/client.d.ts +233 -0
  13. package/dist/client.d.ts.map +1 -0
  14. package/dist/client.js +191 -0
  15. package/dist/client.js.map +1 -0
  16. package/dist/durable-cascade.d.ts +280 -0
  17. package/dist/durable-cascade.d.ts.map +1 -0
  18. package/dist/durable-cascade.js +469 -0
  19. package/dist/durable-cascade.js.map +1 -0
  20. package/dist/event-bridge.d.ts +257 -0
  21. package/dist/event-bridge.d.ts.map +1 -0
  22. package/dist/event-bridge.js +317 -0
  23. package/dist/event-bridge.js.map +1 -0
  24. package/dist/generate.d.ts.map +1 -1
  25. package/dist/generate.js +12 -6
  26. package/dist/generate.js.map +1 -1
  27. package/dist/hoc.d.ts.map +1 -1
  28. package/dist/hoc.js +13 -13
  29. package/dist/hoc.js.map +1 -1
  30. package/dist/hono-jsx.d.ts +208 -0
  31. package/dist/hono-jsx.d.ts.map +1 -0
  32. package/dist/hono-jsx.js +459 -0
  33. package/dist/hono-jsx.js.map +1 -0
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/mdx-types.d.ts +152 -0
  39. package/dist/mdx-types.d.ts.map +1 -0
  40. package/dist/mdx-types.js +9 -0
  41. package/dist/mdx-types.js.map +1 -0
  42. package/dist/mdx-utils.d.ts +106 -0
  43. package/dist/mdx-utils.d.ts.map +1 -0
  44. package/dist/mdx-utils.js +384 -0
  45. package/dist/mdx-utils.js.map +1 -0
  46. package/dist/mdx.d.ts +230 -0
  47. package/dist/mdx.d.ts.map +1 -0
  48. package/dist/mdx.js +820 -0
  49. package/dist/mdx.js.map +1 -0
  50. package/dist/rpc.d.ts +313 -0
  51. package/dist/rpc.d.ts.map +1 -0
  52. package/dist/rpc.js +359 -0
  53. package/dist/rpc.js.map +1 -0
  54. package/dist/streaming.d.ts +199 -0
  55. package/dist/streaming.d.ts.map +1 -0
  56. package/dist/streaming.js +402 -0
  57. package/dist/streaming.js.map +1 -0
  58. package/dist/validate.d.ts.map +1 -1
  59. package/dist/validate.js +11 -13
  60. package/dist/validate.js.map +1 -1
  61. package/dist/worker.d.ts +270 -0
  62. package/dist/worker.d.ts.map +1 -0
  63. package/dist/worker.js +405 -0
  64. package/dist/worker.js.map +1 -0
  65. package/package.json +39 -13
  66. package/src/ai.ts +12 -31
  67. package/src/cascade.ts +795 -0
  68. package/src/client.ts +440 -0
  69. package/src/durable-cascade.ts +743 -0
  70. package/src/event-bridge.ts +478 -0
  71. package/src/generate.ts +14 -12
  72. package/src/hoc.ts +15 -19
  73. package/src/hono-jsx.ts +675 -0
  74. package/src/index.ts +30 -0
  75. package/src/mdx-types.ts +169 -0
  76. package/src/mdx-utils.ts +437 -0
  77. package/src/mdx.ts +1008 -0
  78. package/src/rpc.ts +614 -0
  79. package/src/streaming.ts +618 -0
  80. package/src/validate.ts +15 -29
  81. package/src/worker.ts +547 -0
  82. package/test/cascade.test.ts +338 -0
  83. package/test/durable-cascade.test.ts +319 -0
  84. package/test/event-bridge.test.ts +351 -0
  85. package/test/generate.test.ts +6 -16
  86. package/test/mdx.test.ts +817 -0
  87. package/test/worker/capnweb-rpc.test.ts +1084 -0
  88. package/test/worker/full-flow.integration.test.ts +1463 -0
  89. package/test/worker/hono-jsx.test.ts +1258 -0
  90. package/test/worker/mdx-parsing.test.ts +1148 -0
  91. package/test/worker/setup.ts +56 -0
  92. package/test/worker.test.ts +595 -0
  93. package/tsconfig.json +2 -1
  94. package/vitest.config.js +6 -0
  95. package/vitest.config.ts +15 -1
  96. package/vitest.workers.config.ts +58 -0
  97. package/wrangler.jsonc +27 -0
  98. package/LICENSE +0 -21
  99. package/src/ai.js +0 -198
  100. package/src/cache.js +0 -182
  101. package/src/generate.js +0 -220
  102. package/src/hoc.js +0 -235
  103. package/src/index.js +0 -20
  104. package/src/types.js +0 -6
  105. package/src/validate.js +0 -252
@@ -0,0 +1,743 @@
1
+ /**
2
+ * DurableCascadeExecutor - Durable tiered execution with cascade pattern for Workers
3
+ *
4
+ * Combines the cascade pattern (code -> generative -> agentic -> human)
5
+ * with Cloudflare Workflows durability guarantees for use in worker contexts.
6
+ *
7
+ * ## Features
8
+ *
9
+ * - **Durable Execution**: Steps are persisted and can resume after failures
10
+ * - **AI Gateway Integration**: Helpers for Cloudflare AI Gateway configuration
11
+ * - **Automatic Checkpointing**: Each tier creates a durable checkpoint
12
+ * - **Tier Escalation**: Automatic escalation on failure
13
+ * - **5W+H Audit Events**: Full audit trail support
14
+ *
15
+ * ## Basic Usage
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * import { DurableCascadeExecutor, createAIGatewayConfig } from 'ai-props/worker'
20
+ *
21
+ * const cascade = new DurableCascadeExecutor('generate-content', {
22
+ * code: async (input) => {
23
+ * if (input.template) return { content: input.template }
24
+ * throw new Error('No template')
25
+ * },
26
+ * generative: async (input, ctx) => {
27
+ * const result = await ctx.ai.run('@cf/meta/llama-3-8b-instruct', {
28
+ * messages: [{ role: 'user', content: 'Generate content' }]
29
+ * })
30
+ * return { content: result.response }
31
+ * }
32
+ * })
33
+ *
34
+ * // In workflow
35
+ * const result = await cascade.run(step, { topic: 'AI' })
36
+ * ```
37
+ *
38
+ * @packageDocumentation
39
+ */
40
+
41
+ import {
42
+ CascadeExecutor,
43
+ type CascadeConfig,
44
+ type CascadeResult,
45
+ type CascadeContext,
46
+ type TierContext,
47
+ type TierHandler,
48
+ type CapabilityTier,
49
+ type FiveWHEvent,
50
+ type TierResult,
51
+ TIER_ORDER,
52
+ DEFAULT_TIER_TIMEOUTS,
53
+ createCascadeContext,
54
+ recordStep,
55
+ AllTiersFailedError,
56
+ CascadeTimeoutError,
57
+ } from './cascade.js'
58
+
59
+ // Re-export base types
60
+ export {
61
+ CascadeExecutor,
62
+ type CascadeConfig,
63
+ type CascadeResult,
64
+ type CascadeContext,
65
+ type TierContext,
66
+ type TierHandler,
67
+ type CapabilityTier,
68
+ type FiveWHEvent,
69
+ type TierResult,
70
+ TIER_ORDER,
71
+ DEFAULT_TIER_TIMEOUTS,
72
+ createCascadeContext,
73
+ recordStep,
74
+ AllTiersFailedError,
75
+ CascadeTimeoutError,
76
+ }
77
+
78
+ // ============================================================================
79
+ // Worker-specific Types
80
+ // ============================================================================
81
+
82
+ /**
83
+ * Configuration for retry behavior (Cloudflare Workflows format)
84
+ */
85
+ export interface DurableRetryConfig {
86
+ /** Maximum number of retry attempts */
87
+ limit: number
88
+ /** Delay between retries (string like '1 second' or number in ms) */
89
+ delay?: string | number
90
+ /** Backoff strategy */
91
+ backoff?: 'constant' | 'linear' | 'exponential'
92
+ }
93
+
94
+ /**
95
+ * Configuration for a durable step
96
+ */
97
+ export interface DurableStepConfig {
98
+ /** Retry configuration */
99
+ retries?: DurableRetryConfig
100
+ /** Timeout for the step (string like '30 seconds' or number in ms) */
101
+ timeout?: string | number
102
+ }
103
+
104
+ /**
105
+ * Interface for Cloudflare Workflows step object
106
+ */
107
+ export interface WorkflowStep {
108
+ do<T>(name: string, callback: () => Promise<T>): Promise<T>
109
+ do<T>(name: string, config: DurableStepConfig, callback: () => Promise<T>): Promise<T>
110
+ sleep(name: string, duration: string | number): Promise<void>
111
+ sleepUntil(name: string, timestamp: Date | number): Promise<void>
112
+ }
113
+
114
+ /**
115
+ * AI binding interface (matches Cloudflare AI binding)
116
+ */
117
+ export interface AiBinding {
118
+ run<T = unknown>(
119
+ model: string,
120
+ options: { messages: Array<{ role: string; content: string }> }
121
+ ): Promise<T & { response?: string }>
122
+ }
123
+
124
+ /**
125
+ * Human review request
126
+ */
127
+ export interface HumanReviewRequest {
128
+ type: string
129
+ data: unknown
130
+ assignee?: string
131
+ }
132
+
133
+ /**
134
+ * Context provided to durable cascade tier handlers
135
+ */
136
+ export interface DurableCascadeTierContext<TInput = unknown> extends TierContext<TInput> {
137
+ /** AI binding for generative/agentic tiers */
138
+ ai: AiBinding
139
+ /** Request human review */
140
+ requestHumanReview: (request: HumanReviewRequest) => Promise<{ reviewId: string; status: string }>
141
+ /** Workflow step for nested durable operations */
142
+ workflowStep: WorkflowStep
143
+ }
144
+
145
+ /**
146
+ * Code tier handler (deterministic, fast)
147
+ */
148
+ export type CodeTierHandler<TInput, TOutput> = (input: TInput) => Promise<TOutput>
149
+
150
+ /**
151
+ * AI tier handler (generative or agentic)
152
+ */
153
+ export type AiTierHandler<TInput, TOutput> = (
154
+ input: TInput,
155
+ ctx: DurableCascadeTierContext<TInput>
156
+ ) => Promise<TOutput>
157
+
158
+ /**
159
+ * Human tier handler
160
+ */
161
+ export type HumanTierHandler<TInput, TOutput> = (
162
+ input: TInput,
163
+ ctx: DurableCascadeTierContext<TInput>
164
+ ) => Promise<TOutput>
165
+
166
+ /**
167
+ * Configuration for DurableCascadeExecutor
168
+ */
169
+ export interface DurableCascadeConfig<TInput = unknown, TOutput = unknown> {
170
+ /** Code tier handler (deterministic, fast) */
171
+ code?: CodeTierHandler<TInput, TOutput>
172
+ /** Generative AI tier handler */
173
+ generative?: AiTierHandler<TInput, TOutput>
174
+ /** Agentic AI tier handler (multi-step reasoning) */
175
+ agentic?: AiTierHandler<TInput, TOutput>
176
+ /** Human tier handler (human-in-the-loop) */
177
+ human?: HumanTierHandler<TInput, TOutput>
178
+ /** Per-tier timeout configuration (in ms) */
179
+ timeouts?: Partial<Record<CapabilityTier, number>>
180
+ /** Total cascade timeout (in ms) */
181
+ totalTimeout?: number
182
+ /** Per-tier retry configuration */
183
+ tierConfig?: Partial<
184
+ Record<
185
+ CapabilityTier,
186
+ {
187
+ retries?: { limit: number; delay?: number; backoff?: 'constant' | 'linear' | 'exponential' }
188
+ successCondition?: (result: unknown) => boolean
189
+ onError?: (error: Error, tier: CapabilityTier) => void
190
+ }
191
+ >
192
+ >
193
+ /** Event callback for 5W+H audit events */
194
+ onEvent?: (event: FiveWHEvent) => void
195
+ /** Actor identifier for audit trail */
196
+ actor?: string
197
+ }
198
+
199
+ // ============================================================================
200
+ // AI Gateway Configuration
201
+ // ============================================================================
202
+
203
+ /**
204
+ * AI Gateway configuration for Cloudflare Workers
205
+ */
206
+ export interface AIGatewayConfig {
207
+ /** Gateway ID */
208
+ gatewayId: string
209
+ /** Account ID */
210
+ accountId: string
211
+ /** Cache TTL in seconds (0 to disable) */
212
+ cacheTtl?: number
213
+ /** Skip cache for this request */
214
+ skipCache?: boolean
215
+ /** Custom metadata to attach to requests */
216
+ metadata?: Record<string, string>
217
+ }
218
+
219
+ /**
220
+ * Create AI Gateway configuration for use with cascades
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * const gatewayConfig = createAIGatewayConfig({
225
+ * gatewayId: 'my-gateway',
226
+ * accountId: env.CF_ACCOUNT_ID,
227
+ * cacheTtl: 3600, // 1 hour
228
+ * })
229
+ *
230
+ * // Use with AI binding
231
+ * const result = await env.AI.run('@cf/meta/llama-3-8b-instruct', {
232
+ * messages: [...],
233
+ * ...gatewayConfig.toRequestOptions()
234
+ * })
235
+ * ```
236
+ */
237
+ export function createAIGatewayConfig(config: AIGatewayConfig): {
238
+ config: AIGatewayConfig
239
+ toRequestOptions: () => Record<string, unknown>
240
+ getCacheKey: (input: unknown) => string
241
+ } {
242
+ return {
243
+ config,
244
+ toRequestOptions: () => ({
245
+ gateway: {
246
+ id: config.gatewayId,
247
+ skipCache: config.skipCache ?? false,
248
+ cacheTtl: config.cacheTtl ?? 0,
249
+ metadata: config.metadata,
250
+ },
251
+ }),
252
+ getCacheKey: (input: unknown) => {
253
+ const inputStr = JSON.stringify(input)
254
+ return `${config.gatewayId}:${inputStr.slice(0, 100)}`
255
+ },
256
+ }
257
+ }
258
+
259
+ // ============================================================================
260
+ // DurableCascadeExecutor
261
+ // ============================================================================
262
+
263
+ /**
264
+ * DurableCascadeExecutor - Durable tiered execution with cascade pattern
265
+ *
266
+ * Combines the cascade pattern (code -> generative -> agentic -> human)
267
+ * with Cloudflare Workflows durability guarantees. Each tier execution
268
+ * creates a durable checkpoint that survives process restarts.
269
+ */
270
+ export class DurableCascadeExecutor<TInput = unknown, TOutput = unknown> {
271
+ /** Cascade name */
272
+ readonly name: string
273
+
274
+ /** Cascade configuration */
275
+ readonly config: DurableCascadeConfig<TInput, TOutput>
276
+
277
+ /** AI binding (injected at runtime) */
278
+ private ai?: AiBinding
279
+
280
+ /** Human review handler */
281
+ private humanReviewHandler?: (
282
+ request: HumanReviewRequest
283
+ ) => Promise<{ reviewId: string; status: string }>
284
+
285
+ constructor(name: string, config: DurableCascadeConfig<TInput, TOutput>) {
286
+ this.name = name
287
+ this.config = config
288
+ }
289
+
290
+ /**
291
+ * Set the AI binding for generative/agentic tiers
292
+ */
293
+ setAi(ai: AiBinding): this {
294
+ this.ai = ai
295
+ return this
296
+ }
297
+
298
+ /**
299
+ * Set the human review handler
300
+ */
301
+ setHumanReviewHandler(
302
+ handler: (request: HumanReviewRequest) => Promise<{ reviewId: string; status: string }>
303
+ ): this {
304
+ this.humanReviewHandler = handler
305
+ return this
306
+ }
307
+
308
+ /**
309
+ * Run the cascade with durable execution
310
+ */
311
+ async run(
312
+ workflowStep: WorkflowStep,
313
+ input: TInput,
314
+ options?: {
315
+ ai?: AiBinding
316
+ humanReviewHandler?: (
317
+ request: HumanReviewRequest
318
+ ) => Promise<{ reviewId: string; status: string }>
319
+ }
320
+ ): Promise<CascadeResult<TOutput>> {
321
+ const ai = options?.ai ?? this.ai
322
+ const humanReviewHandler = options?.humanReviewHandler ?? this.humanReviewHandler
323
+
324
+ // Create cascade context for tracing
325
+ const cascadeContext = createCascadeContext({ name: this.name })
326
+
327
+ const startTime = Date.now()
328
+ const history: TierResult<TOutput>[] = []
329
+ const skippedTiers: CapabilityTier[] = []
330
+ const tierDurations: Record<string, number> = {}
331
+ const previousErrors: Array<{ tier: CapabilityTier; error: string }> = []
332
+
333
+ // Emit cascade start event
334
+ this.emitEvent({
335
+ who: this.config.actor ?? 'system',
336
+ what: 'cascade-start',
337
+ when: startTime,
338
+ where: this.name,
339
+ how: {
340
+ status: 'running',
341
+ metadata: { input },
342
+ },
343
+ })
344
+
345
+ // Execute tiers in order
346
+ for (const tier of TIER_ORDER) {
347
+ const handler = this.getTierHandler(tier)
348
+
349
+ // Skip unconfigured tiers
350
+ if (!handler) {
351
+ skippedTiers.push(tier)
352
+ continue
353
+ }
354
+
355
+ // Check total timeout
356
+ if (this.config.totalTimeout) {
357
+ const elapsed = Date.now() - startTime
358
+ if (elapsed >= this.config.totalTimeout) {
359
+ throw new CascadeTimeoutError(this.config.totalTimeout, elapsed)
360
+ }
361
+ }
362
+
363
+ // Create tier context
364
+ const tierContext: DurableCascadeTierContext<TInput> = {
365
+ correlationId: cascadeContext.correlationId,
366
+ tier,
367
+ input,
368
+ cascadeContext,
369
+ previousErrors: [...previousErrors],
370
+ ai: ai ?? this.createMockAi(),
371
+ requestHumanReview: async (request) => {
372
+ if (humanReviewHandler) {
373
+ return humanReviewHandler(request)
374
+ }
375
+ const reviewId = `review-${Date.now()}-${Math.random().toString(36).slice(2)}`
376
+ return { reviewId, status: 'pending-human' }
377
+ },
378
+ workflowStep,
379
+ }
380
+
381
+ // Execute tier with durability
382
+ const tierResult = await this.executeTier(
383
+ workflowStep,
384
+ tier,
385
+ handler,
386
+ input,
387
+ tierContext,
388
+ cascadeContext
389
+ )
390
+
391
+ history.push(tierResult)
392
+ tierDurations[tier] = tierResult.duration
393
+
394
+ // If tier succeeded, we're done
395
+ if (tierResult.success && tierResult.value !== undefined) {
396
+ // Check custom success condition
397
+ const tierConfig = this.config.tierConfig?.[tier]
398
+ if (tierConfig?.successCondition && !tierConfig.successCondition(tierResult.value)) {
399
+ // Success condition not met, treat as failure and continue
400
+ previousErrors.push({
401
+ tier,
402
+ error: 'Success condition not met',
403
+ })
404
+ continue
405
+ }
406
+
407
+ const endTime = Date.now()
408
+ this.emitEvent({
409
+ who: this.config.actor ?? 'system',
410
+ what: 'cascade-complete',
411
+ when: endTime,
412
+ where: this.name,
413
+ how: {
414
+ status: 'completed',
415
+ duration: endTime - startTime,
416
+ metadata: { tier, value: tierResult.value },
417
+ },
418
+ })
419
+
420
+ return {
421
+ value: tierResult.value,
422
+ tier,
423
+ history,
424
+ skippedTiers,
425
+ context: cascadeContext,
426
+ metrics: {
427
+ totalDuration: endTime - startTime,
428
+ tierDurations,
429
+ },
430
+ }
431
+ }
432
+
433
+ // Record error for next tier
434
+ if (tierResult.error) {
435
+ previousErrors.push({
436
+ tier,
437
+ error: tierResult.error.message,
438
+ })
439
+
440
+ // Emit escalation event
441
+ const nextTier = this.getNextConfiguredTier(tier)
442
+ if (nextTier) {
443
+ this.emitEvent({
444
+ who: this.config.actor ?? 'system',
445
+ what: `escalate-to-${nextTier}`,
446
+ when: Date.now(),
447
+ where: this.name,
448
+ why: tierResult.error.message,
449
+ how: {
450
+ status: 'running',
451
+ metadata: { fromTier: tier, toTier: nextTier },
452
+ },
453
+ })
454
+ }
455
+ }
456
+ }
457
+
458
+ // All tiers failed
459
+ const endTime = Date.now()
460
+ this.emitEvent({
461
+ who: this.config.actor ?? 'system',
462
+ what: 'cascade-failed',
463
+ when: endTime,
464
+ where: this.name,
465
+ why: 'All tiers failed',
466
+ how: {
467
+ status: 'failed',
468
+ duration: endTime - startTime,
469
+ },
470
+ })
471
+
472
+ throw new AllTiersFailedError(history)
473
+ }
474
+
475
+ /**
476
+ * Execute a single tier with durability
477
+ */
478
+ private async executeTier(
479
+ workflowStep: WorkflowStep,
480
+ tier: CapabilityTier,
481
+ handler: CodeTierHandler<TInput, TOutput> | AiTierHandler<TInput, TOutput>,
482
+ input: TInput,
483
+ tierContext: DurableCascadeTierContext<TInput>,
484
+ cascadeContext: CascadeContext
485
+ ): Promise<TierResult<TOutput>> {
486
+ const tierStartTime = Date.now()
487
+ const tierConfig = this.config.tierConfig?.[tier]
488
+ const timeout = this.config.timeouts?.[tier] ?? DEFAULT_TIER_TIMEOUTS[tier]
489
+ const maxRetries = tierConfig?.retries?.limit ?? 0
490
+
491
+ // Record step in cascade context
492
+ const step = recordStep(cascadeContext, tier, {
493
+ actor: this.config.actor ?? 'system',
494
+ action: `execute-${tier}`,
495
+ })
496
+
497
+ // Emit tier start event
498
+ this.emitEvent({
499
+ who: this.config.actor ?? 'system',
500
+ what: `tier-${tier}-execute`,
501
+ when: tierStartTime,
502
+ where: this.name,
503
+ how: {
504
+ status: 'running',
505
+ metadata: { tier },
506
+ },
507
+ })
508
+
509
+ let lastError: Error | undefined
510
+ let attempts = 0
511
+
512
+ while (attempts <= maxRetries) {
513
+ attempts++
514
+
515
+ try {
516
+ // Execute with timeout using workflow step (durable)
517
+ const result = await workflowStep.do(
518
+ `${this.name}-${tier}-attempt-${attempts}`,
519
+ timeout !== undefined ? { timeout: `${timeout} milliseconds` } : {},
520
+ async () => {
521
+ if (tier === 'code') {
522
+ return (handler as CodeTierHandler<TInput, TOutput>)(input)
523
+ } else {
524
+ return (handler as AiTierHandler<TInput, TOutput>)(input, tierContext)
525
+ }
526
+ }
527
+ )
528
+
529
+ const duration = Date.now() - tierStartTime
530
+
531
+ // Mark step complete
532
+ step.complete()
533
+
534
+ // Emit tier success event
535
+ this.emitEvent({
536
+ who: this.config.actor ?? 'system',
537
+ what: `tier-${tier}-execute`,
538
+ when: Date.now(),
539
+ where: this.name,
540
+ how: {
541
+ status: 'completed',
542
+ duration,
543
+ metadata: { tier, result, attempts },
544
+ },
545
+ })
546
+
547
+ return {
548
+ tier,
549
+ success: true,
550
+ value: result as TOutput,
551
+ duration,
552
+ attempts,
553
+ }
554
+ } catch (error) {
555
+ lastError = error instanceof Error ? error : new Error(String(error))
556
+
557
+ // Check if it's a timeout error
558
+ const isTimeout =
559
+ lastError.message.includes('timed out') ||
560
+ lastError.message.includes('timeout') ||
561
+ lastError.name === 'TimeoutError'
562
+
563
+ // Call custom error handler if provided
564
+ if (tierConfig?.onError) {
565
+ tierConfig.onError(lastError, tier)
566
+ }
567
+
568
+ // If we've exhausted retries, stop
569
+ if (attempts > maxRetries) {
570
+ break
571
+ }
572
+
573
+ // Wait before retry with backoff (using durable sleep)
574
+ if (tierConfig?.retries?.delay) {
575
+ const backoff = tierConfig.retries.backoff ?? 'constant'
576
+ let delay = tierConfig.retries.delay
577
+
578
+ if (backoff === 'exponential') {
579
+ delay = tierConfig.retries.delay * Math.pow(2, attempts - 1)
580
+ } else if (backoff === 'linear') {
581
+ delay = tierConfig.retries.delay * attempts
582
+ }
583
+
584
+ await workflowStep.sleep(`${this.name}-${tier}-retry-wait-${attempts}`, delay)
585
+ }
586
+ }
587
+ }
588
+
589
+ const duration = Date.now() - tierStartTime
590
+ const isTimeout =
591
+ lastError?.message.includes('timed out') ||
592
+ lastError?.message.includes('timeout') ||
593
+ lastError?.name === 'TimeoutError'
594
+
595
+ // Mark step failed
596
+ if (lastError) {
597
+ step.fail(lastError)
598
+ }
599
+
600
+ // Emit tier failure event
601
+ this.emitEvent({
602
+ who: this.config.actor ?? 'system',
603
+ what: `tier-${tier}-execute`,
604
+ when: Date.now(),
605
+ where: this.name,
606
+ ...(lastError?.message !== undefined && { why: lastError.message }),
607
+ how: {
608
+ status: 'failed',
609
+ duration,
610
+ metadata: { tier, error: lastError?.message, attempts },
611
+ },
612
+ })
613
+
614
+ return {
615
+ tier,
616
+ success: false,
617
+ ...(lastError !== undefined && { error: lastError }),
618
+ timedOut: isTimeout,
619
+ duration,
620
+ attempts,
621
+ }
622
+ }
623
+
624
+ /**
625
+ * Get the handler for a tier
626
+ */
627
+ private getTierHandler(
628
+ tier: CapabilityTier
629
+ ): CodeTierHandler<TInput, TOutput> | AiTierHandler<TInput, TOutput> | undefined {
630
+ switch (tier) {
631
+ case 'code':
632
+ return this.config.code
633
+ case 'generative':
634
+ return this.config.generative
635
+ case 'agentic':
636
+ return this.config.agentic
637
+ case 'human':
638
+ return this.config.human
639
+ default:
640
+ return undefined
641
+ }
642
+ }
643
+
644
+ /**
645
+ * Get the next configured tier
646
+ */
647
+ private getNextConfiguredTier(currentTier: CapabilityTier): CapabilityTier | undefined {
648
+ const currentIndex = TIER_ORDER.indexOf(currentTier)
649
+ for (let i = currentIndex + 1; i < TIER_ORDER.length; i++) {
650
+ const tier = TIER_ORDER[i]
651
+ if (tier && this.getTierHandler(tier)) {
652
+ return tier
653
+ }
654
+ }
655
+ return undefined
656
+ }
657
+
658
+ /**
659
+ * Emit a 5W+H event
660
+ */
661
+ private emitEvent(event: FiveWHEvent): void {
662
+ if (this.config.onEvent) {
663
+ this.config.onEvent(event)
664
+ }
665
+ }
666
+
667
+ /**
668
+ * Create a mock AI binding for testing
669
+ */
670
+ private createMockAi(): AiBinding {
671
+ return {
672
+ run: async <T>(): Promise<T> => ({ response: 'mock response' } as unknown as T),
673
+ }
674
+ }
675
+ }
676
+
677
+ // ============================================================================
678
+ // Helper Functions
679
+ // ============================================================================
680
+
681
+ /**
682
+ * Create a durable cascade step for common AI props patterns
683
+ *
684
+ * @example
685
+ * ```typescript
686
+ * const titleCascade = createDurableCascadeStep({
687
+ * name: 'generate-title',
688
+ * code: async (input) => {
689
+ * if (input.template) return { title: input.template }
690
+ * throw new Error('No template')
691
+ * },
692
+ * generative: async (input, ctx) => {
693
+ * const result = await ctx.ai.run('@cf/meta/llama-3-8b-instruct', {
694
+ * messages: [{ role: 'user', content: `Generate a title for: ${input.topic}` }]
695
+ * })
696
+ * return { title: result.response }
697
+ * }
698
+ * })
699
+ *
700
+ * // In workflow
701
+ * const result = await titleCascade.run(step, { topic: 'AI' })
702
+ * ```
703
+ */
704
+ export function createDurableCascadeStep<TInput = unknown, TOutput = unknown>(config: {
705
+ name: string
706
+ code?: CodeTierHandler<TInput, TOutput>
707
+ generative?: AiTierHandler<TInput, TOutput>
708
+ agentic?: AiTierHandler<TInput, TOutput>
709
+ human?: HumanTierHandler<TInput, TOutput>
710
+ timeouts?: Partial<Record<CapabilityTier, number>>
711
+ tierConfig?: DurableCascadeConfig<TInput, TOutput>['tierConfig']
712
+ onEvent?: (event: FiveWHEvent) => void
713
+ actor?: string
714
+ }): DurableCascadeExecutor<TInput, TOutput> {
715
+ const durableConfig: DurableCascadeConfig<TInput, TOutput> = {}
716
+
717
+ if (config.code) {
718
+ durableConfig.code = config.code
719
+ }
720
+ if (config.generative) {
721
+ durableConfig.generative = config.generative
722
+ }
723
+ if (config.agentic) {
724
+ durableConfig.agentic = config.agentic
725
+ }
726
+ if (config.human) {
727
+ durableConfig.human = config.human
728
+ }
729
+ if (config.timeouts) {
730
+ durableConfig.timeouts = config.timeouts
731
+ }
732
+ if (config.tierConfig) {
733
+ durableConfig.tierConfig = config.tierConfig
734
+ }
735
+ if (config.onEvent) {
736
+ durableConfig.onEvent = config.onEvent
737
+ }
738
+ if (config.actor) {
739
+ durableConfig.actor = config.actor
740
+ }
741
+
742
+ return new DurableCascadeExecutor(config.name, durableConfig)
743
+ }