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,488 @@
1
+ /**
2
+ * Cascade Context - Correlation IDs and step metadata for workflow execution
3
+ *
4
+ * Provides distributed tracing support with:
5
+ * - Unique correlation IDs per cascade
6
+ * - Step timing metadata (started_at, completed_at, duration)
7
+ * - Cascade path recording
8
+ * - Context inheritance for nested operations
9
+ * - Serialization for distributed systems
10
+ * - W3C Trace Context compatibility
11
+ */
12
+
13
+ /**
14
+ * Step status in the cascade
15
+ */
16
+ export type StepStatus = 'running' | 'completed' | 'failed'
17
+
18
+ /**
19
+ * 5W+H Event structure
20
+ */
21
+ export interface FiveWHEvent {
22
+ who: string
23
+ what: string
24
+ when: number
25
+ where: string
26
+ why?: string
27
+ how: {
28
+ duration?: number
29
+ status: StepStatus
30
+ metadata?: Record<string, unknown>
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Cascade step with timing and metadata
36
+ */
37
+ export interface CascadeStep {
38
+ /** Step name */
39
+ name: string
40
+ /** When the step started (unix timestamp ms) */
41
+ startedAt: number
42
+ /** When the step completed (unix timestamp ms) */
43
+ completedAt?: number
44
+ /** Duration in milliseconds */
45
+ duration?: number
46
+ /** Current status */
47
+ status: StepStatus
48
+ /** Error if failed */
49
+ error?: Error
50
+ /** Additional metadata */
51
+ metadata?: Record<string, unknown>
52
+ /** Mark step as completed */
53
+ complete: () => void
54
+ /** Mark step as failed */
55
+ fail: (error: Error) => void
56
+ /** Add metadata to the step */
57
+ addMetadata: (data: Record<string, unknown>) => void
58
+ /** Convert step to 5W+H event format */
59
+ to5WHEvent: () => FiveWHEvent
60
+ }
61
+
62
+ /**
63
+ * Serialized cascade step (for transmission/storage)
64
+ */
65
+ export interface SerializedCascadeStep {
66
+ name: string
67
+ startedAt: number
68
+ completedAt?: number
69
+ duration?: number
70
+ status: StepStatus
71
+ metadata?: Record<string, unknown>
72
+ }
73
+
74
+ /**
75
+ * W3C Trace Context
76
+ */
77
+ export interface TraceContext {
78
+ traceparent: string
79
+ tracestate?: string
80
+ }
81
+
82
+ /**
83
+ * Serialized cascade context
84
+ */
85
+ export interface SerializedCascadeContext {
86
+ correlationId: string
87
+ spanId: string
88
+ parentId?: string
89
+ name?: string
90
+ depth: number
91
+ steps: SerializedCascadeStep[]
92
+ path: string[]
93
+ createdAt: number
94
+ }
95
+
96
+ /**
97
+ * Cascade context for tracking workflow execution
98
+ */
99
+ export interface CascadeContext {
100
+ /** Unique correlation ID for this cascade (propagates to children) */
101
+ correlationId: string
102
+ /** Unique span ID for this context */
103
+ spanId: string
104
+ /** Parent span ID (if this is a child context) */
105
+ parentId?: string
106
+ /** Name of this cascade/context */
107
+ name?: string
108
+ /** Depth in the context hierarchy (0 = root) */
109
+ depth: number
110
+ /** Steps recorded in this context */
111
+ steps: CascadeStep[]
112
+ /** Path of step names in this context */
113
+ path: string[]
114
+ /** Full path including parent context steps */
115
+ fullPath: string[]
116
+ /** When the context was created */
117
+ createdAt: number
118
+ /** Parent context reference */
119
+ parent?: CascadeContext
120
+ /** Serialize context for transmission */
121
+ serialize: () => SerializedCascadeContext
122
+ /** Format context as readable string */
123
+ format: () => string
124
+ /** Format as tree including parent contexts */
125
+ formatTree: () => string
126
+ /** Convert to W3C Trace Context format */
127
+ toTraceContext: () => TraceContext
128
+ }
129
+
130
+ /**
131
+ * Options for creating a cascade context
132
+ */
133
+ export interface CascadeContextOptions {
134
+ /** Custom correlation ID (auto-generated if not provided) */
135
+ correlationId?: string
136
+ /** Parent context (for nested operations) */
137
+ parent?: CascadeContext
138
+ /** Name for this cascade */
139
+ name?: string
140
+ /** Restore from serialized format */
141
+ fromSerialized?: SerializedCascadeContext
142
+ /** Restore from W3C trace context */
143
+ fromTraceContext?: TraceContext
144
+ }
145
+
146
+ /**
147
+ * Generate a UUID v4
148
+ */
149
+ function generateUUID(): string {
150
+ // Use crypto.randomUUID if available, otherwise fallback
151
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
152
+ return crypto.randomUUID()
153
+ }
154
+ // Fallback UUID v4 generation
155
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
156
+ const r = (Math.random() * 16) | 0
157
+ const v = c === 'x' ? r : (r & 0x3) | 0x8
158
+ return v.toString(16)
159
+ })
160
+ }
161
+
162
+ /**
163
+ * Generate a span ID (16 hex characters)
164
+ */
165
+ function generateSpanId(): string {
166
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
167
+ return crypto.randomUUID().replace(/-/g, '').slice(0, 16)
168
+ }
169
+ return 'xxxxxxxxxxxxxxxx'.replace(/x/g, () => {
170
+ return ((Math.random() * 16) | 0).toString(16)
171
+ })
172
+ }
173
+
174
+ /**
175
+ * Parse W3C traceparent header
176
+ */
177
+ function parseTraceparent(traceparent: string): { traceId: string; parentId: string } | null {
178
+ const parts = traceparent.split('-')
179
+ if (parts.length !== 4) return null
180
+ return {
181
+ traceId: parts[1]!, // 32 hex chars
182
+ parentId: parts[2]!, // 16 hex chars
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Convert correlation ID to trace ID format (32 hex chars)
188
+ */
189
+ function correlationIdToTraceId(correlationId: string): string {
190
+ // Remove dashes and pad/truncate to 32 chars
191
+ const hex = correlationId.replace(/-/g, '')
192
+ return hex.slice(0, 32).padEnd(32, '0')
193
+ }
194
+
195
+ /**
196
+ * Convert trace ID to correlation ID format (UUID)
197
+ */
198
+ function traceIdToCorrelationId(traceId: string): string {
199
+ // Convert 32 hex chars to UUID format
200
+ return `${traceId.slice(0, 8)}-${traceId.slice(8, 12)}-${traceId.slice(12, 16)}-${traceId.slice(16, 20)}-${traceId.slice(20, 32)}`
201
+ }
202
+
203
+ /**
204
+ * Create a cascade context for tracking workflow execution
205
+ */
206
+ export function createCascadeContext(options: CascadeContextOptions = {}): CascadeContext {
207
+ const { parent, name, fromSerialized, fromTraceContext } = options
208
+
209
+ // Handle restoration from serialized format
210
+ if (fromSerialized) {
211
+ const restoredSteps: CascadeStep[] = fromSerialized.steps.map((s) => createRestoredStep(s, name))
212
+
213
+ const ctx: CascadeContext = {
214
+ correlationId: fromSerialized.correlationId,
215
+ spanId: fromSerialized.spanId,
216
+ parentId: fromSerialized.parentId,
217
+ name: fromSerialized.name,
218
+ depth: fromSerialized.depth,
219
+ steps: restoredSteps,
220
+ path: fromSerialized.path,
221
+ createdAt: fromSerialized.createdAt,
222
+ get fullPath() {
223
+ return this.path
224
+ },
225
+ serialize: () => serializeContext(ctx),
226
+ format: () => formatContext(ctx),
227
+ formatTree: () => formatContextTree(ctx),
228
+ toTraceContext: () => toTraceContext(ctx),
229
+ }
230
+ return ctx
231
+ }
232
+
233
+ // Handle restoration from W3C trace context
234
+ if (fromTraceContext) {
235
+ const parsed = parseTraceparent(fromTraceContext.traceparent)
236
+ if (parsed) {
237
+ const correlationId = traceIdToCorrelationId(parsed.traceId)
238
+ const newSpanId = generateSpanId()
239
+
240
+ const ctx: CascadeContext = {
241
+ correlationId,
242
+ spanId: newSpanId,
243
+ parentId: parsed.parentId,
244
+ name,
245
+ depth: 0,
246
+ steps: [],
247
+ path: [],
248
+ createdAt: Date.now(),
249
+ get fullPath() {
250
+ return this.path
251
+ },
252
+ serialize: () => serializeContext(ctx),
253
+ format: () => formatContext(ctx),
254
+ formatTree: () => formatContextTree(ctx),
255
+ toTraceContext: () => toTraceContext(ctx),
256
+ }
257
+ return ctx
258
+ }
259
+ }
260
+
261
+ // Determine correlation ID
262
+ let correlationId: string
263
+ if (parent) {
264
+ correlationId = parent.correlationId
265
+ } else if (options.correlationId) {
266
+ correlationId = options.correlationId
267
+ } else {
268
+ correlationId = generateUUID()
269
+ }
270
+
271
+ const spanId = generateSpanId()
272
+ const parentId = parent?.spanId
273
+ const depth = parent ? parent.depth + 1 : 0
274
+ const createdAt = Date.now()
275
+
276
+ const steps: CascadeStep[] = []
277
+ const path: string[] = []
278
+
279
+ const ctx: CascadeContext = {
280
+ correlationId,
281
+ spanId,
282
+ parentId,
283
+ name,
284
+ depth,
285
+ steps,
286
+ path,
287
+ createdAt,
288
+ parent,
289
+ get fullPath() {
290
+ if (this.parent) {
291
+ return [...this.parent.fullPath, ...this.path]
292
+ }
293
+ return this.path
294
+ },
295
+ serialize: () => serializeContext(ctx),
296
+ format: () => formatContext(ctx),
297
+ formatTree: () => formatContextTree(ctx),
298
+ toTraceContext: () => toTraceContext(ctx),
299
+ }
300
+
301
+ return ctx
302
+ }
303
+
304
+ /**
305
+ * Create a restored step from serialized data
306
+ */
307
+ function createRestoredStep(serialized: SerializedCascadeStep, contextName?: string): CascadeStep {
308
+ const step: CascadeStep = {
309
+ name: serialized.name,
310
+ startedAt: serialized.startedAt,
311
+ completedAt: serialized.completedAt,
312
+ duration: serialized.duration,
313
+ status: serialized.status,
314
+ metadata: serialized.metadata ? { ...serialized.metadata } : undefined,
315
+ complete: () => {
316
+ step.status = 'completed'
317
+ step.completedAt = Date.now()
318
+ step.duration = step.completedAt - step.startedAt
319
+ },
320
+ fail: (error: Error) => {
321
+ step.status = 'failed'
322
+ step.error = error
323
+ step.completedAt = Date.now()
324
+ step.duration = step.completedAt - step.startedAt
325
+ },
326
+ addMetadata: (data: Record<string, unknown>) => {
327
+ step.metadata = { ...step.metadata, ...data }
328
+ },
329
+ to5WHEvent: () => ({
330
+ who: (step.metadata?.actor as string) || 'system',
331
+ what: (step.metadata?.action as string) || step.name,
332
+ when: step.startedAt,
333
+ where: contextName || 'unknown',
334
+ why: step.metadata?.reason as string,
335
+ how: {
336
+ duration: step.duration,
337
+ status: step.status,
338
+ metadata: step.metadata,
339
+ },
340
+ }),
341
+ }
342
+ return step
343
+ }
344
+
345
+ /**
346
+ * Record a step in the cascade context
347
+ */
348
+ export function recordStep(
349
+ ctx: CascadeContext,
350
+ name: string,
351
+ metadata?: Record<string, unknown>
352
+ ): CascadeStep {
353
+ const startedAt = Date.now()
354
+
355
+ const step: CascadeStep = {
356
+ name,
357
+ startedAt,
358
+ status: 'running',
359
+ metadata: metadata ? { ...metadata } : undefined,
360
+ complete: () => {
361
+ step.status = 'completed'
362
+ step.completedAt = Date.now()
363
+ step.duration = step.completedAt - step.startedAt
364
+ ctx.path.push(name)
365
+ },
366
+ fail: (error: Error) => {
367
+ step.status = 'failed'
368
+ step.error = error
369
+ step.completedAt = Date.now()
370
+ step.duration = step.completedAt - step.startedAt
371
+ },
372
+ addMetadata: (data: Record<string, unknown>) => {
373
+ step.metadata = { ...step.metadata, ...data }
374
+ },
375
+ to5WHEvent: () => ({
376
+ who: (step.metadata?.actor as string) || 'system',
377
+ what: (step.metadata?.action as string) || name,
378
+ when: startedAt,
379
+ where: ctx.name || 'cascade',
380
+ why: step.metadata?.reason as string,
381
+ how: {
382
+ duration: step.duration,
383
+ status: step.status,
384
+ metadata: step.metadata,
385
+ },
386
+ }),
387
+ }
388
+
389
+ ctx.steps.push(step)
390
+ return step
391
+ }
392
+
393
+ /**
394
+ * Execute a callback with cascade context
395
+ */
396
+ export async function withCascadeContext<T>(
397
+ callback: (ctx: CascadeContext) => Promise<T>,
398
+ options: CascadeContextOptions = {}
399
+ ): Promise<T> {
400
+ const ctx = createCascadeContext(options)
401
+ return callback(ctx)
402
+ }
403
+
404
+ /**
405
+ * Serialize context for transmission/storage
406
+ */
407
+ function serializeContext(ctx: CascadeContext): SerializedCascadeContext {
408
+ return {
409
+ correlationId: ctx.correlationId,
410
+ spanId: ctx.spanId,
411
+ parentId: ctx.parentId,
412
+ name: ctx.name,
413
+ depth: ctx.depth,
414
+ steps: ctx.steps.map((step) => ({
415
+ name: step.name,
416
+ startedAt: step.startedAt,
417
+ completedAt: step.completedAt,
418
+ duration: step.duration,
419
+ status: step.status,
420
+ metadata: step.metadata,
421
+ })),
422
+ path: ctx.path,
423
+ createdAt: ctx.createdAt,
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Format context as readable string
429
+ */
430
+ function formatContext(ctx: CascadeContext): string {
431
+ const lines: string[] = []
432
+ lines.push(`Cascade: ${ctx.name || 'unnamed'}`)
433
+ lines.push(` Correlation ID: ${ctx.correlationId}`)
434
+ lines.push(` Span ID: ${ctx.spanId}`)
435
+ if (ctx.parentId) {
436
+ lines.push(` Parent ID: ${ctx.parentId}`)
437
+ }
438
+ lines.push(` Depth: ${ctx.depth}`)
439
+ lines.push(` Steps:`)
440
+ for (const step of ctx.steps) {
441
+ const status = step.status === 'completed' ? '[OK]' : step.status === 'failed' ? '[FAIL]' : '[...]'
442
+ const duration = step.duration ? ` (${step.duration}ms)` : ''
443
+ lines.push(` ${status} ${step.name}${duration}`)
444
+ }
445
+ return lines.join('\n')
446
+ }
447
+
448
+ /**
449
+ * Format context as tree including parent contexts
450
+ */
451
+ function formatContextTree(ctx: CascadeContext): string {
452
+ const lines: string[] = []
453
+
454
+ // Build tree from root
455
+ const contexts: CascadeContext[] = []
456
+ let current: CascadeContext | undefined = ctx
457
+ while (current) {
458
+ contexts.unshift(current)
459
+ current = current.parent
460
+ }
461
+
462
+ for (let i = 0; i < contexts.length; i++) {
463
+ const context = contexts[i]!
464
+ const indent = ' '.repeat(i)
465
+ lines.push(`${indent}${context.name || 'cascade'} (depth: ${context.depth})`)
466
+ for (const step of context.steps) {
467
+ const status = step.status === 'completed' ? '[OK]' : step.status === 'failed' ? '[FAIL]' : '[...]'
468
+ const duration = step.duration ? ` (${step.duration}ms)` : ''
469
+ lines.push(`${indent} ${status} ${step.name}${duration}`)
470
+ }
471
+ }
472
+
473
+ return lines.join('\n')
474
+ }
475
+
476
+ /**
477
+ * Convert to W3C Trace Context format
478
+ */
479
+ function toTraceContext(ctx: CascadeContext): TraceContext {
480
+ const version = '00'
481
+ const traceId = correlationIdToTraceId(ctx.correlationId)
482
+ const spanId = ctx.spanId.padEnd(16, '0').slice(0, 16)
483
+ const flags = '01' // sampled
484
+
485
+ return {
486
+ traceparent: `${version}-${traceId}-${spanId}-${flags}`,
487
+ }
488
+ }