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.
- package/README.md +44 -23
- package/dist/{constants-B_ftYTTE.d.ts → constants-B2zqu10b.d.ts} +7 -57
- package/dist/{constants-CiyHX8L-.d.cts → constants-DJZU1pwJ.d.cts} +7 -57
- package/dist/continuous-event-graph/index.cjs +1161 -182
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.d.cts +567 -48
- package/dist/continuous-event-graph/index.d.ts +567 -48
- package/dist/continuous-event-graph/index.js +1151 -183
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/event-graph/index.cjs +35 -11
- package/dist/event-graph/index.cjs.map +1 -1
- package/dist/event-graph/index.d.cts +14 -5
- package/dist/event-graph/index.d.ts +14 -5
- package/dist/event-graph/index.js +34 -11
- package/dist/event-graph/index.js.map +1 -1
- package/dist/index.cjs +945 -414
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +936 -415
- package/dist/index.js.map +1 -1
- package/dist/inference/index.cjs +31 -7
- package/dist/inference/index.cjs.map +1 -1
- package/dist/inference/index.d.cts +2 -2
- package/dist/inference/index.d.ts +2 -2
- package/dist/inference/index.js +31 -7
- package/dist/inference/index.js.map +1 -1
- package/dist/{types-CxJg9Jrt.d.cts → types-BwvgvlOO.d.cts} +2 -2
- package/dist/{types-BuEo3wVG.d.ts → types-ClRA8hzC.d.ts} +2 -2
- package/dist/{types-BpWrH1sf.d.cts → types-DEj7OakX.d.cts} +14 -4
- package/dist/{types-BpWrH1sf.d.ts → types-DEj7OakX.d.ts} +14 -4
- package/dist/validate-DEZ2Ymdb.d.ts +53 -0
- package/dist/validate-DqKTZg_o.d.cts +53 -0
- package/examples/batch/batch-step-machine.ts +121 -0
- package/examples/browser/index.html +367 -0
- package/examples/continuous-event-graph/live-cards-board.ts +215 -0
- package/examples/continuous-event-graph/live-portfolio-dashboard.ts +555 -0
- package/examples/continuous-event-graph/portfolio-tracker.ts +287 -0
- package/examples/continuous-event-graph/reactive-monitoring.ts +265 -0
- package/examples/continuous-event-graph/reactive-pipeline.ts +168 -0
- package/examples/continuous-event-graph/soc-incident-board.ts +287 -0
- package/examples/continuous-event-graph/stock-dashboard.ts +229 -0
- package/examples/event-graph/ci-cd-pipeline.ts +243 -0
- package/examples/event-graph/executor-diamond.ts +165 -0
- package/examples/event-graph/executor-pipeline.ts +161 -0
- package/examples/event-graph/research-pipeline.ts +137 -0
- package/examples/flows/ai-conversation.yaml +116 -0
- package/examples/flows/order-processing.yaml +143 -0
- package/examples/flows/simple-greeting.yaml +54 -0
- package/examples/graph-of-graphs/multi-stage-etl.ts +307 -0
- package/examples/graph-of-graphs/url-processing-pipeline.ts +254 -0
- package/examples/inference/azure-deployment.ts +149 -0
- package/examples/inference/copilot-cli.ts +138 -0
- package/examples/inference/data-pipeline.ts +145 -0
- package/examples/inference/pluggable-adapters.ts +254 -0
- package/examples/ingest.js +733 -0
- package/examples/node/ai-conversation.ts +195 -0
- package/examples/node/simple-greeting.ts +101 -0
- 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);
|