yaml-flow 3.0.0 → 3.1.1

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 (59) hide show
  1. package/README.md +44 -23
  2. package/dist/{constants-B_ftYTTE.d.ts → constants-B2zqu10b.d.ts} +7 -57
  3. package/dist/{constants-CiyHX8L-.d.cts → constants-DJZU1pwJ.d.cts} +7 -57
  4. package/dist/continuous-event-graph/index.cjs +1161 -182
  5. package/dist/continuous-event-graph/index.cjs.map +1 -1
  6. package/dist/continuous-event-graph/index.d.cts +567 -48
  7. package/dist/continuous-event-graph/index.d.ts +567 -48
  8. package/dist/continuous-event-graph/index.js +1151 -183
  9. package/dist/continuous-event-graph/index.js.map +1 -1
  10. package/dist/event-graph/index.cjs +35 -11
  11. package/dist/event-graph/index.cjs.map +1 -1
  12. package/dist/event-graph/index.d.cts +14 -5
  13. package/dist/event-graph/index.d.ts +14 -5
  14. package/dist/event-graph/index.js +34 -11
  15. package/dist/event-graph/index.js.map +1 -1
  16. package/dist/index.cjs +945 -414
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +5 -4
  19. package/dist/index.d.ts +5 -4
  20. package/dist/index.js +936 -415
  21. package/dist/index.js.map +1 -1
  22. package/dist/inference/index.cjs +31 -7
  23. package/dist/inference/index.cjs.map +1 -1
  24. package/dist/inference/index.d.cts +2 -2
  25. package/dist/inference/index.d.ts +2 -2
  26. package/dist/inference/index.js +31 -7
  27. package/dist/inference/index.js.map +1 -1
  28. package/dist/{types-CxJg9Jrt.d.cts → types-BwvgvlOO.d.cts} +2 -2
  29. package/dist/{types-BuEo3wVG.d.ts → types-ClRA8hzC.d.ts} +2 -2
  30. package/dist/{types-BpWrH1sf.d.cts → types-DEj7OakX.d.cts} +14 -4
  31. package/dist/{types-BpWrH1sf.d.ts → types-DEj7OakX.d.ts} +14 -4
  32. package/dist/validate-DEZ2Ymdb.d.ts +53 -0
  33. package/dist/validate-DqKTZg_o.d.cts +53 -0
  34. package/examples/batch/batch-step-machine.ts +121 -0
  35. package/examples/browser/index.html +367 -0
  36. package/examples/continuous-event-graph/live-cards-board.ts +215 -0
  37. package/examples/continuous-event-graph/live-portfolio-dashboard.ts +555 -0
  38. package/examples/continuous-event-graph/portfolio-tracker.ts +287 -0
  39. package/examples/continuous-event-graph/reactive-monitoring.ts +265 -0
  40. package/examples/continuous-event-graph/reactive-pipeline.ts +168 -0
  41. package/examples/continuous-event-graph/soc-incident-board.ts +287 -0
  42. package/examples/continuous-event-graph/stock-dashboard.ts +229 -0
  43. package/examples/event-graph/ci-cd-pipeline.ts +243 -0
  44. package/examples/event-graph/executor-diamond.ts +165 -0
  45. package/examples/event-graph/executor-pipeline.ts +161 -0
  46. package/examples/event-graph/research-pipeline.ts +137 -0
  47. package/examples/flows/ai-conversation.yaml +116 -0
  48. package/examples/flows/order-processing.yaml +143 -0
  49. package/examples/flows/simple-greeting.yaml +54 -0
  50. package/examples/graph-of-graphs/multi-stage-etl.ts +307 -0
  51. package/examples/graph-of-graphs/url-processing-pipeline.ts +254 -0
  52. package/examples/inference/azure-deployment.ts +149 -0
  53. package/examples/inference/copilot-cli.ts +138 -0
  54. package/examples/inference/data-pipeline.ts +145 -0
  55. package/examples/inference/pluggable-adapters.ts +254 -0
  56. package/examples/ingest.js +733 -0
  57. package/examples/node/ai-conversation.ts +195 -0
  58. package/examples/node/simple-greeting.ts +101 -0
  59. package/package.json +3 -2
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Event Graph Example: CI/CD Pipeline
3
+ *
4
+ * Demonstrates:
5
+ * - External event injection (human approval gate)
6
+ * - Conditional routing via `on`
7
+ * - Failure tokens via `on_failure`
8
+ * - Retry configuration
9
+ * - Stuck detection
10
+ *
11
+ * Run with: npx tsx examples/event-graph/ci-cd-pipeline.ts
12
+ */
13
+
14
+ import {
15
+ next,
16
+ apply,
17
+ createInitialExecutionState,
18
+ } from '../../src/event-graph/index.js';
19
+ import type { GraphConfig } from '../../src/event-graph/index.js';
20
+
21
+ // ============================================================================
22
+ // 1. Define the graph
23
+ // ============================================================================
24
+
25
+ const graph: GraphConfig = {
26
+ id: 'ci-cd-pipeline',
27
+ settings: {
28
+ completion: 'goal-reached',
29
+ goal: ['deployed'],
30
+ conflict_strategy: 'alphabetical',
31
+ },
32
+ tasks: {
33
+ build: {
34
+ provides: ['build-artifact'],
35
+ description: 'Compile the project',
36
+ },
37
+ unit_tests: {
38
+ requires: ['build-artifact'],
39
+ provides: ['tests-passed'],
40
+ retry: { max_attempts: 2 },
41
+ on_failure: ['tests-failed'],
42
+ description: 'Run unit tests',
43
+ },
44
+ lint: {
45
+ requires: ['build-artifact'],
46
+ provides: ['lint-passed'],
47
+ description: 'Run linter',
48
+ },
49
+ security_scan: {
50
+ requires: ['build-artifact'],
51
+ provides: ['scan-passed'],
52
+ on: {
53
+ clean: ['scan-passed'],
54
+ vulnerable: ['scan-blocked'],
55
+ },
56
+ description: 'Run security scan',
57
+ },
58
+ approve: {
59
+ // Needs all checks AND a human approval token
60
+ requires: ['tests-passed', 'lint-passed', 'scan-passed', 'human-approval'],
61
+ provides: ['approved'],
62
+ description: 'Human approval gate',
63
+ },
64
+ deploy: {
65
+ requires: ['approved'],
66
+ provides: ['deployed'],
67
+ retry: { max_attempts: 3 },
68
+ on_failure: ['deploy-failed'],
69
+ description: 'Deploy to production',
70
+ },
71
+ notify_failure: {
72
+ // Activates if tests fail, scan is blocked, or deploy fails
73
+ requires: ['tests-failed'],
74
+ provides: ['failure-notified'],
75
+ description: 'Notify team of failure',
76
+ },
77
+ notify_blocked: {
78
+ requires: ['scan-blocked'],
79
+ provides: ['block-notified'],
80
+ description: 'Notify team of security block',
81
+ },
82
+ },
83
+ };
84
+
85
+ // ============================================================================
86
+ // 2. Simulated task executor
87
+ // ============================================================================
88
+
89
+ // Simulate tasks with controlled outcomes
90
+ const taskOutcomes: Record<string, { success: boolean; result?: string }> = {
91
+ build: { success: true },
92
+ unit_tests: { success: true },
93
+ lint: { success: true },
94
+ security_scan: { success: true, result: 'clean' },
95
+ approve: { success: true },
96
+ deploy: { success: true },
97
+ notify_failure: { success: true },
98
+ notify_blocked: { success: true },
99
+ };
100
+
101
+ async function executeTask(taskName: string): Promise<{ success: boolean; result?: string; error?: string }> {
102
+ await new Promise((r) => setTimeout(r, Math.random() * 100 + 20));
103
+ const outcome = taskOutcomes[taskName] ?? { success: true };
104
+
105
+ if (!outcome.success) {
106
+ console.log(` ✗ ${taskName} FAILED`);
107
+ return { success: false, error: `${taskName} execution error` };
108
+ }
109
+
110
+ console.log(` ✓ ${taskName} completed${outcome.result ? ` (result: ${outcome.result})` : ''}`);
111
+ return { success: true, result: outcome.result };
112
+ }
113
+
114
+ // ============================================================================
115
+ // 3. Driver loop with external event injection
116
+ // ============================================================================
117
+
118
+ async function main() {
119
+ let state = createInitialExecutionState(graph, 'pipeline-42');
120
+ let iteration = 0;
121
+ let approvalInjected = false;
122
+
123
+ console.log('CI/CD Pipeline — Event Graph Demo');
124
+ console.log('==================================\n');
125
+
126
+ while (iteration < 20) {
127
+ iteration++;
128
+
129
+ // Simulate human approval after all automated checks pass
130
+ if (
131
+ !approvalInjected &&
132
+ state.availableOutputs.includes('tests-passed') &&
133
+ state.availableOutputs.includes('lint-passed') &&
134
+ state.availableOutputs.includes('scan-passed')
135
+ ) {
136
+ console.log('\n 🔔 All checks passed — simulating human approval...');
137
+ state = apply(
138
+ state,
139
+ { type: 'inject-tokens', tokens: ['human-approval'], timestamp: new Date().toISOString() },
140
+ graph
141
+ );
142
+ approvalInjected = true;
143
+ }
144
+
145
+ const schedule = next(graph, state);
146
+
147
+ console.log(`\n[iteration ${iteration}] eligible: [${schedule.eligibleTasks.join(', ')}]`);
148
+
149
+ if (schedule.isComplete) {
150
+ console.log('\n✅ Pipeline complete! Deployment successful.');
151
+ console.log('Final outputs:', state.availableOutputs);
152
+ break;
153
+ }
154
+
155
+ if (schedule.stuckDetection.is_stuck) {
156
+ console.error('\n❌ Pipeline stuck:', schedule.stuckDetection.stuck_description);
157
+ console.log('Blocked tasks:', schedule.stuckDetection.tasks_blocked);
158
+ break;
159
+ }
160
+
161
+ if (schedule.eligibleTasks.length === 0) {
162
+ console.log(' (no eligible tasks — waiting for events)');
163
+ break;
164
+ }
165
+
166
+ // Start + execute eligible tasks
167
+ const ts = new Date().toISOString();
168
+ for (const taskName of schedule.eligibleTasks) {
169
+ state = apply(state, { type: 'task-started', taskName, timestamp: ts }, graph);
170
+ }
171
+
172
+ const results = await Promise.all(schedule.eligibleTasks.map(executeTask));
173
+
174
+ for (let i = 0; i < results.length; i++) {
175
+ const taskName = schedule.eligibleTasks[i];
176
+ const r = results[i];
177
+ const ts2 = new Date().toISOString();
178
+
179
+ if (r.success) {
180
+ state = apply(state, { type: 'task-completed', taskName, result: r.result, timestamp: ts2 }, graph);
181
+ } else {
182
+ state = apply(state, { type: 'task-failed', taskName, error: r.error!, timestamp: ts2 }, graph);
183
+ }
184
+ }
185
+
186
+ console.log(` outputs: [${state.availableOutputs.join(', ')}]`);
187
+ }
188
+
189
+ // ------------------------------------------------------------------
190
+ // Now demonstrate a failure scenario
191
+ // ------------------------------------------------------------------
192
+ console.log('\n\n========== Failure Scenario ==========\n');
193
+
194
+ // Override: make security scan find vulnerabilities
195
+ taskOutcomes.security_scan = { success: true, result: 'vulnerable' };
196
+
197
+ state = createInitialExecutionState(graph, 'pipeline-43');
198
+ iteration = 0;
199
+ approvalInjected = false;
200
+
201
+ while (iteration < 20) {
202
+ iteration++;
203
+ const schedule = next(graph, state);
204
+
205
+ console.log(`\n[iteration ${iteration}] eligible: [${schedule.eligibleTasks.join(', ')}]`);
206
+
207
+ if (schedule.isComplete) {
208
+ console.log('\n✅ Complete.');
209
+ break;
210
+ }
211
+
212
+ if (schedule.stuckDetection.is_stuck) {
213
+ console.log('\n⚠️ Pipeline blocked (as expected with vulnerability).');
214
+ console.log('Reason:', schedule.stuckDetection.stuck_description);
215
+ break;
216
+ }
217
+
218
+ if (schedule.eligibleTasks.length === 0) break;
219
+
220
+ const ts = new Date().toISOString();
221
+ for (const taskName of schedule.eligibleTasks) {
222
+ state = apply(state, { type: 'task-started', taskName, timestamp: ts }, graph);
223
+ }
224
+
225
+ const results = await Promise.all(schedule.eligibleTasks.map(executeTask));
226
+
227
+ for (let i = 0; i < results.length; i++) {
228
+ const taskName = schedule.eligibleTasks[i];
229
+ const r = results[i];
230
+ const ts2 = new Date().toISOString();
231
+
232
+ if (r.success) {
233
+ state = apply(state, { type: 'task-completed', taskName, result: r.result, timestamp: ts2 }, graph);
234
+ } else {
235
+ state = apply(state, { type: 'task-failed', taskName, error: r.error!, timestamp: ts2 }, graph);
236
+ }
237
+ }
238
+
239
+ console.log(` outputs: [${state.availableOutputs.join(', ')}]`);
240
+ }
241
+ }
242
+
243
+ main().catch(console.error);
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Event Graph Example: Executor-driven Diamond DAG (Library Mode)
3
+ *
4
+ * A diamond-shaped graph with parallel fan-out and fan-in.
5
+ * Tasks simulate real-world async work with random delays.
6
+ *
7
+ * fetch
8
+ * / \
9
+ * parse validate
10
+ * \ /
11
+ * combine
12
+ * |
13
+ * report
14
+ *
15
+ * Run with: npx tsx examples/event-graph/executor-diamond.ts
16
+ */
17
+
18
+ import {
19
+ next, apply, createInitialExecutionState,
20
+ } from '../../src/event-graph/index.js';
21
+ import type { GraphConfig, ExecutionState } from '../../src/event-graph/types.js';
22
+
23
+ // ============================================================================
24
+ // 1. Define the diamond graph
25
+ // ============================================================================
26
+
27
+ const graph: GraphConfig = {
28
+ id: 'diamond-dag',
29
+ settings: {
30
+ completion: 'all-tasks-done',
31
+ execution_mode: 'eligibility-mode',
32
+ conflict_strategy: 'parallel-all',
33
+ },
34
+ tasks: {
35
+ fetch: {
36
+ provides: ['raw-payload'],
37
+ },
38
+ parse: {
39
+ requires: ['raw-payload'],
40
+ provides: ['parsed-records'],
41
+ },
42
+ validate: {
43
+ requires: ['raw-payload'],
44
+ provides: ['validation-report'],
45
+ },
46
+ combine: {
47
+ requires: ['parsed-records', 'validation-report'],
48
+ provides: ['final-dataset'],
49
+ },
50
+ report: {
51
+ requires: ['final-dataset'],
52
+ provides: ['report-sent'],
53
+ },
54
+ },
55
+ };
56
+
57
+ // ============================================================================
58
+ // 2. Simulate async executors with random delays
59
+ // ============================================================================
60
+
61
+ const taskSimulations: Record<string, () => Promise<{ ok: boolean; detail: string }>> = {
62
+ fetch: async () => {
63
+ await sleep(randomMs(100, 300));
64
+ return { ok: true, detail: 'fetched 5MB payload' };
65
+ },
66
+ parse: async () => {
67
+ await sleep(randomMs(150, 400));
68
+ return { ok: true, detail: 'parsed 12,000 records' };
69
+ },
70
+ validate: async () => {
71
+ await sleep(randomMs(200, 500));
72
+ const passRate = 95 + Math.floor(Math.random() * 5);
73
+ return { ok: true, detail: `${passRate}% pass rate` };
74
+ },
75
+ combine: async () => {
76
+ await sleep(randomMs(100, 250));
77
+ return { ok: true, detail: 'merged parsed + validated' };
78
+ },
79
+ report: async () => {
80
+ await sleep(randomMs(50, 150));
81
+ return { ok: true, detail: 'emailed to stakeholders' };
82
+ },
83
+ };
84
+
85
+ // ============================================================================
86
+ // 3. Execution loop — you drive, engine decides
87
+ // ============================================================================
88
+
89
+ async function run() {
90
+ console.log('=== Executor-driven Diamond DAG ===\n');
91
+ console.log(' Graph shape: fetch → [parse, validate] → combine → report\n');
92
+
93
+ let state: ExecutionState = createInitialExecutionState(graph, 'diamond-1');
94
+ let iteration = 0;
95
+ const startTime = Date.now();
96
+
97
+ while (iteration < 20) {
98
+ iteration++;
99
+ const result = next(graph, state);
100
+
101
+ if (result.isComplete) {
102
+ const elapsed = Date.now() - startTime;
103
+ console.log(`\n✅ All tasks complete in ${elapsed}ms (${iteration} iterations)\n`);
104
+ break;
105
+ }
106
+
107
+ if (result.eligibleTasks.length === 0) {
108
+ if (result.stuckDetection.is_stuck) {
109
+ console.log(`⚠️ Stuck: ${result.stuckDetection.stuck_description}`);
110
+ break;
111
+ }
112
+ continue;
113
+ }
114
+
115
+ console.log(`[iteration ${iteration}] dispatching: [${result.eligibleTasks.join(', ')}]`);
116
+
117
+ // Start all eligible
118
+ const ts = new Date().toISOString();
119
+ for (const taskName of result.eligibleTasks) {
120
+ state = apply(state, { type: 'task-started', taskName, timestamp: ts }, graph);
121
+ }
122
+
123
+ // Execute in parallel — this is where your real handlers would go
124
+ const execResults = await Promise.all(
125
+ result.eligibleTasks.map(async (taskName) => {
126
+ const sim = taskSimulations[taskName];
127
+ const r = await sim();
128
+ console.log(` [${taskName}] ${r.ok ? '✓' : '✗'} ${r.detail}`);
129
+ return { taskName, ...r };
130
+ }),
131
+ );
132
+
133
+ // Feed results back
134
+ for (const r of execResults) {
135
+ const ts2 = new Date().toISOString();
136
+ if (r.ok) {
137
+ state = apply(state, { type: 'task-completed', taskName: r.taskName, timestamp: ts2 }, graph);
138
+ } else {
139
+ state = apply(state, { type: 'task-failed', taskName: r.taskName, error: 'failed', timestamp: ts2 }, graph);
140
+ }
141
+ }
142
+
143
+ console.log(` → outputs: [${state.availableOutputs.join(', ')}]`);
144
+ }
145
+
146
+ // Summary
147
+ console.log('=== Execution Summary ===');
148
+ for (const [name, task] of Object.entries(state.tasks)) {
149
+ console.log(` ${name}: ${task.status}`);
150
+ }
151
+ }
152
+
153
+ run();
154
+
155
+ // ============================================================================
156
+ // Util
157
+ // ============================================================================
158
+
159
+ function sleep(ms: number): Promise<void> {
160
+ return new Promise(resolve => setTimeout(resolve, ms));
161
+ }
162
+
163
+ function randomMs(min: number, max: number): number {
164
+ return min + Math.floor(Math.random() * (max - min));
165
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Event Graph Example: Executor-driven Pipeline (Library Mode)
3
+ *
4
+ * Same ETL pipeline as reactive-pipeline.ts, but YOU drive the loop.
5
+ * Each task simulates async work with random sleep.
6
+ *
7
+ * Demonstrates:
8
+ * - Manual executor loop with next/apply
9
+ * - validateGraph for static config validation before running
10
+ * - validateLiveGraph for runtime state-consistency after running
11
+ *
12
+ * Contrast with reactive-pipeline.ts where the graph drives itself.
13
+ *
14
+ * Run with: npx tsx examples/event-graph/executor-pipeline.ts
15
+ */
16
+
17
+ import {
18
+ next, apply, createInitialExecutionState, validateGraph,
19
+ } from '../../src/event-graph/index.js';
20
+ import type { GraphConfig, ExecutionState } from '../../src/event-graph/types.js';
21
+ import { validateLiveGraph } from '../../src/continuous-event-graph/index.js';
22
+
23
+ // ============================================================================
24
+ // 1. Define the graph (same as reactive-pipeline)
25
+ // ============================================================================
26
+
27
+ const graph: GraphConfig = {
28
+ id: 'etl-pipeline',
29
+ settings: {
30
+ completion: 'all-tasks-done',
31
+ execution_mode: 'eligibility-mode',
32
+ conflict_strategy: 'parallel-all',
33
+ },
34
+ tasks: {
35
+ extract: {
36
+ provides: ['raw-data'],
37
+ },
38
+ validate: {
39
+ requires: ['raw-data'],
40
+ provides: ['valid-data'],
41
+ },
42
+ enrich: {
43
+ requires: ['valid-data'],
44
+ provides: ['enriched-data'],
45
+ },
46
+ load: {
47
+ requires: ['enriched-data'],
48
+ provides: ['loaded'],
49
+ },
50
+ },
51
+ };
52
+
53
+ // ============================================================================
54
+ // 2. Task handlers — simulate async work with random sleep
55
+ // ============================================================================
56
+
57
+ async function executeTask(taskName: string): Promise<{ ok: boolean; error?: string }> {
58
+ const delay = 100 + Math.floor(Math.random() * 400); // 100–500ms
59
+ console.log(` [${taskName}] executing... (${delay}ms)`);
60
+ await sleep(delay);
61
+
62
+ // Simulate occasional failure (10% chance)
63
+ if (Math.random() < 0.1) {
64
+ console.log(` [${taskName}] FAILED`);
65
+ return { ok: false, error: `${taskName} timed out` };
66
+ }
67
+
68
+ console.log(` [${taskName}] done`);
69
+ return { ok: true };
70
+ }
71
+
72
+ // ============================================================================
73
+ // 3. YOU drive the execution loop
74
+ // ============================================================================
75
+
76
+ async function run() {
77
+ console.log('=== Executor-driven ETL Pipeline (Library Mode) ===\n');
78
+
79
+ // Pre-flight: validate static config before running
80
+ const configValidation = validateGraph(graph);
81
+ console.log(`Config validation: ${configValidation.valid ? '✅ valid' : '❌ invalid'} (${configValidation.issues.length} issues)`);
82
+ for (const issue of configValidation.issues) {
83
+ console.log(` [${issue.severity}] ${issue.code}: ${issue.message}`);
84
+ }
85
+ if (!configValidation.valid) return;
86
+
87
+ let state: ExecutionState = createInitialExecutionState(graph, 'exec-1');
88
+ let iteration = 0;
89
+
90
+ while (iteration < 20) {
91
+ iteration++;
92
+ const result = next(graph, state);
93
+
94
+ console.log(`\n[iteration ${iteration}] eligible: [${result.eligibleTasks.join(', ')}]`);
95
+
96
+ if (result.isComplete) {
97
+ console.log('\n✅ Pipeline complete!');
98
+ break;
99
+ }
100
+
101
+ if (result.stuckDetection.is_stuck) {
102
+ console.log(`\n⚠️ Pipeline stuck: ${result.stuckDetection.stuck_description}`);
103
+ break;
104
+ }
105
+
106
+ if (result.eligibleTasks.length === 0) {
107
+ console.log(' (waiting for running tasks...)');
108
+ break;
109
+ }
110
+
111
+ // Mark all eligible tasks as started
112
+ const ts = new Date().toISOString();
113
+ for (const taskName of result.eligibleTasks) {
114
+ state = apply(state, { type: 'task-started', taskName, timestamp: ts }, graph);
115
+ }
116
+
117
+ // Execute in parallel — simulate real async work
118
+ const results = await Promise.all(
119
+ result.eligibleTasks.map(async (taskName) => {
120
+ const r = await executeTask(taskName);
121
+ return { taskName, ...r };
122
+ }),
123
+ );
124
+
125
+ // Feed results back into the reducer
126
+ for (const r of results) {
127
+ const ts2 = new Date().toISOString();
128
+ if (r.ok) {
129
+ state = apply(state, { type: 'task-completed', taskName: r.taskName, timestamp: ts2 }, graph);
130
+ } else {
131
+ state = apply(state, { type: 'task-failed', taskName: r.taskName, error: r.error!, timestamp: ts2 }, graph);
132
+ }
133
+ }
134
+
135
+ console.log(` outputs: [${state.availableOutputs.join(', ')}]`);
136
+ }
137
+
138
+ // Final state
139
+ console.log('\n=== Final State ===');
140
+ for (const [name, task] of Object.entries(state.tasks)) {
141
+ console.log(` ${name}: ${task.status} (${task.executionCount}x)`);
142
+ }
143
+
144
+ // Post-run: validate runtime state consistency
145
+ console.log('\n=== Runtime Validation ===');
146
+ const runtimeValidation = validateLiveGraph({ config: graph, state });
147
+ console.log(` Valid: ${runtimeValidation.valid} (${runtimeValidation.issues.length} issues)`);
148
+ for (const issue of runtimeValidation.issues) {
149
+ console.log(` [${issue.severity}] ${issue.code}: ${issue.message}`);
150
+ }
151
+ }
152
+
153
+ run();
154
+
155
+ // ============================================================================
156
+ // Util
157
+ // ============================================================================
158
+
159
+ function sleep(ms: number): Promise<void> {
160
+ return new Promise(resolve => setTimeout(resolve, ms));
161
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Event Graph Example: Research Pipeline
3
+ *
4
+ * Demonstrates:
5
+ * - Parallel task execution (fan-out / fan-in)
6
+ * - Goal-based completion
7
+ * - The standard next() / apply() driver loop
8
+ *
9
+ * Run with: npx tsx examples/event-graph/research-pipeline.ts
10
+ */
11
+
12
+ import {
13
+ next,
14
+ apply,
15
+ createInitialExecutionState,
16
+ } from '../../src/event-graph/index.js';
17
+ import type { GraphConfig } from '../../src/event-graph/index.js';
18
+
19
+ // ============================================================================
20
+ // 1. Define the graph
21
+ // ============================================================================
22
+
23
+ const graph: GraphConfig = {
24
+ id: 'research-pipeline',
25
+ settings: {
26
+ completion: 'goal-reached',
27
+ goal: ['final-report'],
28
+ conflict_strategy: 'parallel-all',
29
+ },
30
+ tasks: {
31
+ fetch_sources: {
32
+ provides: ['raw-sources'],
33
+ description: 'Fetch source documents from the web',
34
+ },
35
+ analyse_sentiment: {
36
+ requires: ['raw-sources'],
37
+ provides: ['sentiment-result'],
38
+ description: 'Run sentiment analysis on sources',
39
+ },
40
+ analyse_entities: {
41
+ requires: ['raw-sources'],
42
+ provides: ['entity-result'],
43
+ description: 'Extract named entities from sources',
44
+ },
45
+ merge_analysis: {
46
+ requires: ['sentiment-result', 'entity-result'],
47
+ provides: ['merged-analysis'],
48
+ description: 'Merge both analysis results',
49
+ },
50
+ generate_report: {
51
+ requires: ['merged-analysis'],
52
+ provides: ['final-report'],
53
+ description: 'Generate a final report',
54
+ },
55
+ },
56
+ };
57
+
58
+ // ============================================================================
59
+ // 2. Simulated task executor
60
+ // ============================================================================
61
+
62
+ async function executeTask(taskName: string): Promise<string | undefined> {
63
+ // Simulate varying work durations
64
+ const delay = Math.random() * 200 + 50;
65
+ await new Promise((r) => setTimeout(r, delay));
66
+
67
+ console.log(` ✓ ${taskName} completed (${delay.toFixed(0)}ms)`);
68
+ return undefined; // no special result key → default provides
69
+ }
70
+
71
+ // ============================================================================
72
+ // 3. Driver loop
73
+ // ============================================================================
74
+
75
+ async function main() {
76
+ let state = createInitialExecutionState(graph, 'exec-1');
77
+ let iteration = 0;
78
+
79
+ console.log('Research Pipeline — Event Graph Demo');
80
+ console.log('====================================\n');
81
+
82
+ while (true) {
83
+ iteration++;
84
+ const schedule = next(graph, state);
85
+
86
+ console.log(`[iteration ${iteration}] eligible: [${schedule.eligibleTasks.join(', ')}]`);
87
+
88
+ if (schedule.isComplete) {
89
+ console.log('\n✅ Pipeline complete!');
90
+ console.log('Available outputs:', state.availableOutputs);
91
+ break;
92
+ }
93
+
94
+ if (schedule.stuckDetection.is_stuck) {
95
+ console.error('\n❌ Pipeline stuck:', schedule.stuckDetection.stuck_description);
96
+ break;
97
+ }
98
+
99
+ if (schedule.eligibleTasks.length === 0) {
100
+ console.log(' (waiting for running tasks to complete)');
101
+ // In a real system you'd await running task results here.
102
+ break;
103
+ }
104
+
105
+ // Mark all eligible tasks as started
106
+ const ts = new Date().toISOString();
107
+ for (const taskName of schedule.eligibleTasks) {
108
+ state = apply(state, { type: 'task-started', taskName, timestamp: ts }, graph);
109
+ }
110
+
111
+ // Execute in parallel
112
+ const results = await Promise.all(
113
+ schedule.eligibleTasks.map(async (taskName) => {
114
+ try {
115
+ const result = await executeTask(taskName);
116
+ return { taskName, ok: true, result };
117
+ } catch (err: unknown) {
118
+ return { taskName, ok: false, error: (err as Error).message };
119
+ }
120
+ })
121
+ );
122
+
123
+ // Feed results back into the reducer
124
+ for (const r of results) {
125
+ const ts = new Date().toISOString();
126
+ if (r.ok) {
127
+ state = apply(state, { type: 'task-completed', taskName: r.taskName, result: r.result, timestamp: ts }, graph);
128
+ } else {
129
+ state = apply(state, { type: 'task-failed', taskName: r.taskName, error: r.error!, timestamp: ts }, graph);
130
+ }
131
+ }
132
+
133
+ console.log(` outputs so far: [${state.availableOutputs.join(', ')}]\n`);
134
+ }
135
+ }
136
+
137
+ main().catch(console.error);