ai-props 2.1.3 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dev.vars +2 -0
- package/CHANGELOG.md +11 -0
- package/README.md +2 -0
- package/package.json +39 -13
- package/src/ai.ts +12 -31
- package/src/cascade.ts +795 -0
- package/src/client.ts +440 -0
- package/src/durable-cascade.ts +743 -0
- package/src/event-bridge.ts +478 -0
- package/src/generate.ts +14 -12
- package/src/hoc.ts +15 -19
- package/src/hono-jsx.ts +675 -0
- package/src/index.ts +30 -0
- package/src/mdx-types.ts +169 -0
- package/src/mdx-utils.ts +437 -0
- package/src/mdx.ts +1008 -0
- package/src/rpc.ts +614 -0
- package/src/streaming.ts +618 -0
- package/src/validate.ts +15 -29
- package/src/worker.ts +547 -0
- package/test/cascade.test.ts +338 -0
- package/test/durable-cascade.test.ts +319 -0
- package/test/event-bridge.test.ts +351 -0
- package/test/generate.test.ts +6 -16
- package/test/mdx.test.ts +817 -0
- package/test/worker/capnweb-rpc.test.ts +1084 -0
- package/test/worker/full-flow.integration.test.ts +1463 -0
- package/test/worker/hono-jsx.test.ts +1258 -0
- package/test/worker/mdx-parsing.test.ts +1148 -0
- package/test/worker/setup.ts +56 -0
- package/test/worker.test.ts +595 -0
- package/tsconfig.json +2 -1
- package/vitest.config.js +6 -0
- package/vitest.config.ts +15 -1
- package/vitest.workers.config.ts +58 -0
- package/wrangler.jsonc +27 -0
- package/.turbo/turbo-build.log +0 -4
- package/LICENSE +0 -21
- package/dist/ai.d.ts +0 -125
- package/dist/ai.d.ts.map +0 -1
- package/dist/ai.js +0 -199
- package/dist/ai.js.map +0 -1
- package/dist/cache.d.ts +0 -66
- package/dist/cache.d.ts.map +0 -1
- package/dist/cache.js +0 -183
- package/dist/cache.js.map +0 -1
- package/dist/generate.d.ts +0 -69
- package/dist/generate.d.ts.map +0 -1
- package/dist/generate.js +0 -221
- package/dist/generate.js.map +0 -1
- package/dist/hoc.d.ts +0 -164
- package/dist/hoc.d.ts.map +0 -1
- package/dist/hoc.js +0 -236
- package/dist/hoc.js.map +0 -1
- package/dist/index.d.ts +0 -15
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -21
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts +0 -152
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -7
- package/dist/types.js.map +0 -1
- package/dist/validate.d.ts +0 -58
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js +0 -253
- package/dist/validate.js.map +0 -1
- package/src/ai.js +0 -198
- package/src/cache.js +0 -182
- package/src/generate.js +0 -220
- package/src/hoc.js +0 -235
- package/src/index.js +0 -20
- package/src/types.js +0 -6
- 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
|
+
}
|