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,518 @@
1
+ /**
2
+ * Dependency Graph Architecture for Workflow Steps
3
+ *
4
+ * Provides a directed acyclic graph (DAG) for managing workflow step dependencies.
5
+ * Supports:
6
+ * - Hard vs soft dependencies
7
+ * - Cycle detection with path reporting
8
+ * - Parallel group identification for concurrent execution
9
+ * - Graph visualization (DOT, JSON)
10
+ */
11
+
12
+ import type { EventRegistration } from './types.js'
13
+
14
+ /**
15
+ * Dependency type: hard (must succeed) or soft (can proceed on failure)
16
+ */
17
+ export type DependencyType = 'hard' | 'soft'
18
+
19
+ /**
20
+ * Configuration for step dependencies
21
+ */
22
+ export interface DependencyConfig {
23
+ /**
24
+ * Step(s) that must complete before this step runs
25
+ * Can be a single step ID or array of step IDs
26
+ */
27
+ dependsOn: string | string[]
28
+
29
+ /**
30
+ * Type of dependency (default: 'hard')
31
+ * - 'hard': Dependency must complete successfully
32
+ * - 'soft': Step can proceed even if dependency fails
33
+ */
34
+ type?: DependencyType
35
+ }
36
+
37
+ /**
38
+ * A node in the dependency graph
39
+ */
40
+ export interface GraphNode {
41
+ /** Unique identifier (e.g., 'Step1.complete') */
42
+ id: string
43
+
44
+ /** Direct dependencies (step IDs that must complete first) */
45
+ dependencies: string[]
46
+
47
+ /** Map of dependency ID to dependency type */
48
+ dependencyTypes?: Record<string, DependencyType>
49
+
50
+ /** Optional metadata */
51
+ metadata?: Record<string, unknown>
52
+ }
53
+
54
+ /**
55
+ * A group of nodes that can execute in parallel
56
+ */
57
+ export interface ParallelGroup {
58
+ /** Execution level (0 = no dependencies, 1 = depends on level 0, etc.) */
59
+ level: number
60
+
61
+ /** Node IDs that can run concurrently at this level */
62
+ nodes: string[]
63
+ }
64
+
65
+ /**
66
+ * JSON representation of the graph
67
+ */
68
+ export interface GraphJSON {
69
+ nodes: GraphNode[]
70
+ edges: Array<{
71
+ from: string
72
+ to: string
73
+ type: DependencyType
74
+ }>
75
+ }
76
+
77
+ /**
78
+ * Error thrown when a circular dependency is detected
79
+ */
80
+ export class CircularDependencyError extends Error {
81
+ /** The path of nodes forming the cycle */
82
+ cyclePath: string[]
83
+
84
+ constructor(cyclePath: string[]) {
85
+ const pathStr = cyclePath.join(' -> ')
86
+ super(`Circular dependency detected: ${pathStr}`)
87
+ this.name = 'CircularDependencyError'
88
+ this.cyclePath = cyclePath
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Error thrown when a referenced dependency doesn't exist
94
+ */
95
+ export class MissingDependencyError extends Error {
96
+ /** The missing dependency ID */
97
+ dependency: string
98
+
99
+ /** The node that references the missing dependency */
100
+ node: string
101
+
102
+ constructor(dependency: string, node: string) {
103
+ super(
104
+ `Missing dependency '${dependency}' referenced by '${node}'. ` +
105
+ `Ensure '${dependency}' is added to the graph before '${node}'.`
106
+ )
107
+ this.name = 'MissingDependencyError'
108
+ this.dependency = dependency
109
+ this.node = node
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Extended event registration with optional dependencies
115
+ */
116
+ export interface EventRegistrationWithDeps extends EventRegistration {
117
+ dependencies?: DependencyConfig
118
+ }
119
+
120
+ /**
121
+ * Directed Acyclic Graph for workflow step dependencies
122
+ */
123
+ export class DependencyGraph {
124
+ private nodes: Map<string, GraphNode> = new Map()
125
+
126
+ /**
127
+ * Create a DependencyGraph from event registrations
128
+ */
129
+ static fromEventRegistrations(
130
+ registrations: EventRegistrationWithDeps[]
131
+ ): DependencyGraph {
132
+ const graph = new DependencyGraph()
133
+
134
+ // First pass: add all nodes without dependencies
135
+ for (const reg of registrations) {
136
+ const nodeId = `${reg.noun}.${reg.event}`
137
+ if (!graph.hasNode(nodeId)) {
138
+ // Add as placeholder first (will be updated with deps in second pass)
139
+ graph.nodes.set(nodeId, {
140
+ id: nodeId,
141
+ dependencies: [],
142
+ dependencyTypes: {},
143
+ })
144
+ }
145
+ }
146
+
147
+ // Second pass: add dependencies
148
+ for (const reg of registrations) {
149
+ const nodeId = `${reg.noun}.${reg.event}`
150
+ if (reg.dependencies) {
151
+ const deps = Array.isArray(reg.dependencies.dependsOn)
152
+ ? reg.dependencies.dependsOn
153
+ : [reg.dependencies.dependsOn]
154
+
155
+ const depType = reg.dependencies.type || 'hard'
156
+
157
+ for (const dep of deps) {
158
+ if (!graph.hasNode(dep)) {
159
+ throw new MissingDependencyError(dep, nodeId)
160
+ }
161
+ const node = graph.nodes.get(nodeId)!
162
+ if (!node.dependencies.includes(dep)) {
163
+ node.dependencies.push(dep)
164
+ node.dependencyTypes = node.dependencyTypes || {}
165
+ node.dependencyTypes[dep] = depType
166
+ }
167
+ }
168
+
169
+ // Check for cycles after adding edges
170
+ const cycle = graph.detectCycles()
171
+ if (cycle) {
172
+ throw new CircularDependencyError(cycle)
173
+ }
174
+ }
175
+ }
176
+
177
+ return graph
178
+ }
179
+
180
+ /**
181
+ * Check if a node exists
182
+ */
183
+ hasNode(id: string): boolean {
184
+ return this.nodes.has(id)
185
+ }
186
+
187
+ /**
188
+ * Add a node to the graph
189
+ */
190
+ addNode(id: string, config?: DependencyConfig): void {
191
+ // Check for self-reference
192
+ if (config?.dependsOn) {
193
+ const deps = Array.isArray(config.dependsOn)
194
+ ? config.dependsOn
195
+ : [config.dependsOn]
196
+
197
+ if (deps.includes(id)) {
198
+ throw new CircularDependencyError([id, id])
199
+ }
200
+
201
+ // Verify all dependencies exist
202
+ for (const dep of deps) {
203
+ if (!this.nodes.has(dep)) {
204
+ throw new MissingDependencyError(dep, id)
205
+ }
206
+ }
207
+ }
208
+
209
+ const dependencies = config?.dependsOn
210
+ ? Array.isArray(config.dependsOn)
211
+ ? config.dependsOn
212
+ : [config.dependsOn]
213
+ : []
214
+
215
+ const depType = config?.type || 'hard'
216
+ const dependencyTypes: Record<string, DependencyType> = {}
217
+ for (const dep of dependencies) {
218
+ dependencyTypes[dep] = depType
219
+ }
220
+
221
+ this.nodes.set(id, {
222
+ id,
223
+ dependencies,
224
+ dependencyTypes,
225
+ })
226
+
227
+ // Check for cycles after adding node with dependencies
228
+ if (dependencies.length > 0) {
229
+ const cycle = this.detectCycles()
230
+ if (cycle) {
231
+ // Remove the node we just added
232
+ this.nodes.delete(id)
233
+ throw new CircularDependencyError(cycle)
234
+ }
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Add an edge (dependency) between existing nodes
240
+ */
241
+ addEdge(from: string, to: string, type: DependencyType = 'hard'): void {
242
+ if (!this.nodes.has(from)) {
243
+ throw new MissingDependencyError(from, to)
244
+ }
245
+ if (!this.nodes.has(to)) {
246
+ throw new MissingDependencyError(to, from)
247
+ }
248
+
249
+ const toNode = this.nodes.get(to)!
250
+
251
+ // Check for self-reference
252
+ if (from === to) {
253
+ throw new CircularDependencyError([from, to])
254
+ }
255
+
256
+ // Add the dependency
257
+ if (!toNode.dependencies.includes(from)) {
258
+ toNode.dependencies.push(from)
259
+ toNode.dependencyTypes = toNode.dependencyTypes || {}
260
+ toNode.dependencyTypes[from] = type
261
+ }
262
+
263
+ // Check for cycles
264
+ const cycle = this.detectCycles()
265
+ if (cycle) {
266
+ // Rollback the edge
267
+ toNode.dependencies = toNode.dependencies.filter((d) => d !== from)
268
+ if (toNode.dependencyTypes) {
269
+ delete toNode.dependencyTypes[from]
270
+ }
271
+ throw new CircularDependencyError(cycle)
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Get all nodes in the graph
277
+ */
278
+ getNodes(): GraphNode[] {
279
+ return Array.from(this.nodes.values())
280
+ }
281
+
282
+ /**
283
+ * Get a specific node
284
+ */
285
+ getNode(id: string): GraphNode | undefined {
286
+ return this.nodes.get(id)
287
+ }
288
+
289
+ /**
290
+ * Get direct dependencies of a node
291
+ */
292
+ getDependencies(id: string): string[] {
293
+ const node = this.nodes.get(id)
294
+ if (!node) {
295
+ throw new MissingDependencyError(id, 'getDependencies()')
296
+ }
297
+ return [...node.dependencies]
298
+ }
299
+
300
+ /**
301
+ * Get all transitive dependencies of a node
302
+ */
303
+ getAllDependencies(id: string): string[] {
304
+ const node = this.nodes.get(id)
305
+ if (!node) {
306
+ throw new MissingDependencyError(id, 'getAllDependencies()')
307
+ }
308
+
309
+ const visited = new Set<string>()
310
+ const stack = [...node.dependencies]
311
+
312
+ while (stack.length > 0) {
313
+ const current = stack.pop()!
314
+ if (visited.has(current)) continue
315
+ visited.add(current)
316
+
317
+ const currentNode = this.nodes.get(current)
318
+ if (currentNode) {
319
+ for (const dep of currentNode.dependencies) {
320
+ if (!visited.has(dep)) {
321
+ stack.push(dep)
322
+ }
323
+ }
324
+ }
325
+ }
326
+
327
+ return Array.from(visited)
328
+ }
329
+
330
+ /**
331
+ * Get nodes that depend on a given node
332
+ */
333
+ getDependents(id: string): string[] {
334
+ const dependents: string[] = []
335
+ for (const node of this.nodes.values()) {
336
+ if (node.dependencies.includes(id)) {
337
+ dependents.push(node.id)
338
+ }
339
+ }
340
+ return dependents
341
+ }
342
+
343
+ /**
344
+ * Get only hard dependencies
345
+ */
346
+ getHardDependencies(id: string): string[] {
347
+ const node = this.nodes.get(id)
348
+ if (!node) {
349
+ throw new MissingDependencyError(id, 'getHardDependencies()')
350
+ }
351
+
352
+ return node.dependencies.filter(
353
+ (dep) => (node.dependencyTypes?.[dep] || 'hard') === 'hard'
354
+ )
355
+ }
356
+
357
+ /**
358
+ * Get only soft dependencies
359
+ */
360
+ getSoftDependencies(id: string): string[] {
361
+ const node = this.nodes.get(id)
362
+ if (!node) {
363
+ throw new MissingDependencyError(id, 'getSoftDependencies()')
364
+ }
365
+
366
+ return node.dependencies.filter(
367
+ (dep) => node.dependencyTypes?.[dep] === 'soft'
368
+ )
369
+ }
370
+
371
+ /**
372
+ * Detect cycles using DFS
373
+ * Returns the cycle path if found, null otherwise
374
+ */
375
+ detectCycles(): string[] | null {
376
+ const visited = new Set<string>()
377
+ const recursionStack = new Set<string>()
378
+ const path: string[] = []
379
+
380
+ const dfs = (nodeId: string): string[] | null => {
381
+ visited.add(nodeId)
382
+ recursionStack.add(nodeId)
383
+ path.push(nodeId)
384
+
385
+ const node = this.nodes.get(nodeId)
386
+ if (node) {
387
+ for (const dep of node.dependencies) {
388
+ if (!visited.has(dep)) {
389
+ const cycle = dfs(dep)
390
+ if (cycle) return cycle
391
+ } else if (recursionStack.has(dep)) {
392
+ // Found a cycle - construct the cycle path
393
+ const cycleStart = path.indexOf(dep)
394
+ const cyclePath = path.slice(cycleStart)
395
+ cyclePath.push(dep) // Complete the cycle
396
+ return cyclePath
397
+ }
398
+ }
399
+ }
400
+
401
+ path.pop()
402
+ recursionStack.delete(nodeId)
403
+ return null
404
+ }
405
+
406
+ for (const nodeId of this.nodes.keys()) {
407
+ if (!visited.has(nodeId)) {
408
+ const cycle = dfs(nodeId)
409
+ if (cycle) return cycle
410
+ }
411
+ }
412
+
413
+ return null
414
+ }
415
+
416
+ /**
417
+ * Get parallel execution groups
418
+ * Returns nodes grouped by execution level (0 = no deps, 1 = depends on 0, etc.)
419
+ */
420
+ getParallelGroups(): ParallelGroup[] {
421
+ const levels = new Map<string, number>()
422
+
423
+ // Calculate level for each node
424
+ const calculateLevel = (nodeId: string): number => {
425
+ if (levels.has(nodeId)) {
426
+ return levels.get(nodeId)!
427
+ }
428
+
429
+ const node = this.nodes.get(nodeId)
430
+ if (!node || node.dependencies.length === 0) {
431
+ levels.set(nodeId, 0)
432
+ return 0
433
+ }
434
+
435
+ let maxDepLevel = -1
436
+ for (const dep of node.dependencies) {
437
+ const depLevel = calculateLevel(dep)
438
+ maxDepLevel = Math.max(maxDepLevel, depLevel)
439
+ }
440
+
441
+ const level = maxDepLevel + 1
442
+ levels.set(nodeId, level)
443
+ return level
444
+ }
445
+
446
+ // Calculate levels for all nodes
447
+ for (const nodeId of this.nodes.keys()) {
448
+ calculateLevel(nodeId)
449
+ }
450
+
451
+ // Group nodes by level
452
+ const groups = new Map<number, string[]>()
453
+ for (const [nodeId, level] of levels) {
454
+ if (!groups.has(level)) {
455
+ groups.set(level, [])
456
+ }
457
+ groups.get(level)!.push(nodeId)
458
+ }
459
+
460
+ // Convert to sorted array of ParallelGroups
461
+ const result: ParallelGroup[] = []
462
+ const sortedLevels = Array.from(groups.keys()).sort((a, b) => a - b)
463
+ for (const level of sortedLevels) {
464
+ result.push({
465
+ level,
466
+ nodes: groups.get(level)!,
467
+ })
468
+ }
469
+
470
+ return result
471
+ }
472
+
473
+ /**
474
+ * Export graph to DOT format for visualization
475
+ */
476
+ toDot(): string {
477
+ const lines: string[] = ['digraph DependencyGraph {']
478
+ lines.push(' rankdir=TB;')
479
+ lines.push(' node [shape=box];')
480
+
481
+ // Add nodes
482
+ for (const node of this.nodes.values()) {
483
+ const label = node.id.replace(/\./g, '\\n')
484
+ lines.push(` "${node.id}" [label="${label}"];`)
485
+ }
486
+
487
+ // Add edges
488
+ for (const node of this.nodes.values()) {
489
+ for (const dep of node.dependencies) {
490
+ const style = node.dependencyTypes?.[dep] === 'soft' ? 'dashed' : 'solid'
491
+ lines.push(` "${dep}" -> "${node.id}" [style=${style}];`)
492
+ }
493
+ }
494
+
495
+ lines.push('}')
496
+ return lines.join('\n')
497
+ }
498
+
499
+ /**
500
+ * Export graph to JSON format
501
+ */
502
+ toJSON(): GraphJSON {
503
+ const nodes = this.getNodes()
504
+ const edges: GraphJSON['edges'] = []
505
+
506
+ for (const node of nodes) {
507
+ for (const dep of node.dependencies) {
508
+ edges.push({
509
+ from: dep,
510
+ to: node.id,
511
+ type: node.dependencyTypes?.[dep] || 'hard',
512
+ })
513
+ }
514
+ }
515
+
516
+ return { nodes, edges }
517
+ }
518
+ }