ai-props 2.1.1 → 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 +24 -0
- package/README.md +131 -118
- package/package.json +30 -4
- 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 -5
- 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,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Cascade Executor
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
6
|
+
import {
|
|
7
|
+
CascadeExecutor,
|
|
8
|
+
createCascadeStep,
|
|
9
|
+
TIER_ORDER,
|
|
10
|
+
DEFAULT_TIER_TIMEOUTS,
|
|
11
|
+
AllTiersFailedError,
|
|
12
|
+
CascadeTimeoutError,
|
|
13
|
+
type CascadeConfig,
|
|
14
|
+
type TierContext,
|
|
15
|
+
type FiveWHEvent,
|
|
16
|
+
} from '../src/cascade.js'
|
|
17
|
+
|
|
18
|
+
describe('CascadeExecutor', () => {
|
|
19
|
+
describe('basic execution', () => {
|
|
20
|
+
it('should execute code tier successfully', async () => {
|
|
21
|
+
const executor = new CascadeExecutor<{ value: number }, { result: number }>({
|
|
22
|
+
tiers: {
|
|
23
|
+
code: {
|
|
24
|
+
name: 'test-code',
|
|
25
|
+
execute: async (input) => ({ result: input.value * 2 }),
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const result = await executor.execute({ value: 5 })
|
|
31
|
+
|
|
32
|
+
expect(result.value).toEqual({ result: 10 })
|
|
33
|
+
expect(result.tier).toBe('code')
|
|
34
|
+
expect(result.history).toHaveLength(1)
|
|
35
|
+
expect(result.history[0]!.success).toBe(true)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should escalate to next tier on failure', async () => {
|
|
39
|
+
const executor = new CascadeExecutor<{ value: number }, { result: number }>({
|
|
40
|
+
tiers: {
|
|
41
|
+
code: {
|
|
42
|
+
name: 'test-code',
|
|
43
|
+
execute: async () => {
|
|
44
|
+
throw new Error('Code tier failed')
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
generative: {
|
|
48
|
+
name: 'test-generative',
|
|
49
|
+
execute: async (input) => ({ result: input.value * 3 }),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const result = await executor.execute({ value: 5 })
|
|
55
|
+
|
|
56
|
+
expect(result.value).toEqual({ result: 15 })
|
|
57
|
+
expect(result.tier).toBe('generative')
|
|
58
|
+
expect(result.history).toHaveLength(2)
|
|
59
|
+
expect(result.history[0]!.success).toBe(false)
|
|
60
|
+
expect(result.history[1]!.success).toBe(true)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should throw AllTiersFailedError when all tiers fail', async () => {
|
|
64
|
+
const executor = new CascadeExecutor({
|
|
65
|
+
tiers: {
|
|
66
|
+
code: {
|
|
67
|
+
name: 'test-code',
|
|
68
|
+
execute: async () => {
|
|
69
|
+
throw new Error('Code failed')
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
generative: {
|
|
73
|
+
name: 'test-generative',
|
|
74
|
+
execute: async () => {
|
|
75
|
+
throw new Error('Generative failed')
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
await expect(executor.execute({ value: 5 })).rejects.toThrow(AllTiersFailedError)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should skip unconfigured tiers', async () => {
|
|
85
|
+
const executor = new CascadeExecutor<{ value: number }, { result: number }>({
|
|
86
|
+
tiers: {
|
|
87
|
+
generative: {
|
|
88
|
+
name: 'test-generative',
|
|
89
|
+
execute: async (input) => ({ result: input.value }),
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const result = await executor.execute({ value: 10 })
|
|
95
|
+
|
|
96
|
+
expect(result.tier).toBe('generative')
|
|
97
|
+
expect(result.skippedTiers).toContain('code')
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('timeouts', () => {
|
|
102
|
+
it('should use default timeouts when configured', async () => {
|
|
103
|
+
const executor = new CascadeExecutor({
|
|
104
|
+
tiers: {
|
|
105
|
+
code: {
|
|
106
|
+
name: 'test-code',
|
|
107
|
+
execute: async () => ({ success: true }),
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
useDefaultTimeouts: true,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Default code timeout is 5000ms
|
|
114
|
+
expect(DEFAULT_TIER_TIMEOUTS.code).toBe(5000)
|
|
115
|
+
|
|
116
|
+
const result = await executor.execute({})
|
|
117
|
+
expect(result.tier).toBe('code')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should throw CascadeTimeoutError on total timeout', async () => {
|
|
121
|
+
const executor = new CascadeExecutor({
|
|
122
|
+
tiers: {
|
|
123
|
+
code: {
|
|
124
|
+
name: 'slow-code',
|
|
125
|
+
execute: async () => {
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
127
|
+
return { success: true }
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
totalTimeout: 10,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
await expect(executor.execute({})).rejects.toThrow(CascadeTimeoutError)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('5W+H events', () => {
|
|
139
|
+
it('should emit start and complete events', async () => {
|
|
140
|
+
const events: FiveWHEvent[] = []
|
|
141
|
+
|
|
142
|
+
const executor = new CascadeExecutor({
|
|
143
|
+
tiers: {
|
|
144
|
+
code: {
|
|
145
|
+
name: 'test-code',
|
|
146
|
+
execute: async () => ({ success: true }),
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
cascadeName: 'test-cascade',
|
|
150
|
+
actor: 'test-user',
|
|
151
|
+
onEvent: (event) => events.push(event),
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
await executor.execute({})
|
|
155
|
+
|
|
156
|
+
expect(events.length).toBeGreaterThanOrEqual(2)
|
|
157
|
+
expect(events[0]!.what).toBe('cascade-start')
|
|
158
|
+
expect(events[0]!.who).toBe('test-user')
|
|
159
|
+
expect(events[0]!.where).toBe('test-cascade')
|
|
160
|
+
|
|
161
|
+
const completeEvent = events.find((e) => e.what === 'cascade-complete')
|
|
162
|
+
expect(completeEvent).toBeDefined()
|
|
163
|
+
expect(completeEvent!.how?.status).toBe('completed')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should emit escalation events on tier failure', async () => {
|
|
167
|
+
const events: FiveWHEvent[] = []
|
|
168
|
+
|
|
169
|
+
const executor = new CascadeExecutor({
|
|
170
|
+
tiers: {
|
|
171
|
+
code: {
|
|
172
|
+
name: 'test-code',
|
|
173
|
+
execute: async () => {
|
|
174
|
+
throw new Error('Code failed')
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
generative: {
|
|
178
|
+
name: 'test-generative',
|
|
179
|
+
execute: async () => ({ success: true }),
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
onEvent: (event) => events.push(event),
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
await executor.execute({})
|
|
186
|
+
|
|
187
|
+
const escalationEvent = events.find((e) => e.what === 'escalate-to-generative')
|
|
188
|
+
expect(escalationEvent).toBeDefined()
|
|
189
|
+
expect(escalationEvent!.why).toBe('Code failed')
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('retry configuration', () => {
|
|
194
|
+
it('should retry tier on failure', async () => {
|
|
195
|
+
let attempts = 0
|
|
196
|
+
|
|
197
|
+
const executor = new CascadeExecutor({
|
|
198
|
+
tiers: {
|
|
199
|
+
code: {
|
|
200
|
+
name: 'retry-code',
|
|
201
|
+
execute: async () => {
|
|
202
|
+
attempts++
|
|
203
|
+
if (attempts < 3) {
|
|
204
|
+
throw new Error('Not yet')
|
|
205
|
+
}
|
|
206
|
+
return { success: true }
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
retryConfig: {
|
|
211
|
+
code: {
|
|
212
|
+
maxRetries: 3,
|
|
213
|
+
baseDelay: 10,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const result = await executor.execute({})
|
|
219
|
+
|
|
220
|
+
expect(result.tier).toBe('code')
|
|
221
|
+
expect(result.history[0]!.attempts).toBe(3)
|
|
222
|
+
expect(attempts).toBe(3)
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
describe('skip conditions', () => {
|
|
227
|
+
it('should skip tier when condition returns true', async () => {
|
|
228
|
+
const executor = new CascadeExecutor<{ skipCode?: boolean }, { result: string }>({
|
|
229
|
+
tiers: {
|
|
230
|
+
code: {
|
|
231
|
+
name: 'skippable-code',
|
|
232
|
+
execute: async () => ({ result: 'code' }),
|
|
233
|
+
},
|
|
234
|
+
generative: {
|
|
235
|
+
name: 'test-generative',
|
|
236
|
+
execute: async () => ({ result: 'generative' }),
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
skipConditions: {
|
|
240
|
+
code: (input) => !!input.skipCode,
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const result = await executor.execute({ skipCode: true })
|
|
245
|
+
|
|
246
|
+
expect(result.tier).toBe('generative')
|
|
247
|
+
expect(result.skippedTiers).toContain('code')
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
describe('context propagation', () => {
|
|
252
|
+
it('should pass previous errors to tier context', async () => {
|
|
253
|
+
let receivedPreviousErrors: Array<{ tier: string; error: string }> | undefined
|
|
254
|
+
|
|
255
|
+
const executor = new CascadeExecutor({
|
|
256
|
+
tiers: {
|
|
257
|
+
code: {
|
|
258
|
+
name: 'failing-code',
|
|
259
|
+
execute: async () => {
|
|
260
|
+
throw new Error('Code error')
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
generative: {
|
|
264
|
+
name: 'test-generative',
|
|
265
|
+
execute: async (_, ctx) => {
|
|
266
|
+
receivedPreviousErrors = ctx.previousErrors
|
|
267
|
+
return { success: true }
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
await executor.execute({})
|
|
274
|
+
|
|
275
|
+
expect(receivedPreviousErrors).toBeDefined()
|
|
276
|
+
expect(receivedPreviousErrors).toHaveLength(1)
|
|
277
|
+
expect(receivedPreviousErrors![0]!.tier).toBe('code')
|
|
278
|
+
expect(receivedPreviousErrors![0]!.error).toBe('Code error')
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
describe('createCascadeStep', () => {
|
|
284
|
+
it('should create executor with configured tiers', async () => {
|
|
285
|
+
const cascade = createCascadeStep({
|
|
286
|
+
name: 'test-cascade',
|
|
287
|
+
code: async (input: { value: number }) => ({ result: input.value }),
|
|
288
|
+
generative: async (input: { value: number }) => ({ result: input.value * 2 }),
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const result = await cascade.execute({ value: 5 })
|
|
292
|
+
|
|
293
|
+
expect(result.value).toEqual({ result: 5 })
|
|
294
|
+
expect(result.tier).toBe('code')
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('should use default timeouts', async () => {
|
|
298
|
+
const cascade = createCascadeStep({
|
|
299
|
+
name: 'test-cascade',
|
|
300
|
+
code: async () => ({ success: true }),
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// The cascade should have default timeouts enabled
|
|
304
|
+
const result = await cascade.execute({})
|
|
305
|
+
expect(result.tier).toBe('code')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('should accept custom timeouts', async () => {
|
|
309
|
+
const cascade = createCascadeStep({
|
|
310
|
+
name: 'test-cascade',
|
|
311
|
+
code: async () => {
|
|
312
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
313
|
+
return { success: true }
|
|
314
|
+
},
|
|
315
|
+
timeouts: {
|
|
316
|
+
code: 10,
|
|
317
|
+
},
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
// Should timeout since execution takes longer than 10ms
|
|
321
|
+
await expect(cascade.execute({})).rejects.toThrow()
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
describe('Constants', () => {
|
|
326
|
+
it('should have correct tier order', () => {
|
|
327
|
+
expect(TIER_ORDER).toEqual(['code', 'generative', 'agentic', 'human'])
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('should have correct default timeouts', () => {
|
|
331
|
+
expect(DEFAULT_TIER_TIMEOUTS).toEqual({
|
|
332
|
+
code: 5000,
|
|
333
|
+
generative: 30000,
|
|
334
|
+
agentic: 300000,
|
|
335
|
+
human: 86400000,
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
})
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for DurableCascadeExecutor
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
6
|
+
import {
|
|
7
|
+
DurableCascadeExecutor,
|
|
8
|
+
createDurableCascadeStep,
|
|
9
|
+
createAIGatewayConfig,
|
|
10
|
+
type WorkflowStep,
|
|
11
|
+
type DurableCascadeTierContext,
|
|
12
|
+
type FiveWHEvent,
|
|
13
|
+
} from '../src/durable-cascade.js'
|
|
14
|
+
import { AllTiersFailedError, CascadeTimeoutError } from '../src/cascade.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a mock workflow step for testing
|
|
18
|
+
*/
|
|
19
|
+
function createMockWorkflowStep(): WorkflowStep {
|
|
20
|
+
return {
|
|
21
|
+
do: vi.fn(
|
|
22
|
+
async (name: string, configOrCallback: unknown, maybeCallback?: () => Promise<unknown>) => {
|
|
23
|
+
const callback = typeof configOrCallback === 'function' ? configOrCallback : maybeCallback
|
|
24
|
+
return callback?.()
|
|
25
|
+
}
|
|
26
|
+
),
|
|
27
|
+
sleep: vi.fn(async () => {}),
|
|
28
|
+
sleepUntil: vi.fn(async () => {}),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('DurableCascadeExecutor', () => {
|
|
33
|
+
describe('basic execution', () => {
|
|
34
|
+
it('should execute code tier successfully', async () => {
|
|
35
|
+
const step = createMockWorkflowStep()
|
|
36
|
+
const executor = new DurableCascadeExecutor<{ value: number }, { result: number }>('test', {
|
|
37
|
+
code: async (input) => ({ result: input.value * 2 }),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const result = await executor.run(step, { value: 5 })
|
|
41
|
+
|
|
42
|
+
expect(result.value).toEqual({ result: 10 })
|
|
43
|
+
expect(result.tier).toBe('code')
|
|
44
|
+
expect(step.do).toHaveBeenCalled()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should escalate to generative tier on code failure', async () => {
|
|
48
|
+
const step = createMockWorkflowStep()
|
|
49
|
+
const executor = new DurableCascadeExecutor<{ value: number }, { result: number }>('test', {
|
|
50
|
+
code: async () => {
|
|
51
|
+
throw new Error('Code failed')
|
|
52
|
+
},
|
|
53
|
+
generative: async (input) => ({ result: input.value * 3 }),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const result = await executor.run(step, { value: 5 })
|
|
57
|
+
|
|
58
|
+
expect(result.value).toEqual({ result: 15 })
|
|
59
|
+
expect(result.tier).toBe('generative')
|
|
60
|
+
expect(result.history).toHaveLength(2)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should throw AllTiersFailedError when all tiers fail', async () => {
|
|
64
|
+
const step = createMockWorkflowStep()
|
|
65
|
+
const executor = new DurableCascadeExecutor('test', {
|
|
66
|
+
code: async () => {
|
|
67
|
+
throw new Error('Code failed')
|
|
68
|
+
},
|
|
69
|
+
generative: async () => {
|
|
70
|
+
throw new Error('Generative failed')
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
await expect(executor.run(step, {})).rejects.toThrow(AllTiersFailedError)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('AI binding', () => {
|
|
79
|
+
it('should pass AI binding to generative tier', async () => {
|
|
80
|
+
const step = createMockWorkflowStep()
|
|
81
|
+
const mockAi = {
|
|
82
|
+
run: vi.fn(async () => ({ response: 'AI generated content' })),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const executor = new DurableCascadeExecutor<{ prompt: string }, { content: string }>('test', {
|
|
86
|
+
generative: async (input, ctx) => {
|
|
87
|
+
const result = await ctx.ai.run('@cf/meta/llama-3-8b-instruct', {
|
|
88
|
+
messages: [{ role: 'user', content: input.prompt }],
|
|
89
|
+
})
|
|
90
|
+
return { content: result.response ?? '' }
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const result = await executor.run(step, { prompt: 'Hello' }, { ai: mockAi })
|
|
95
|
+
|
|
96
|
+
expect(result.value.content).toBe('AI generated content')
|
|
97
|
+
expect(mockAi.run).toHaveBeenCalled()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should use setAi method', async () => {
|
|
101
|
+
const step = createMockWorkflowStep()
|
|
102
|
+
const mockAi = {
|
|
103
|
+
run: vi.fn(async () => ({ response: 'Configured AI response' })),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const executor = new DurableCascadeExecutor<{ prompt: string }, { content: string }>('test', {
|
|
107
|
+
generative: async (input, ctx) => {
|
|
108
|
+
const result = await ctx.ai.run('@cf/meta/llama-3-8b-instruct', {
|
|
109
|
+
messages: [{ role: 'user', content: input.prompt }],
|
|
110
|
+
})
|
|
111
|
+
return { content: result.response ?? '' }
|
|
112
|
+
},
|
|
113
|
+
}).setAi(mockAi)
|
|
114
|
+
|
|
115
|
+
const result = await executor.run(step, { prompt: 'Test' })
|
|
116
|
+
|
|
117
|
+
expect(result.value.content).toBe('Configured AI response')
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('human review', () => {
|
|
122
|
+
it('should handle human review requests', async () => {
|
|
123
|
+
const step = createMockWorkflowStep()
|
|
124
|
+
const mockHumanReviewHandler = vi.fn(async () => ({
|
|
125
|
+
reviewId: 'review-123',
|
|
126
|
+
status: 'approved',
|
|
127
|
+
}))
|
|
128
|
+
|
|
129
|
+
const executor = new DurableCascadeExecutor<{ data: string }, { approved: boolean }>('test', {
|
|
130
|
+
human: async (input, ctx) => {
|
|
131
|
+
const review = await ctx.requestHumanReview({
|
|
132
|
+
type: 'approval',
|
|
133
|
+
data: input.data,
|
|
134
|
+
})
|
|
135
|
+
return { approved: review.status === 'approved' }
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const result = await executor.run(
|
|
140
|
+
step,
|
|
141
|
+
{ data: 'test' },
|
|
142
|
+
{ humanReviewHandler: mockHumanReviewHandler }
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
expect(result.value.approved).toBe(true)
|
|
146
|
+
expect(mockHumanReviewHandler).toHaveBeenCalledWith({
|
|
147
|
+
type: 'approval',
|
|
148
|
+
data: 'test',
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('5W+H events', () => {
|
|
154
|
+
it('should emit cascade events', async () => {
|
|
155
|
+
const step = createMockWorkflowStep()
|
|
156
|
+
const events: FiveWHEvent[] = []
|
|
157
|
+
|
|
158
|
+
const executor = new DurableCascadeExecutor('test-cascade', {
|
|
159
|
+
code: async () => ({ success: true }),
|
|
160
|
+
actor: 'test-user',
|
|
161
|
+
onEvent: (event) => events.push(event),
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
await executor.run(step, {})
|
|
165
|
+
|
|
166
|
+
expect(events.length).toBeGreaterThanOrEqual(2)
|
|
167
|
+
|
|
168
|
+
const startEvent = events.find((e) => e.what === 'cascade-start')
|
|
169
|
+
expect(startEvent).toBeDefined()
|
|
170
|
+
expect(startEvent!.who).toBe('test-user')
|
|
171
|
+
|
|
172
|
+
const completeEvent = events.find((e) => e.what === 'cascade-complete')
|
|
173
|
+
expect(completeEvent).toBeDefined()
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('tier configuration', () => {
|
|
178
|
+
it('should apply custom success condition', async () => {
|
|
179
|
+
const step = createMockWorkflowStep()
|
|
180
|
+
const executor = new DurableCascadeExecutor<unknown, { valid: boolean }>('test', {
|
|
181
|
+
code: async () => ({ valid: false }),
|
|
182
|
+
generative: async () => ({ valid: true }),
|
|
183
|
+
tierConfig: {
|
|
184
|
+
code: {
|
|
185
|
+
successCondition: (result) => (result as { valid: boolean }).valid,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const result = await executor.run(step, {})
|
|
191
|
+
|
|
192
|
+
// Code tier returned { valid: false }, which fails successCondition
|
|
193
|
+
// So it should escalate to generative
|
|
194
|
+
expect(result.tier).toBe('generative')
|
|
195
|
+
expect(result.value.valid).toBe(true)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should call onError handler on tier failure', async () => {
|
|
199
|
+
const step = createMockWorkflowStep()
|
|
200
|
+
const onError = vi.fn()
|
|
201
|
+
|
|
202
|
+
const executor = new DurableCascadeExecutor('test', {
|
|
203
|
+
code: async () => {
|
|
204
|
+
throw new Error('Test error')
|
|
205
|
+
},
|
|
206
|
+
generative: async () => ({ success: true }),
|
|
207
|
+
tierConfig: {
|
|
208
|
+
code: {
|
|
209
|
+
onError,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
await executor.run(step, {})
|
|
215
|
+
|
|
216
|
+
expect(onError).toHaveBeenCalledWith(expect.any(Error), 'code')
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
describe('createDurableCascadeStep', () => {
|
|
222
|
+
it('should create executor with configured handlers', async () => {
|
|
223
|
+
const step = createMockWorkflowStep()
|
|
224
|
+
const cascade = createDurableCascadeStep({
|
|
225
|
+
name: 'test-cascade',
|
|
226
|
+
code: async (input: { value: number }) => ({ result: input.value }),
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const result = await cascade.run(step, { value: 10 })
|
|
230
|
+
|
|
231
|
+
expect(result.value).toEqual({ result: 10 })
|
|
232
|
+
expect(result.tier).toBe('code')
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('should support all tier types', async () => {
|
|
236
|
+
const step = createMockWorkflowStep()
|
|
237
|
+
const mockAi = {
|
|
238
|
+
run: vi.fn(async () => ({ response: 'agentic' })),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const cascade = createDurableCascadeStep({
|
|
242
|
+
name: 'full-cascade',
|
|
243
|
+
code: async () => {
|
|
244
|
+
throw new Error('Skip')
|
|
245
|
+
},
|
|
246
|
+
generative: async () => {
|
|
247
|
+
throw new Error('Skip')
|
|
248
|
+
},
|
|
249
|
+
agentic: async (_, ctx) => {
|
|
250
|
+
const result = await ctx.ai.run('model', { messages: [] })
|
|
251
|
+
return { tier: result.response }
|
|
252
|
+
},
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
const result = await cascade.run(step, {}, { ai: mockAi })
|
|
256
|
+
|
|
257
|
+
expect(result.tier).toBe('agentic')
|
|
258
|
+
expect(result.value).toEqual({ tier: 'agentic' })
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
describe('createAIGatewayConfig', () => {
|
|
263
|
+
it('should create gateway configuration', () => {
|
|
264
|
+
const config = createAIGatewayConfig({
|
|
265
|
+
gatewayId: 'my-gateway',
|
|
266
|
+
accountId: 'account-123',
|
|
267
|
+
cacheTtl: 3600,
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
expect(config.config.gatewayId).toBe('my-gateway')
|
|
271
|
+
expect(config.config.accountId).toBe('account-123')
|
|
272
|
+
expect(config.config.cacheTtl).toBe(3600)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('should generate request options', () => {
|
|
276
|
+
const config = createAIGatewayConfig({
|
|
277
|
+
gatewayId: 'my-gateway',
|
|
278
|
+
accountId: 'account-123',
|
|
279
|
+
cacheTtl: 3600,
|
|
280
|
+
skipCache: false,
|
|
281
|
+
metadata: { key: 'value' },
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
const options = config.toRequestOptions()
|
|
285
|
+
|
|
286
|
+
expect(options).toEqual({
|
|
287
|
+
gateway: {
|
|
288
|
+
id: 'my-gateway',
|
|
289
|
+
skipCache: false,
|
|
290
|
+
cacheTtl: 3600,
|
|
291
|
+
metadata: { key: 'value' },
|
|
292
|
+
},
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('should generate cache key', () => {
|
|
297
|
+
const config = createAIGatewayConfig({
|
|
298
|
+
gatewayId: 'my-gateway',
|
|
299
|
+
accountId: 'account-123',
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
const key = config.getCacheKey({ prompt: 'test' })
|
|
303
|
+
|
|
304
|
+
expect(key).toContain('my-gateway:')
|
|
305
|
+
expect(key).toContain('prompt')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('should use default values', () => {
|
|
309
|
+
const config = createAIGatewayConfig({
|
|
310
|
+
gatewayId: 'gateway',
|
|
311
|
+
accountId: 'account',
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const options = config.toRequestOptions()
|
|
315
|
+
|
|
316
|
+
expect((options.gateway as { cacheTtl: number }).cacheTtl).toBe(0)
|
|
317
|
+
expect((options.gateway as { skipCache: boolean }).skipCache).toBe(false)
|
|
318
|
+
})
|
|
319
|
+
})
|