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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +14 -1
- package/README.md +2 -0
- package/dist/barrier.d.ts +6 -0
- package/dist/barrier.d.ts.map +1 -1
- package/dist/barrier.js +45 -7
- package/dist/barrier.js.map +1 -1
- package/dist/cascade-context.d.ts.map +1 -1
- package/dist/cascade-context.js +25 -25
- package/dist/cascade-context.js.map +1 -1
- package/dist/cascade-executor.d.ts.map +1 -1
- package/dist/cascade-executor.js +1 -1
- package/dist/cascade-executor.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +23 -7
- package/dist/context.js.map +1 -1
- package/dist/cron-parser.d.ts +65 -0
- package/dist/cron-parser.d.ts.map +1 -0
- package/dist/cron-parser.js +294 -0
- package/dist/cron-parser.js.map +1 -0
- package/dist/cron-scheduler.d.ts +117 -0
- package/dist/cron-scheduler.d.ts.map +1 -0
- package/dist/cron-scheduler.js +176 -0
- package/dist/cron-scheduler.js.map +1 -0
- package/dist/database-context.d.ts +184 -0
- package/dist/database-context.d.ts.map +1 -0
- package/dist/database-context.js +428 -0
- package/dist/database-context.js.map +1 -0
- package/dist/digital-objects-adapter.d.ts +159 -0
- package/dist/digital-objects-adapter.d.ts.map +1 -0
- package/dist/digital-objects-adapter.js +229 -0
- package/dist/digital-objects-adapter.js.map +1 -0
- package/dist/durable-execution-cloudflare.d.ts +427 -0
- package/dist/durable-execution-cloudflare.d.ts.map +1 -0
- package/dist/durable-execution-cloudflare.js +510 -0
- package/dist/durable-execution-cloudflare.js.map +1 -0
- package/dist/durable-execution.d.ts +482 -0
- package/dist/durable-execution.d.ts.map +1 -0
- package/dist/durable-execution.js +594 -0
- package/dist/durable-execution.js.map +1 -0
- package/dist/durable-workflow.d.ts +176 -0
- package/dist/durable-workflow.d.ts.map +1 -0
- package/dist/durable-workflow.js +552 -0
- package/dist/durable-workflow.js.map +1 -0
- package/dist/graph/topological-sort.d.ts.map +1 -1
- package/dist/graph/topological-sort.js +5 -5
- package/dist/graph/topological-sort.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +101 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +115 -0
- package/dist/logger.js.map +1 -0
- package/dist/on.d.ts.map +1 -1
- package/dist/on.js +3 -3
- package/dist/on.js.map +1 -1
- package/dist/runtime.d.ts +169 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +275 -0
- package/dist/runtime.js.map +1 -0
- package/dist/send.d.ts.map +1 -1
- package/dist/send.js +4 -3
- package/dist/send.js.map +1 -1
- package/dist/telemetry.d.ts +150 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +388 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/timer-registry.d.ts +25 -0
- package/dist/timer-registry.d.ts.map +1 -1
- package/dist/timer-registry.js +42 -8
- package/dist/timer-registry.js.map +1 -1
- package/dist/types.d.ts +17 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/worker/durable-step.d.ts +481 -0
- package/dist/worker/durable-step.d.ts.map +1 -0
- package/dist/worker/durable-step.js +606 -0
- package/dist/worker/durable-step.js.map +1 -0
- package/dist/worker/index.d.ts +106 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +124 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/state-adapter.d.ts +230 -0
- package/dist/worker/state-adapter.d.ts.map +1 -0
- package/dist/worker/state-adapter.js +409 -0
- package/dist/worker/state-adapter.js.map +1 -0
- package/dist/worker/topological-executor.d.ts +282 -0
- package/dist/worker/topological-executor.d.ts.map +1 -0
- package/dist/worker/topological-executor.js +396 -0
- package/dist/worker/topological-executor.js.map +1 -0
- package/dist/worker/workflow-builder.d.ts +286 -0
- package/dist/worker/workflow-builder.d.ts.map +1 -0
- package/dist/worker/workflow-builder.js +565 -0
- package/dist/worker/workflow-builder.js.map +1 -0
- package/dist/worker.d.ts +800 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +2428 -0
- package/dist/worker.js.map +1 -0
- package/dist/workflow-builder.d.ts +287 -0
- package/dist/workflow-builder.d.ts.map +1 -0
- package/dist/workflow-builder.js +762 -0
- package/dist/workflow-builder.js.map +1 -0
- package/dist/workflow.d.ts +14 -30
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +132 -292
- package/dist/workflow.js.map +1 -1
- package/examples/01-ecommerce-order-pipeline.ts +358 -0
- package/examples/02-content-moderation-cascade.ts +454 -0
- package/examples/03-scheduled-reporting-dependencies.ts +479 -0
- package/examples/04-database-persistence.ts +518 -0
- package/examples/README.md +173 -0
- package/package.json +30 -13
- package/src/__tests__/digital-objects-adapter.test.ts +274 -0
- package/src/__tests__/durable-workflow.test.ts +297 -0
- package/src/barrier.ts +48 -7
- package/src/cascade-context.ts +36 -29
- package/src/cascade-executor.ts +3 -2
- package/src/context.ts +41 -12
- package/src/cron-parser.ts +347 -0
- package/src/cron-scheduler.ts +239 -0
- package/src/database-context.ts +658 -0
- package/src/digital-objects-adapter.ts +351 -0
- package/src/durable-execution-cloudflare.ts +855 -0
- package/src/durable-execution.ts +1042 -0
- package/src/durable-workflow.ts +717 -0
- package/src/graph/topological-sort.ts +6 -8
- package/src/index.ts +69 -0
- package/src/logger.ts +148 -0
- package/src/on.ts +8 -9
- package/src/runtime.ts +436 -0
- package/src/send.ts +4 -5
- package/src/telemetry.ts +577 -0
- package/src/timer-registry.ts +44 -10
- package/src/types.ts +32 -17
- package/src/worker/durable-step.ts +976 -0
- package/src/worker/index.ts +216 -0
- package/src/worker/state-adapter.ts +589 -0
- package/src/worker/topological-executor.ts +625 -0
- package/src/worker/workflow-builder.ts +871 -0
- package/src/worker.ts +2906 -0
- package/src/workflow-builder.ts +1068 -0
- package/src/workflow.ts +188 -351
- package/test/barrier-join.test.ts +32 -24
- package/test/cascade-executor.test.ts +9 -16
- package/test/cron-parser.test.ts +314 -0
- package/test/cron-scheduler.test.ts +291 -0
- package/test/database-context.test.ts +770 -0
- package/test/db-provider-adapter.test.ts +862 -0
- package/test/durable-execution-cloudflare.test.ts +606 -0
- package/test/durable-execution-in-process.test.ts +286 -0
- package/test/durable-execution.test.ts +247 -0
- package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
- package/test/integration.test.ts +442 -0
- package/test/rpc-surface.test.ts +946 -0
- package/test/runtime.test.ts +262 -0
- package/test/schedule-timer-cleanup.test.ts +30 -21
- package/test/send-race-conditions.test.ts +30 -40
- package/test/worker/durable-cascade.test.ts +1117 -0
- package/test/worker/durable-step.test.ts +723 -0
- package/test/worker/topological-executor.test.ts +1240 -0
- package/test/worker/workflow-builder.test.ts +1067 -0
- package/test/worker.test.ts +608 -0
- package/test/workflow-builder.test.ts +1670 -0
- package/test/workflow-cron.test.ts +256 -0
- package/test/workflow-state-adapter.test.ts +923 -0
- package/test/workflow.test.ts +25 -22
- package/tsconfig.json +3 -1
- package/vitest.config.ts +38 -1
- package/vitest.workers.config.ts +44 -0
- package/wrangler.jsonc +22 -0
- package/.turbo/turbo-test.log +0 -169
- package/LICENSE +0 -21
- package/src/context.js +0 -83
- package/src/every.js +0 -267
- package/src/index.js +0 -71
- package/src/on.js +0 -79
- package/src/send.js +0 -111
- package/src/types.js +0 -4
- package/src/workflow.js +0 -455
- package/test/context.test.js +0 -116
- package/test/every.test.js +0 -282
- package/test/on.test.js +0 -80
- package/test/send.test.js +0 -89
- package/test/workflow.test.js +0 -224
- 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
|
+
}
|