ai-workflows 2.0.2 → 2.1.3
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 +4 -5
- package/.turbo/turbo-test.log +169 -0
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +303 -184
- package/dist/barrier.d.ts +153 -0
- package/dist/barrier.d.ts.map +1 -0
- package/dist/barrier.js +339 -0
- package/dist/barrier.js.map +1 -0
- package/dist/cascade-context.d.ts +149 -0
- package/dist/cascade-context.d.ts.map +1 -0
- package/dist/cascade-context.js +324 -0
- package/dist/cascade-context.js.map +1 -0
- package/dist/cascade-executor.d.ts +196 -0
- package/dist/cascade-executor.d.ts.map +1 -0
- package/dist/cascade-executor.js +384 -0
- package/dist/cascade-executor.js.map +1 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +4 -1
- package/dist/context.js.map +1 -1
- package/dist/dependency-graph.d.ts +157 -0
- package/dist/dependency-graph.d.ts.map +1 -0
- package/dist/dependency-graph.js +382 -0
- package/dist/dependency-graph.js.map +1 -0
- package/dist/every.d.ts +31 -2
- package/dist/every.d.ts.map +1 -1
- package/dist/every.js +63 -32
- package/dist/every.js.map +1 -1
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +8 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/topological-sort.d.ts +121 -0
- package/dist/graph/topological-sort.d.ts.map +1 -0
- package/dist/graph/topological-sort.js +292 -0
- package/dist/graph/topological-sort.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/on.d.ts +35 -10
- package/dist/on.d.ts.map +1 -1
- package/dist/on.js +52 -18
- package/dist/on.js.map +1 -1
- package/dist/send.d.ts +0 -5
- package/dist/send.d.ts.map +1 -1
- package/dist/send.js +1 -14
- package/dist/send.js.map +1 -1
- package/dist/timer-registry.d.ts +52 -0
- package/dist/timer-registry.d.ts.map +1 -0
- package/dist/timer-registry.js +120 -0
- package/dist/timer-registry.js.map +1 -0
- package/dist/types.d.ts +171 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -1
- package/dist/types.js.map +1 -1
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +22 -18
- package/dist/workflow.js.map +1 -1
- package/package.json +12 -16
- package/src/barrier.ts +466 -0
- package/src/cascade-context.ts +488 -0
- package/src/cascade-executor.ts +587 -0
- package/src/context.js +83 -0
- package/src/context.ts +12 -7
- package/src/dependency-graph.ts +518 -0
- package/src/every.js +267 -0
- package/src/every.ts +104 -35
- package/src/graph/index.ts +19 -0
- package/src/graph/topological-sort.ts +414 -0
- package/src/index.js +71 -0
- package/src/index.ts +78 -0
- package/src/on.js +79 -0
- package/src/on.ts +81 -25
- package/src/send.js +111 -0
- package/src/send.ts +1 -16
- package/src/timer-registry.ts +145 -0
- package/src/types.js +4 -0
- package/src/types.ts +218 -11
- package/src/workflow.js +455 -0
- package/src/workflow.ts +32 -23
- package/test/barrier-join.test.ts +434 -0
- package/test/barrier-unhandled-rejections.test.ts +359 -0
- package/test/cascade-context.test.ts +390 -0
- package/test/cascade-executor.test.ts +859 -0
- package/test/context.test.js +116 -0
- package/test/dependency-graph.test.ts +512 -0
- package/test/every.test.js +282 -0
- package/test/graph/topological-sort.test.ts +586 -0
- package/test/on.test.js +80 -0
- package/test/schedule-timer-cleanup.test.ts +344 -0
- package/test/send-race-conditions.test.ts +410 -0
- package/test/send.test.js +89 -0
- package/test/type-safety-every.test.ts +303 -0
- package/test/types-event-handler.test.ts +225 -0
- package/test/types-proxy-autocomplete.test.ts +345 -0
- package/test/workflow.test.js +224 -0
- package/vitest.config.js +7 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CascadeExecutor - Code -> Generative -> Agentic -> Human escalation pattern
|
|
3
|
+
*
|
|
4
|
+
* Implements a tiered execution strategy that tries deterministic code first,
|
|
5
|
+
* then escalates to AI-powered solutions, and finally to human-in-the-loop
|
|
6
|
+
* if all automated approaches fail.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Tier escalation on failure
|
|
10
|
+
* - Per-tier and total cascade timeouts
|
|
11
|
+
* - 5W+H event emission for audit trails
|
|
12
|
+
* - Context propagation through tiers
|
|
13
|
+
* - Retry support per tier
|
|
14
|
+
* - Custom skip conditions
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
createCascadeContext,
|
|
21
|
+
recordStep,
|
|
22
|
+
type CascadeContext,
|
|
23
|
+
type FiveWHEvent,
|
|
24
|
+
type StepStatus,
|
|
25
|
+
} from './cascade-context.js'
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Constants
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Ordered list of capability tiers
|
|
33
|
+
*/
|
|
34
|
+
export const TIER_ORDER = ['code', 'generative', 'agentic', 'human'] as const
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Default timeouts per tier (from capability-tiers)
|
|
38
|
+
*/
|
|
39
|
+
export const DEFAULT_TIER_TIMEOUTS: Record<CapabilityTier, number> = {
|
|
40
|
+
code: 5000, // 5 seconds
|
|
41
|
+
generative: 30000, // 30 seconds
|
|
42
|
+
agentic: 300000, // 5 minutes
|
|
43
|
+
human: 86400000, // 24 hours
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Types
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Capability tier levels
|
|
52
|
+
*/
|
|
53
|
+
export type CapabilityTier = 'code' | 'generative' | 'agentic' | 'human'
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Context passed to tier handlers
|
|
57
|
+
*/
|
|
58
|
+
export interface TierContext {
|
|
59
|
+
/** Correlation ID for tracing */
|
|
60
|
+
correlationId: string
|
|
61
|
+
/** Current tier being executed */
|
|
62
|
+
tier: CapabilityTier
|
|
63
|
+
/** Input data */
|
|
64
|
+
input: unknown
|
|
65
|
+
/** Cascade context */
|
|
66
|
+
cascadeContext: CascadeContext
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Handler for a specific tier
|
|
71
|
+
*/
|
|
72
|
+
export interface TierHandler<T = unknown> {
|
|
73
|
+
/** Handler name for debugging */
|
|
74
|
+
name: string
|
|
75
|
+
/** Execute the tier logic */
|
|
76
|
+
execute: (input: unknown, context: TierContext) => Promise<T>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Retry configuration per tier
|
|
81
|
+
*/
|
|
82
|
+
export interface TierRetryConfig {
|
|
83
|
+
/** Maximum number of retries */
|
|
84
|
+
maxRetries: number
|
|
85
|
+
/** Base delay in milliseconds */
|
|
86
|
+
baseDelay: number
|
|
87
|
+
/** Multiplier for exponential backoff */
|
|
88
|
+
multiplier?: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Result from a single tier execution
|
|
93
|
+
*/
|
|
94
|
+
export interface TierResult {
|
|
95
|
+
/** Tier that was executed */
|
|
96
|
+
tier: CapabilityTier
|
|
97
|
+
/** Whether the tier succeeded */
|
|
98
|
+
success: boolean
|
|
99
|
+
/** Result value (if success) */
|
|
100
|
+
value?: unknown
|
|
101
|
+
/** Error (if failure) */
|
|
102
|
+
error?: Error
|
|
103
|
+
/** Whether the tier timed out */
|
|
104
|
+
timedOut?: boolean
|
|
105
|
+
/** Duration in milliseconds */
|
|
106
|
+
duration: number
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Metrics from cascade execution
|
|
111
|
+
*/
|
|
112
|
+
export interface CascadeMetrics {
|
|
113
|
+
/** Total execution duration */
|
|
114
|
+
totalDuration: number
|
|
115
|
+
/** Duration per tier */
|
|
116
|
+
tierDurations: Partial<Record<CapabilityTier, number>>
|
|
117
|
+
/** Number of retries per tier */
|
|
118
|
+
tierRetries?: Partial<Record<CapabilityTier, number>>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Result from cascade execution
|
|
123
|
+
*/
|
|
124
|
+
export interface CascadeResult<T = unknown> {
|
|
125
|
+
/** Final result value */
|
|
126
|
+
value: T
|
|
127
|
+
/** Tier that produced the result */
|
|
128
|
+
tier: CapabilityTier
|
|
129
|
+
/** History of all tier executions */
|
|
130
|
+
history: TierResult[]
|
|
131
|
+
/** Tiers that were skipped */
|
|
132
|
+
skippedTiers: CapabilityTier[]
|
|
133
|
+
/** Cascade context with tracing info */
|
|
134
|
+
context: CascadeContext
|
|
135
|
+
/** Execution metrics */
|
|
136
|
+
metrics: CascadeMetrics
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Skip condition function
|
|
141
|
+
*/
|
|
142
|
+
export type SkipCondition = (input: unknown) => boolean
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Configuration for CascadeExecutor
|
|
146
|
+
*/
|
|
147
|
+
export interface CascadeConfig<T = unknown> {
|
|
148
|
+
/** Tier handlers */
|
|
149
|
+
tiers: Partial<Record<CapabilityTier, TierHandler<T>>>
|
|
150
|
+
/** Per-tier timeouts in milliseconds */
|
|
151
|
+
timeouts?: Partial<Record<CapabilityTier, number>>
|
|
152
|
+
/** Total cascade timeout in milliseconds */
|
|
153
|
+
totalTimeout?: number
|
|
154
|
+
/** Use default timeouts from capability-tiers */
|
|
155
|
+
useDefaultTimeouts?: boolean
|
|
156
|
+
/** Actor identifier for 5W+H events */
|
|
157
|
+
actor?: string
|
|
158
|
+
/** Cascade name for 5W+H events */
|
|
159
|
+
cascadeName?: string
|
|
160
|
+
/** Event callback for 5W+H events */
|
|
161
|
+
onEvent?: (event: FiveWHEvent) => void
|
|
162
|
+
/** Skip conditions per tier */
|
|
163
|
+
skipConditions?: Partial<Record<CapabilityTier, SkipCondition>>
|
|
164
|
+
/** Retry configuration per tier */
|
|
165
|
+
retryConfig?: Partial<Record<CapabilityTier, TierRetryConfig>>
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// Errors
|
|
170
|
+
// ============================================================================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Error thrown when cascade times out
|
|
174
|
+
*/
|
|
175
|
+
export class CascadeTimeoutError extends Error {
|
|
176
|
+
public readonly timeout: number
|
|
177
|
+
public readonly elapsed: number
|
|
178
|
+
|
|
179
|
+
constructor(timeout: number, elapsed: number) {
|
|
180
|
+
super(`Cascade timed out after ${elapsed}ms (limit: ${timeout}ms)`)
|
|
181
|
+
this.name = 'CascadeTimeoutError'
|
|
182
|
+
this.timeout = timeout
|
|
183
|
+
this.elapsed = elapsed
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Error thrown when a tier is skipped
|
|
189
|
+
*/
|
|
190
|
+
export class TierSkippedError extends Error {
|
|
191
|
+
public readonly tier: CapabilityTier
|
|
192
|
+
public readonly reason: string
|
|
193
|
+
|
|
194
|
+
constructor(tier: CapabilityTier, reason: string) {
|
|
195
|
+
super(`Tier '${tier}' was skipped: ${reason}`)
|
|
196
|
+
this.name = 'TierSkippedError'
|
|
197
|
+
this.tier = tier
|
|
198
|
+
this.reason = reason
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Error thrown when all tiers fail
|
|
204
|
+
*/
|
|
205
|
+
export class AllTiersFailedError extends Error {
|
|
206
|
+
public readonly history: TierResult[]
|
|
207
|
+
|
|
208
|
+
constructor(history: TierResult[]) {
|
|
209
|
+
super('All cascade tiers failed')
|
|
210
|
+
this.name = 'AllTiersFailedError'
|
|
211
|
+
this.history = history
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ============================================================================
|
|
216
|
+
// CascadeExecutor
|
|
217
|
+
// ============================================================================
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* CascadeExecutor implements the code -> generative -> agentic -> human pattern
|
|
221
|
+
*/
|
|
222
|
+
export class CascadeExecutor<T = unknown> {
|
|
223
|
+
private readonly config: CascadeConfig<T>
|
|
224
|
+
private readonly actor: string
|
|
225
|
+
private readonly cascadeName: string
|
|
226
|
+
|
|
227
|
+
constructor(config: CascadeConfig<T>) {
|
|
228
|
+
this.config = config
|
|
229
|
+
this.actor = config.actor || 'system'
|
|
230
|
+
this.cascadeName = config.cascadeName || 'cascade'
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Execute the cascade
|
|
235
|
+
*/
|
|
236
|
+
async execute(input: unknown): Promise<CascadeResult<T>> {
|
|
237
|
+
const startTime = Date.now()
|
|
238
|
+
const context = createCascadeContext({ name: this.cascadeName })
|
|
239
|
+
const history: TierResult[] = []
|
|
240
|
+
const skippedTiers: CapabilityTier[] = []
|
|
241
|
+
const tierDurations: Partial<Record<CapabilityTier, number>> = {}
|
|
242
|
+
|
|
243
|
+
// Emit cascade start event
|
|
244
|
+
this.emitEvent({
|
|
245
|
+
who: this.actor,
|
|
246
|
+
what: 'cascade-start',
|
|
247
|
+
when: startTime,
|
|
248
|
+
where: this.cascadeName,
|
|
249
|
+
how: {
|
|
250
|
+
status: 'running',
|
|
251
|
+
metadata: { input },
|
|
252
|
+
},
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// Set up total timeout if configured
|
|
256
|
+
let totalTimeoutId: ReturnType<typeof setTimeout> | undefined
|
|
257
|
+
let totalTimedOut = false
|
|
258
|
+
|
|
259
|
+
const totalTimeoutPromise = new Promise<never>((_, reject) => {
|
|
260
|
+
if (this.config.totalTimeout) {
|
|
261
|
+
totalTimeoutId = setTimeout(() => {
|
|
262
|
+
totalTimedOut = true
|
|
263
|
+
reject(new CascadeTimeoutError(this.config.totalTimeout!, Date.now() - startTime))
|
|
264
|
+
}, this.config.totalTimeout)
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
// Execute tiers in order
|
|
270
|
+
for (const tier of TIER_ORDER) {
|
|
271
|
+
if (totalTimedOut) break
|
|
272
|
+
|
|
273
|
+
const handler = this.config.tiers[tier]
|
|
274
|
+
|
|
275
|
+
// Check if tier should be skipped
|
|
276
|
+
if (!handler) {
|
|
277
|
+
skippedTiers.push(tier)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check skip condition
|
|
282
|
+
const skipCondition = this.config.skipConditions?.[tier]
|
|
283
|
+
if (skipCondition && skipCondition(input)) {
|
|
284
|
+
skippedTiers.push(tier)
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Execute tier
|
|
289
|
+
const tierResult = await this.executeTier(
|
|
290
|
+
tier,
|
|
291
|
+
handler,
|
|
292
|
+
input,
|
|
293
|
+
context,
|
|
294
|
+
startTime,
|
|
295
|
+
totalTimeoutPromise
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
history.push(tierResult)
|
|
299
|
+
tierDurations[tier] = tierResult.duration
|
|
300
|
+
|
|
301
|
+
// If tier succeeded, we're done
|
|
302
|
+
if (tierResult.success && tierResult.value !== undefined) {
|
|
303
|
+
if (totalTimeoutId) clearTimeout(totalTimeoutId)
|
|
304
|
+
|
|
305
|
+
const endTime = Date.now()
|
|
306
|
+
this.emitEvent({
|
|
307
|
+
who: this.actor,
|
|
308
|
+
what: 'cascade-complete',
|
|
309
|
+
when: endTime,
|
|
310
|
+
where: this.cascadeName,
|
|
311
|
+
how: {
|
|
312
|
+
status: 'completed',
|
|
313
|
+
duration: endTime - startTime,
|
|
314
|
+
metadata: { tier, value: tierResult.value },
|
|
315
|
+
},
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
value: tierResult.value as T,
|
|
320
|
+
tier,
|
|
321
|
+
history,
|
|
322
|
+
skippedTiers,
|
|
323
|
+
context,
|
|
324
|
+
metrics: {
|
|
325
|
+
totalDuration: endTime - startTime,
|
|
326
|
+
tierDurations,
|
|
327
|
+
},
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Check if we timed out
|
|
333
|
+
if (totalTimedOut) {
|
|
334
|
+
if (totalTimeoutId) clearTimeout(totalTimeoutId)
|
|
335
|
+
throw new CascadeTimeoutError(this.config.totalTimeout!, Date.now() - startTime)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// All tiers failed
|
|
339
|
+
if (totalTimeoutId) clearTimeout(totalTimeoutId)
|
|
340
|
+
throw new AllTiersFailedError(history)
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (totalTimeoutId) clearTimeout(totalTimeoutId)
|
|
343
|
+
|
|
344
|
+
const endTime = Date.now()
|
|
345
|
+
this.emitEvent({
|
|
346
|
+
who: this.actor,
|
|
347
|
+
what: 'cascade-failed',
|
|
348
|
+
when: endTime,
|
|
349
|
+
where: this.cascadeName,
|
|
350
|
+
why: error instanceof Error ? error.message : String(error),
|
|
351
|
+
how: {
|
|
352
|
+
status: 'failed',
|
|
353
|
+
duration: endTime - startTime,
|
|
354
|
+
},
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
throw error
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Execute a single tier with timeout and retry support
|
|
363
|
+
*/
|
|
364
|
+
private async executeTier(
|
|
365
|
+
tier: CapabilityTier,
|
|
366
|
+
handler: TierHandler<T>,
|
|
367
|
+
input: unknown,
|
|
368
|
+
cascadeContext: CascadeContext,
|
|
369
|
+
cascadeStartTime: number,
|
|
370
|
+
totalTimeoutPromise: Promise<never>
|
|
371
|
+
): Promise<TierResult> {
|
|
372
|
+
const tierStartTime = Date.now()
|
|
373
|
+
|
|
374
|
+
// Record step start
|
|
375
|
+
const step = recordStep(cascadeContext, tier, {
|
|
376
|
+
actor: this.actor,
|
|
377
|
+
action: `execute-${tier}`,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// Emit tier start event
|
|
381
|
+
this.emitEvent({
|
|
382
|
+
who: this.actor,
|
|
383
|
+
what: `tier-${tier}-execute`,
|
|
384
|
+
when: tierStartTime,
|
|
385
|
+
where: this.cascadeName,
|
|
386
|
+
how: {
|
|
387
|
+
status: 'running',
|
|
388
|
+
metadata: { tier },
|
|
389
|
+
},
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
// Determine timeout
|
|
393
|
+
const timeout = this.getTierTimeout(tier)
|
|
394
|
+
|
|
395
|
+
// Create tier context
|
|
396
|
+
const tierContext: TierContext = {
|
|
397
|
+
correlationId: cascadeContext.correlationId,
|
|
398
|
+
tier,
|
|
399
|
+
input,
|
|
400
|
+
cascadeContext,
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Get retry config
|
|
404
|
+
const retryConfig = this.config.retryConfig?.[tier]
|
|
405
|
+
const maxRetries = retryConfig?.maxRetries ?? 0
|
|
406
|
+
|
|
407
|
+
let lastError: Error | undefined
|
|
408
|
+
let attempts = 0
|
|
409
|
+
|
|
410
|
+
while (attempts <= maxRetries) {
|
|
411
|
+
try {
|
|
412
|
+
// Execute with timeout
|
|
413
|
+
const result = await this.executeWithTimeout(
|
|
414
|
+
() => handler.execute(input, tierContext),
|
|
415
|
+
timeout,
|
|
416
|
+
totalTimeoutPromise
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
const duration = Date.now() - tierStartTime
|
|
420
|
+
|
|
421
|
+
// Mark step complete
|
|
422
|
+
step.complete()
|
|
423
|
+
|
|
424
|
+
// Emit tier success event
|
|
425
|
+
this.emitEvent({
|
|
426
|
+
who: this.actor,
|
|
427
|
+
what: `tier-${tier}-execute`,
|
|
428
|
+
when: Date.now(),
|
|
429
|
+
where: this.cascadeName,
|
|
430
|
+
how: {
|
|
431
|
+
status: 'completed',
|
|
432
|
+
duration,
|
|
433
|
+
metadata: { tier, result },
|
|
434
|
+
},
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
tier,
|
|
439
|
+
success: true,
|
|
440
|
+
value: result,
|
|
441
|
+
duration,
|
|
442
|
+
}
|
|
443
|
+
} catch (error) {
|
|
444
|
+
lastError = error instanceof Error ? error : new Error(String(error))
|
|
445
|
+
attempts++
|
|
446
|
+
|
|
447
|
+
// Check if it's a timeout error
|
|
448
|
+
const isTimeout = lastError.message.includes('timed out') || lastError.name === 'TimeoutError'
|
|
449
|
+
|
|
450
|
+
// If we've exhausted retries or it's a total timeout, stop
|
|
451
|
+
if (attempts > maxRetries || lastError instanceof CascadeTimeoutError) {
|
|
452
|
+
const duration = Date.now() - tierStartTime
|
|
453
|
+
|
|
454
|
+
// Mark step failed
|
|
455
|
+
step.fail(lastError)
|
|
456
|
+
|
|
457
|
+
// Emit tier failure event
|
|
458
|
+
this.emitEvent({
|
|
459
|
+
who: this.actor,
|
|
460
|
+
what: `tier-${tier}-execute`,
|
|
461
|
+
when: Date.now(),
|
|
462
|
+
where: this.cascadeName,
|
|
463
|
+
why: lastError.message,
|
|
464
|
+
how: {
|
|
465
|
+
status: 'failed',
|
|
466
|
+
duration,
|
|
467
|
+
metadata: { tier, error: lastError.message },
|
|
468
|
+
},
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
// Emit escalation event if not last tier
|
|
472
|
+
const nextTier = this.getNextConfiguredTier(tier)
|
|
473
|
+
if (nextTier) {
|
|
474
|
+
this.emitEvent({
|
|
475
|
+
who: this.actor,
|
|
476
|
+
what: `escalate-to-${nextTier}`,
|
|
477
|
+
when: Date.now(),
|
|
478
|
+
where: this.cascadeName,
|
|
479
|
+
why: lastError.message,
|
|
480
|
+
how: {
|
|
481
|
+
status: 'running',
|
|
482
|
+
metadata: { fromTier: tier, toTier: nextTier },
|
|
483
|
+
},
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
tier,
|
|
489
|
+
success: false,
|
|
490
|
+
error: lastError,
|
|
491
|
+
timedOut: isTimeout,
|
|
492
|
+
duration,
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Wait before retry with exponential backoff
|
|
497
|
+
if (retryConfig) {
|
|
498
|
+
const delay = retryConfig.baseDelay * Math.pow(retryConfig.multiplier ?? 2, attempts - 1)
|
|
499
|
+
await this.sleep(delay)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Should not reach here, but handle edge case
|
|
505
|
+
const duration = Date.now() - tierStartTime
|
|
506
|
+
return {
|
|
507
|
+
tier,
|
|
508
|
+
success: false,
|
|
509
|
+
error: lastError,
|
|
510
|
+
duration,
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Execute a function with a timeout
|
|
516
|
+
*/
|
|
517
|
+
private async executeWithTimeout<R>(
|
|
518
|
+
fn: () => Promise<R>,
|
|
519
|
+
timeout: number | undefined,
|
|
520
|
+
totalTimeoutPromise: Promise<never>
|
|
521
|
+
): Promise<R> {
|
|
522
|
+
const promises: Promise<R>[] = [fn()]
|
|
523
|
+
|
|
524
|
+
// Add total timeout race
|
|
525
|
+
if (this.config.totalTimeout) {
|
|
526
|
+
promises.push(totalTimeoutPromise)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Add tier timeout
|
|
530
|
+
if (timeout) {
|
|
531
|
+
promises.push(
|
|
532
|
+
new Promise<never>((_, reject) => {
|
|
533
|
+
setTimeout(() => {
|
|
534
|
+
const error = new Error(`Tier timed out after ${timeout}ms`)
|
|
535
|
+
error.name = 'TimeoutError'
|
|
536
|
+
reject(error)
|
|
537
|
+
}, timeout)
|
|
538
|
+
})
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return Promise.race(promises)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Get timeout for a tier
|
|
547
|
+
*/
|
|
548
|
+
private getTierTimeout(tier: CapabilityTier): number | undefined {
|
|
549
|
+
if (this.config.timeouts?.[tier]) {
|
|
550
|
+
return this.config.timeouts[tier]
|
|
551
|
+
}
|
|
552
|
+
if (this.config.useDefaultTimeouts) {
|
|
553
|
+
return DEFAULT_TIER_TIMEOUTS[tier]
|
|
554
|
+
}
|
|
555
|
+
return undefined
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Get the next configured tier after the given tier
|
|
560
|
+
*/
|
|
561
|
+
private getNextConfiguredTier(currentTier: CapabilityTier): CapabilityTier | undefined {
|
|
562
|
+
const currentIndex = TIER_ORDER.indexOf(currentTier)
|
|
563
|
+
for (let i = currentIndex + 1; i < TIER_ORDER.length; i++) {
|
|
564
|
+
const tier = TIER_ORDER[i]
|
|
565
|
+
if (tier && this.config.tiers[tier]) {
|
|
566
|
+
return tier
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return undefined
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Emit a 5W+H event
|
|
574
|
+
*/
|
|
575
|
+
private emitEvent(event: FiveWHEvent): void {
|
|
576
|
+
if (this.config.onEvent) {
|
|
577
|
+
this.config.onEvent(event)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Sleep for a given duration
|
|
583
|
+
*/
|
|
584
|
+
private sleep(ms: number): Promise<void> {
|
|
585
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
586
|
+
}
|
|
587
|
+
}
|
package/src/context.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow context implementation
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Create a workflow context
|
|
6
|
+
*/
|
|
7
|
+
export function createWorkflowContext(eventBus) {
|
|
8
|
+
const workflowState = {
|
|
9
|
+
context: {},
|
|
10
|
+
history: [],
|
|
11
|
+
};
|
|
12
|
+
const addHistory = (entry) => {
|
|
13
|
+
workflowState.history.push({
|
|
14
|
+
...entry,
|
|
15
|
+
timestamp: Date.now(),
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
// Create no-op proxies for on/every (these are used in send context, not workflow setup)
|
|
19
|
+
const noOpOnProxy = new Proxy({}, {
|
|
20
|
+
get() {
|
|
21
|
+
return new Proxy({}, {
|
|
22
|
+
get() {
|
|
23
|
+
return () => { };
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const noOpEveryProxy = new Proxy(function () { }, {
|
|
29
|
+
get() {
|
|
30
|
+
return () => () => { };
|
|
31
|
+
},
|
|
32
|
+
apply() { }
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
async send(event, data) {
|
|
36
|
+
addHistory({ type: 'event', name: event, data });
|
|
37
|
+
await eventBus.emit(event, data);
|
|
38
|
+
},
|
|
39
|
+
async do(_event, _data) {
|
|
40
|
+
throw new Error('$.do not available in this context');
|
|
41
|
+
},
|
|
42
|
+
async try(_event, _data) {
|
|
43
|
+
throw new Error('$.try not available in this context');
|
|
44
|
+
},
|
|
45
|
+
on: noOpOnProxy,
|
|
46
|
+
every: noOpEveryProxy,
|
|
47
|
+
state: workflowState.context,
|
|
48
|
+
getState() {
|
|
49
|
+
// Return a deep copy to prevent mutation
|
|
50
|
+
return {
|
|
51
|
+
current: workflowState.current,
|
|
52
|
+
context: { ...workflowState.context },
|
|
53
|
+
history: [...workflowState.history],
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
set(key, value) {
|
|
57
|
+
workflowState.context[key] = value;
|
|
58
|
+
},
|
|
59
|
+
get(key) {
|
|
60
|
+
return workflowState.context[key];
|
|
61
|
+
},
|
|
62
|
+
log(message, data) {
|
|
63
|
+
addHistory({ type: 'action', name: 'log', data: { message, data } });
|
|
64
|
+
console.log(`[workflow] ${message}`, data ?? '');
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Create an isolated workflow context (not connected to event bus)
|
|
70
|
+
* Useful for testing or standalone execution
|
|
71
|
+
*/
|
|
72
|
+
export function createIsolatedContext() {
|
|
73
|
+
const emittedEvents = [];
|
|
74
|
+
const ctx = createWorkflowContext({
|
|
75
|
+
async emit(event, data) {
|
|
76
|
+
emittedEvents.push({ event, data });
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
...ctx,
|
|
81
|
+
getEmittedEvents: () => [...emittedEvents],
|
|
82
|
+
};
|
|
83
|
+
}
|
package/src/context.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Workflow context implementation
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { WorkflowContext, WorkflowState, WorkflowHistoryEntry, OnProxy, EveryProxy } from './types.js'
|
|
5
|
+
import type { WorkflowContext, WorkflowState, WorkflowHistoryEntry, OnProxy, EveryProxy, EveryProxyTarget, ScheduleHandler } from './types.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Event bus interface (imported from send.ts to avoid circular dependency)
|
|
@@ -38,12 +38,17 @@ export function createWorkflowContext(eventBus: EventBusLike): WorkflowContext {
|
|
|
38
38
|
}
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
// Cast to EveryProxy is safe: Proxy handler implements all EveryProxy behaviors dynamically
|
|
42
|
+
const noOpEveryProxy = new Proxy(
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
44
|
+
((_description: string, _handler: ScheduleHandler) => {}) as EveryProxyTarget,
|
|
45
|
+
{
|
|
46
|
+
get() {
|
|
47
|
+
return () => () => {}
|
|
48
|
+
},
|
|
49
|
+
apply() {}
|
|
50
|
+
}
|
|
51
|
+
) as EveryProxy
|
|
47
52
|
|
|
48
53
|
return {
|
|
49
54
|
async send<T = unknown>(event: string, data: T): Promise<void> {
|