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
package/src/cascade.ts ADDED
@@ -0,0 +1,795 @@
1
+ /**
2
+ * Cascade Executor - Code -> Generative -> Agentic -> Human escalation pattern for AI Props
3
+ *
4
+ * Implements a tiered execution strategy that tries deterministic code first,
5
+ * then escalates to AI-powered prop generation, and finally to human-in-the-loop
6
+ * if all automated approaches fail.
7
+ *
8
+ * ## Features
9
+ *
10
+ * - **Tier Escalation**: Code -> Generative -> Agentic -> Human pattern
11
+ * - **Per-tier Timeouts**: Configurable timeouts for each tier
12
+ * - **5W+H Audit Events**: Who, What, When, Where, Why, How event emission
13
+ * - **Context Propagation**: Share context across tiers
14
+ * - **Retry Support**: Configurable retries per tier with backoff
15
+ * - **AI Gateway Support**: Configuration helpers for Cloudflare AI Gateway
16
+ *
17
+ * ## Basic Usage
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * import { CascadeExecutor, createCascadeStep } from 'ai-props/worker'
22
+ *
23
+ * const generateTitleCascade = new CascadeExecutor({
24
+ * tiers: {
25
+ * code: {
26
+ * name: 'template-title',
27
+ * execute: async (input) => {
28
+ * if (input.template) return { title: input.template }
29
+ * throw new Error('No template')
30
+ * }
31
+ * },
32
+ * generative: {
33
+ * name: 'ai-title',
34
+ * execute: async (input) => {
35
+ * const result = await generateProps({
36
+ * schema: { title: 'A compelling title' },
37
+ * context: input
38
+ * })
39
+ * return result.props
40
+ * }
41
+ * }
42
+ * }
43
+ * })
44
+ *
45
+ * const result = await generateTitleCascade.execute({ topic: 'AI' })
46
+ * console.log(result.value) // { title: '...' }
47
+ * console.log(result.tier) // 'code' or 'generative'
48
+ * ```
49
+ *
50
+ * @packageDocumentation
51
+ */
52
+
53
+ // ============================================================================
54
+ // Constants
55
+ // ============================================================================
56
+
57
+ /**
58
+ * Ordered list of capability tiers
59
+ */
60
+ export const TIER_ORDER = ['code', 'generative', 'agentic', 'human'] as const
61
+
62
+ /**
63
+ * Default timeouts per tier (in milliseconds)
64
+ */
65
+ export const DEFAULT_TIER_TIMEOUTS: Record<CapabilityTier, number> = {
66
+ code: 5000, // 5 seconds
67
+ generative: 30000, // 30 seconds
68
+ agentic: 300000, // 5 minutes
69
+ human: 86400000, // 24 hours
70
+ }
71
+
72
+ // ============================================================================
73
+ // Types
74
+ // ============================================================================
75
+
76
+ /**
77
+ * Capability tier levels
78
+ */
79
+ export type CapabilityTier = 'code' | 'generative' | 'agentic' | 'human'
80
+
81
+ /**
82
+ * 5W+H Event for audit trails
83
+ */
84
+ export interface FiveWHEvent {
85
+ /** Who performed the action */
86
+ who: string
87
+ /** What action was performed */
88
+ what: string
89
+ /** When the action occurred (timestamp) */
90
+ when: number
91
+ /** Where the action occurred (cascade/tier name) */
92
+ where: string
93
+ /** Why the action was performed (reason/error) */
94
+ why?: string
95
+ /** How the action was performed */
96
+ how?: {
97
+ status: 'running' | 'completed' | 'failed'
98
+ duration?: number
99
+ metadata?: Record<string, unknown>
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Cascade step tracking
105
+ */
106
+ export interface CascadeStep {
107
+ id: string
108
+ tier: string
109
+ status: 'pending' | 'running' | 'completed' | 'failed'
110
+ startTime?: number
111
+ endTime?: number
112
+ error?: string
113
+ }
114
+
115
+ /**
116
+ * Cascade context for tracing
117
+ */
118
+ export interface CascadeContext {
119
+ correlationId: string
120
+ name: string
121
+ steps: CascadeStep[]
122
+ }
123
+
124
+ /**
125
+ * Context passed to tier handlers
126
+ */
127
+ export interface TierContext<TInput = unknown> {
128
+ /** Correlation ID for tracing */
129
+ correlationId: string
130
+ /** Current tier being executed */
131
+ tier: CapabilityTier
132
+ /** Input data */
133
+ input: TInput
134
+ /** Cascade context */
135
+ cascadeContext: CascadeContext
136
+ /** Previous tier errors */
137
+ previousErrors: Array<{ tier: CapabilityTier; error: string }>
138
+ }
139
+
140
+ /**
141
+ * Handler for a specific tier
142
+ */
143
+ export interface TierHandler<TInput = unknown, TOutput = unknown> {
144
+ /** Handler name for debugging */
145
+ name: string
146
+ /** Execute the tier logic */
147
+ execute: (input: TInput, context: TierContext<TInput>) => Promise<TOutput>
148
+ }
149
+
150
+ /**
151
+ * Retry configuration per tier
152
+ */
153
+ export interface TierRetryConfig {
154
+ /** Maximum number of retries */
155
+ maxRetries: number
156
+ /** Base delay in milliseconds */
157
+ baseDelay: number
158
+ /** Multiplier for exponential backoff */
159
+ multiplier?: number
160
+ }
161
+
162
+ /**
163
+ * Result from a single tier execution
164
+ */
165
+ export interface TierResult<TOutput = unknown> {
166
+ /** Tier that was executed */
167
+ tier: CapabilityTier
168
+ /** Whether the tier succeeded */
169
+ success: boolean
170
+ /** Result value (if success) */
171
+ value?: TOutput
172
+ /** Error (if failure) */
173
+ error?: Error
174
+ /** Whether the tier timed out */
175
+ timedOut?: boolean
176
+ /** Duration in milliseconds */
177
+ duration: number
178
+ /** Number of attempts */
179
+ attempts?: number
180
+ }
181
+
182
+ /**
183
+ * Metrics from cascade execution
184
+ */
185
+ export interface CascadeMetrics {
186
+ /** Total execution duration */
187
+ totalDuration: number
188
+ /** Duration per tier */
189
+ tierDurations: Partial<Record<CapabilityTier, number>>
190
+ /** Number of retries per tier */
191
+ tierRetries?: Partial<Record<CapabilityTier, number>>
192
+ }
193
+
194
+ /**
195
+ * Result from cascade execution
196
+ */
197
+ export interface CascadeResult<TOutput = unknown> {
198
+ /** Final result value */
199
+ value: TOutput
200
+ /** Tier that produced the result */
201
+ tier: CapabilityTier
202
+ /** History of all tier executions */
203
+ history: TierResult<TOutput>[]
204
+ /** Tiers that were skipped */
205
+ skippedTiers: CapabilityTier[]
206
+ /** Cascade context with tracing info */
207
+ context: CascadeContext
208
+ /** Execution metrics */
209
+ metrics: CascadeMetrics
210
+ }
211
+
212
+ /**
213
+ * Skip condition function
214
+ */
215
+ export type SkipCondition<TInput = unknown> = (input: TInput) => boolean
216
+
217
+ /**
218
+ * Configuration for CascadeExecutor
219
+ */
220
+ export interface CascadeConfig<TInput = unknown, TOutput = unknown> {
221
+ /** Tier handlers */
222
+ tiers: Partial<Record<CapabilityTier, TierHandler<TInput, TOutput>>>
223
+ /** Per-tier timeouts in milliseconds */
224
+ timeouts?: Partial<Record<CapabilityTier, number>>
225
+ /** Total cascade timeout in milliseconds */
226
+ totalTimeout?: number
227
+ /** Use default timeouts from capability-tiers */
228
+ useDefaultTimeouts?: boolean
229
+ /** Actor identifier for 5W+H events */
230
+ actor?: string
231
+ /** Cascade name for 5W+H events */
232
+ cascadeName?: string
233
+ /** Event callback for 5W+H events */
234
+ onEvent?: (event: FiveWHEvent) => void
235
+ /** Skip conditions per tier */
236
+ skipConditions?: Partial<Record<CapabilityTier, SkipCondition<TInput>>>
237
+ /** Retry configuration per tier */
238
+ retryConfig?: Partial<Record<CapabilityTier, TierRetryConfig>>
239
+ }
240
+
241
+ // ============================================================================
242
+ // Errors
243
+ // ============================================================================
244
+
245
+ /**
246
+ * Error thrown when cascade times out
247
+ */
248
+ export class CascadeTimeoutError extends Error {
249
+ public readonly timeout: number
250
+ public readonly elapsed: number
251
+
252
+ constructor(timeout: number, elapsed: number) {
253
+ super(`Cascade timed out after ${elapsed}ms (limit: ${timeout}ms)`)
254
+ this.name = 'CascadeTimeoutError'
255
+ this.timeout = timeout
256
+ this.elapsed = elapsed
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Error thrown when a tier is skipped
262
+ */
263
+ export class TierSkippedError extends Error {
264
+ public readonly tier: CapabilityTier
265
+ public readonly reason: string
266
+
267
+ constructor(tier: CapabilityTier, reason: string) {
268
+ super(`Tier '${tier}' was skipped: ${reason}`)
269
+ this.name = 'TierSkippedError'
270
+ this.tier = tier
271
+ this.reason = reason
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Error thrown when all tiers fail
277
+ */
278
+ export class AllTiersFailedError extends Error {
279
+ public readonly history: TierResult[]
280
+
281
+ constructor(history: TierResult[]) {
282
+ super('All cascade tiers failed')
283
+ this.name = 'AllTiersFailedError'
284
+ this.history = history
285
+ }
286
+ }
287
+
288
+ // ============================================================================
289
+ // Helper Functions
290
+ // ============================================================================
291
+
292
+ /**
293
+ * Create a cascade context for tracing
294
+ */
295
+ export function createCascadeContext(options: { name: string }): CascadeContext {
296
+ return {
297
+ correlationId: `cascade-${Date.now()}-${Math.random().toString(36).slice(2)}`,
298
+ name: options.name,
299
+ steps: [],
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Record a step in the cascade context
305
+ */
306
+ export function recordStep(
307
+ context: CascadeContext,
308
+ tier: string,
309
+ meta: { actor: string; action: string }
310
+ ): {
311
+ complete: () => void
312
+ fail: (error: Error) => void
313
+ } {
314
+ const step: CascadeStep = {
315
+ id: `${tier}-${Date.now()}`,
316
+ tier,
317
+ status: 'running',
318
+ startTime: Date.now(),
319
+ }
320
+ context.steps.push(step)
321
+
322
+ return {
323
+ complete: () => {
324
+ step.status = 'completed'
325
+ step.endTime = Date.now()
326
+ },
327
+ fail: (error: Error) => {
328
+ step.status = 'failed'
329
+ step.endTime = Date.now()
330
+ step.error = error.message
331
+ },
332
+ }
333
+ }
334
+
335
+ // ============================================================================
336
+ // CascadeExecutor
337
+ // ============================================================================
338
+
339
+ /**
340
+ * CascadeExecutor implements the code -> generative -> agentic -> human pattern
341
+ *
342
+ * This is the base executor for cascade patterns, suitable for both
343
+ * synchronous and worker contexts.
344
+ */
345
+ export class CascadeExecutor<TInput = unknown, TOutput = unknown> {
346
+ protected readonly config: CascadeConfig<TInput, TOutput>
347
+ protected readonly actor: string
348
+ protected readonly cascadeName: string
349
+
350
+ constructor(config: CascadeConfig<TInput, TOutput>) {
351
+ this.config = config
352
+ this.actor = config.actor || 'system'
353
+ this.cascadeName = config.cascadeName || 'cascade'
354
+ }
355
+
356
+ /**
357
+ * Execute the cascade
358
+ */
359
+ async execute(input: TInput): Promise<CascadeResult<TOutput>> {
360
+ const startTime = Date.now()
361
+ const context = createCascadeContext({ name: this.cascadeName })
362
+ const history: TierResult<TOutput>[] = []
363
+ const skippedTiers: CapabilityTier[] = []
364
+ const tierDurations: Partial<Record<CapabilityTier, number>> = {}
365
+ const previousErrors: Array<{ tier: CapabilityTier; error: string }> = []
366
+
367
+ // Emit cascade start event
368
+ this.emitEvent({
369
+ who: this.actor,
370
+ what: 'cascade-start',
371
+ when: startTime,
372
+ where: this.cascadeName,
373
+ how: {
374
+ status: 'running',
375
+ metadata: { input },
376
+ },
377
+ })
378
+
379
+ // Set up total timeout if configured
380
+ let totalTimeoutId: ReturnType<typeof setTimeout> | undefined
381
+ let totalTimedOut = false
382
+
383
+ const totalTimeoutPromise = new Promise<never>((_, reject) => {
384
+ if (this.config.totalTimeout) {
385
+ totalTimeoutId = setTimeout(() => {
386
+ totalTimedOut = true
387
+ reject(new CascadeTimeoutError(this.config.totalTimeout!, Date.now() - startTime))
388
+ }, this.config.totalTimeout)
389
+ }
390
+ })
391
+
392
+ try {
393
+ // Execute tiers in order
394
+ for (const tier of TIER_ORDER) {
395
+ if (totalTimedOut) break
396
+
397
+ const handler = this.config.tiers[tier]
398
+
399
+ // Check if tier should be skipped
400
+ if (!handler) {
401
+ skippedTiers.push(tier)
402
+ continue
403
+ }
404
+
405
+ // Check skip condition
406
+ const skipCondition = this.config.skipConditions?.[tier]
407
+ if (skipCondition && skipCondition(input)) {
408
+ skippedTiers.push(tier)
409
+ continue
410
+ }
411
+
412
+ // Execute tier
413
+ const tierResult = await this.executeTier(
414
+ tier,
415
+ handler,
416
+ input,
417
+ context,
418
+ previousErrors,
419
+ startTime,
420
+ totalTimeoutPromise
421
+ )
422
+
423
+ history.push(tierResult)
424
+ tierDurations[tier] = tierResult.duration
425
+
426
+ // If tier succeeded, we're done
427
+ if (tierResult.success && tierResult.value !== undefined) {
428
+ if (totalTimeoutId) clearTimeout(totalTimeoutId)
429
+
430
+ const endTime = Date.now()
431
+ this.emitEvent({
432
+ who: this.actor,
433
+ what: 'cascade-complete',
434
+ when: endTime,
435
+ where: this.cascadeName,
436
+ how: {
437
+ status: 'completed',
438
+ duration: endTime - startTime,
439
+ metadata: { tier, value: tierResult.value },
440
+ },
441
+ })
442
+
443
+ return {
444
+ value: tierResult.value,
445
+ tier,
446
+ history,
447
+ skippedTiers,
448
+ context,
449
+ metrics: {
450
+ totalDuration: endTime - startTime,
451
+ tierDurations,
452
+ },
453
+ }
454
+ }
455
+
456
+ // Record error for next tier
457
+ if (tierResult.error) {
458
+ previousErrors.push({
459
+ tier,
460
+ error: tierResult.error.message,
461
+ })
462
+ }
463
+ }
464
+
465
+ // Check if we timed out
466
+ if (totalTimedOut) {
467
+ if (totalTimeoutId) clearTimeout(totalTimeoutId)
468
+ throw new CascadeTimeoutError(this.config.totalTimeout!, Date.now() - startTime)
469
+ }
470
+
471
+ // All tiers failed
472
+ if (totalTimeoutId) clearTimeout(totalTimeoutId)
473
+ throw new AllTiersFailedError(history)
474
+ } catch (error) {
475
+ if (totalTimeoutId) clearTimeout(totalTimeoutId)
476
+
477
+ const endTime = Date.now()
478
+ this.emitEvent({
479
+ who: this.actor,
480
+ what: 'cascade-failed',
481
+ when: endTime,
482
+ where: this.cascadeName,
483
+ why: error instanceof Error ? error.message : String(error),
484
+ how: {
485
+ status: 'failed',
486
+ duration: endTime - startTime,
487
+ },
488
+ })
489
+
490
+ throw error
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Execute a single tier with timeout and retry support
496
+ */
497
+ protected async executeTier(
498
+ tier: CapabilityTier,
499
+ handler: TierHandler<TInput, TOutput>,
500
+ input: TInput,
501
+ cascadeContext: CascadeContext,
502
+ previousErrors: Array<{ tier: CapabilityTier; error: string }>,
503
+ cascadeStartTime: number,
504
+ totalTimeoutPromise: Promise<never>
505
+ ): Promise<TierResult<TOutput>> {
506
+ const tierStartTime = Date.now()
507
+
508
+ // Record step start
509
+ const step = recordStep(cascadeContext, tier, {
510
+ actor: this.actor,
511
+ action: `execute-${tier}`,
512
+ })
513
+
514
+ // Emit tier start event
515
+ this.emitEvent({
516
+ who: this.actor,
517
+ what: `tier-${tier}-execute`,
518
+ when: tierStartTime,
519
+ where: this.cascadeName,
520
+ how: {
521
+ status: 'running',
522
+ metadata: { tier },
523
+ },
524
+ })
525
+
526
+ // Determine timeout
527
+ const timeout = this.getTierTimeout(tier)
528
+
529
+ // Create tier context
530
+ const tierContext: TierContext<TInput> = {
531
+ correlationId: cascadeContext.correlationId,
532
+ tier,
533
+ input,
534
+ cascadeContext,
535
+ previousErrors: [...previousErrors],
536
+ }
537
+
538
+ // Get retry config
539
+ const retryConfig = this.config.retryConfig?.[tier]
540
+ const maxRetries = retryConfig?.maxRetries ?? 0
541
+
542
+ let lastError: Error | undefined
543
+ let attempts = 0
544
+
545
+ while (attempts <= maxRetries) {
546
+ attempts++
547
+ try {
548
+ // Execute with timeout
549
+ const result = await this.executeWithTimeout(
550
+ () => handler.execute(input, tierContext),
551
+ timeout,
552
+ totalTimeoutPromise
553
+ )
554
+
555
+ const duration = Date.now() - tierStartTime
556
+
557
+ // Mark step complete
558
+ step.complete()
559
+
560
+ // Emit tier success event
561
+ this.emitEvent({
562
+ who: this.actor,
563
+ what: `tier-${tier}-execute`,
564
+ when: Date.now(),
565
+ where: this.cascadeName,
566
+ how: {
567
+ status: 'completed',
568
+ duration,
569
+ metadata: { tier, result, attempts },
570
+ },
571
+ })
572
+
573
+ return {
574
+ tier,
575
+ success: true,
576
+ value: result,
577
+ duration,
578
+ attempts,
579
+ }
580
+ } catch (error) {
581
+ lastError = error instanceof Error ? error : new Error(String(error))
582
+
583
+ // Check if it's a timeout error
584
+ const isTimeout =
585
+ lastError.message.includes('timed out') || lastError.name === 'TimeoutError'
586
+
587
+ // If we've exhausted retries or it's a total timeout, stop
588
+ if (attempts > maxRetries || lastError instanceof CascadeTimeoutError) {
589
+ const duration = Date.now() - tierStartTime
590
+
591
+ // Mark step failed
592
+ step.fail(lastError)
593
+
594
+ // Emit tier failure event
595
+ this.emitEvent({
596
+ who: this.actor,
597
+ what: `tier-${tier}-execute`,
598
+ when: Date.now(),
599
+ where: this.cascadeName,
600
+ why: lastError.message,
601
+ how: {
602
+ status: 'failed',
603
+ duration,
604
+ metadata: { tier, error: lastError.message, attempts },
605
+ },
606
+ })
607
+
608
+ // Emit escalation event if not last tier
609
+ const nextTier = this.getNextConfiguredTier(tier)
610
+ if (nextTier) {
611
+ this.emitEvent({
612
+ who: this.actor,
613
+ what: `escalate-to-${nextTier}`,
614
+ when: Date.now(),
615
+ where: this.cascadeName,
616
+ why: lastError.message,
617
+ how: {
618
+ status: 'running',
619
+ metadata: { fromTier: tier, toTier: nextTier },
620
+ },
621
+ })
622
+ }
623
+
624
+ return {
625
+ tier,
626
+ success: false,
627
+ error: lastError,
628
+ timedOut: isTimeout,
629
+ duration,
630
+ attempts,
631
+ }
632
+ }
633
+
634
+ // Wait before retry with exponential backoff
635
+ if (retryConfig) {
636
+ const delay = retryConfig.baseDelay * Math.pow(retryConfig.multiplier ?? 2, attempts - 1)
637
+ await this.sleep(delay)
638
+ }
639
+ }
640
+ }
641
+
642
+ // Should not reach here, but handle edge case
643
+ const duration = Date.now() - tierStartTime
644
+ return {
645
+ tier,
646
+ success: false,
647
+ ...(lastError !== undefined && { error: lastError }),
648
+ duration,
649
+ attempts,
650
+ }
651
+ }
652
+
653
+ /**
654
+ * Execute a function with a timeout
655
+ */
656
+ protected async executeWithTimeout<R>(
657
+ fn: () => Promise<R>,
658
+ timeout: number | undefined,
659
+ totalTimeoutPromise: Promise<never>
660
+ ): Promise<R> {
661
+ const promises: Promise<R>[] = [fn()]
662
+
663
+ // Add total timeout race
664
+ if (this.config.totalTimeout) {
665
+ promises.push(totalTimeoutPromise)
666
+ }
667
+
668
+ // Add tier timeout
669
+ if (timeout) {
670
+ promises.push(
671
+ new Promise<never>((_, reject) => {
672
+ setTimeout(() => {
673
+ const error = new Error(`Tier timed out after ${timeout}ms`)
674
+ error.name = 'TimeoutError'
675
+ reject(error)
676
+ }, timeout)
677
+ })
678
+ )
679
+ }
680
+
681
+ return Promise.race(promises)
682
+ }
683
+
684
+ /**
685
+ * Get timeout for a tier
686
+ */
687
+ protected getTierTimeout(tier: CapabilityTier): number | undefined {
688
+ if (this.config.timeouts?.[tier]) {
689
+ return this.config.timeouts[tier]
690
+ }
691
+ if (this.config.useDefaultTimeouts) {
692
+ return DEFAULT_TIER_TIMEOUTS[tier]
693
+ }
694
+ return undefined
695
+ }
696
+
697
+ /**
698
+ * Get the next configured tier after the given tier
699
+ */
700
+ protected getNextConfiguredTier(currentTier: CapabilityTier): CapabilityTier | undefined {
701
+ const currentIndex = TIER_ORDER.indexOf(currentTier)
702
+ for (let i = currentIndex + 1; i < TIER_ORDER.length; i++) {
703
+ const tier = TIER_ORDER[i]
704
+ if (tier && this.config.tiers[tier]) {
705
+ return tier
706
+ }
707
+ }
708
+ return undefined
709
+ }
710
+
711
+ /**
712
+ * Emit a 5W+H event
713
+ */
714
+ protected emitEvent(event: FiveWHEvent): void {
715
+ if (this.config.onEvent) {
716
+ this.config.onEvent(event)
717
+ }
718
+ }
719
+
720
+ /**
721
+ * Sleep for a given duration
722
+ */
723
+ protected sleep(ms: number): Promise<void> {
724
+ return new Promise((resolve) => setTimeout(resolve, ms))
725
+ }
726
+ }
727
+
728
+ // ============================================================================
729
+ // Helper Functions
730
+ // ============================================================================
731
+
732
+ /**
733
+ * Create a cascade step configuration for common patterns
734
+ *
735
+ * @example
736
+ * ```typescript
737
+ * const titleCascade = createCascadeStep({
738
+ * name: 'generate-title',
739
+ * code: async (input) => {
740
+ * if (input.template) return { title: input.template }
741
+ * throw new Error('No template')
742
+ * },
743
+ * generative: async (input, ctx) => {
744
+ * const result = await generateProps({
745
+ * schema: { title: 'A compelling title' },
746
+ * context: input
747
+ * })
748
+ * return result.props
749
+ * }
750
+ * })
751
+ * ```
752
+ */
753
+ export function createCascadeStep<TInput = unknown, TOutput = unknown>(config: {
754
+ name: string
755
+ code?: (input: TInput, ctx: TierContext<TInput>) => Promise<TOutput>
756
+ generative?: (input: TInput, ctx: TierContext<TInput>) => Promise<TOutput>
757
+ agentic?: (input: TInput, ctx: TierContext<TInput>) => Promise<TOutput>
758
+ human?: (input: TInput, ctx: TierContext<TInput>) => Promise<TOutput>
759
+ timeouts?: Partial<Record<CapabilityTier, number>>
760
+ retryConfig?: Partial<Record<CapabilityTier, TierRetryConfig>>
761
+ onEvent?: (event: FiveWHEvent) => void
762
+ }): CascadeExecutor<TInput, TOutput> {
763
+ const tiers: Partial<Record<CapabilityTier, TierHandler<TInput, TOutput>>> = {}
764
+
765
+ if (config.code) {
766
+ tiers.code = { name: `${config.name}-code`, execute: config.code }
767
+ }
768
+ if (config.generative) {
769
+ tiers.generative = { name: `${config.name}-generative`, execute: config.generative }
770
+ }
771
+ if (config.agentic) {
772
+ tiers.agentic = { name: `${config.name}-agentic`, execute: config.agentic }
773
+ }
774
+ if (config.human) {
775
+ tiers.human = { name: `${config.name}-human`, execute: config.human }
776
+ }
777
+
778
+ const cascadeConfig: CascadeConfig<TInput, TOutput> = {
779
+ tiers,
780
+ cascadeName: config.name,
781
+ useDefaultTimeouts: true,
782
+ }
783
+
784
+ if (config.timeouts) {
785
+ cascadeConfig.timeouts = config.timeouts
786
+ }
787
+ if (config.retryConfig) {
788
+ cascadeConfig.retryConfig = config.retryConfig
789
+ }
790
+ if (config.onEvent) {
791
+ cascadeConfig.onEvent = config.onEvent
792
+ }
793
+
794
+ return new CascadeExecutor(cascadeConfig)
795
+ }