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.
Files changed (98) hide show
  1. package/.turbo/turbo-build.log +4 -5
  2. package/.turbo/turbo-test.log +169 -0
  3. package/CHANGELOG.md +29 -0
  4. package/LICENSE +21 -0
  5. package/README.md +303 -184
  6. package/dist/barrier.d.ts +153 -0
  7. package/dist/barrier.d.ts.map +1 -0
  8. package/dist/barrier.js +339 -0
  9. package/dist/barrier.js.map +1 -0
  10. package/dist/cascade-context.d.ts +149 -0
  11. package/dist/cascade-context.d.ts.map +1 -0
  12. package/dist/cascade-context.js +324 -0
  13. package/dist/cascade-context.js.map +1 -0
  14. package/dist/cascade-executor.d.ts +196 -0
  15. package/dist/cascade-executor.d.ts.map +1 -0
  16. package/dist/cascade-executor.js +384 -0
  17. package/dist/cascade-executor.js.map +1 -0
  18. package/dist/context.d.ts.map +1 -1
  19. package/dist/context.js +4 -1
  20. package/dist/context.js.map +1 -1
  21. package/dist/dependency-graph.d.ts +157 -0
  22. package/dist/dependency-graph.d.ts.map +1 -0
  23. package/dist/dependency-graph.js +382 -0
  24. package/dist/dependency-graph.js.map +1 -0
  25. package/dist/every.d.ts +31 -2
  26. package/dist/every.d.ts.map +1 -1
  27. package/dist/every.js +63 -32
  28. package/dist/every.js.map +1 -1
  29. package/dist/graph/index.d.ts +8 -0
  30. package/dist/graph/index.d.ts.map +1 -0
  31. package/dist/graph/index.js +8 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/graph/topological-sort.d.ts +121 -0
  34. package/dist/graph/topological-sort.d.ts.map +1 -0
  35. package/dist/graph/topological-sort.js +292 -0
  36. package/dist/graph/topological-sort.js.map +1 -0
  37. package/dist/index.d.ts +6 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +10 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/on.d.ts +35 -10
  42. package/dist/on.d.ts.map +1 -1
  43. package/dist/on.js +52 -18
  44. package/dist/on.js.map +1 -1
  45. package/dist/send.d.ts +0 -5
  46. package/dist/send.d.ts.map +1 -1
  47. package/dist/send.js +1 -14
  48. package/dist/send.js.map +1 -1
  49. package/dist/timer-registry.d.ts +52 -0
  50. package/dist/timer-registry.d.ts.map +1 -0
  51. package/dist/timer-registry.js +120 -0
  52. package/dist/timer-registry.js.map +1 -0
  53. package/dist/types.d.ts +171 -9
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js +17 -1
  56. package/dist/types.js.map +1 -1
  57. package/dist/workflow.d.ts.map +1 -1
  58. package/dist/workflow.js +22 -18
  59. package/dist/workflow.js.map +1 -1
  60. package/package.json +12 -16
  61. package/src/barrier.ts +466 -0
  62. package/src/cascade-context.ts +488 -0
  63. package/src/cascade-executor.ts +587 -0
  64. package/src/context.js +83 -0
  65. package/src/context.ts +12 -7
  66. package/src/dependency-graph.ts +518 -0
  67. package/src/every.js +267 -0
  68. package/src/every.ts +104 -35
  69. package/src/graph/index.ts +19 -0
  70. package/src/graph/topological-sort.ts +414 -0
  71. package/src/index.js +71 -0
  72. package/src/index.ts +78 -0
  73. package/src/on.js +79 -0
  74. package/src/on.ts +81 -25
  75. package/src/send.js +111 -0
  76. package/src/send.ts +1 -16
  77. package/src/timer-registry.ts +145 -0
  78. package/src/types.js +4 -0
  79. package/src/types.ts +218 -11
  80. package/src/workflow.js +455 -0
  81. package/src/workflow.ts +32 -23
  82. package/test/barrier-join.test.ts +434 -0
  83. package/test/barrier-unhandled-rejections.test.ts +359 -0
  84. package/test/cascade-context.test.ts +390 -0
  85. package/test/cascade-executor.test.ts +859 -0
  86. package/test/context.test.js +116 -0
  87. package/test/dependency-graph.test.ts +512 -0
  88. package/test/every.test.js +282 -0
  89. package/test/graph/topological-sort.test.ts +586 -0
  90. package/test/on.test.js +80 -0
  91. package/test/schedule-timer-cleanup.test.ts +344 -0
  92. package/test/send-race-conditions.test.ts +410 -0
  93. package/test/send.test.js +89 -0
  94. package/test/type-safety-every.test.ts +303 -0
  95. package/test/types-event-handler.test.ts +225 -0
  96. package/test/types-proxy-autocomplete.test.ts +345 -0
  97. package/test/workflow.test.js +224 -0
  98. 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
- const noOpEveryProxy = new Proxy(function() {} as any, {
42
- get() {
43
- return () => () => {}
44
- },
45
- apply() {}
46
- }) as EveryProxy
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> {