yaml-flow 1.0.0 → 2.1.0
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 +705 -225
- package/dist/batch/index.cjs +109 -0
- package/dist/batch/index.cjs.map +1 -0
- package/dist/batch/index.d.cts +126 -0
- package/dist/batch/index.d.ts +126 -0
- package/dist/batch/index.js +107 -0
- package/dist/batch/index.js.map +1 -0
- package/dist/config/index.cjs +80 -0
- package/dist/config/index.cjs.map +1 -0
- package/dist/config/index.d.cts +71 -0
- package/dist/config/index.d.ts +71 -0
- package/dist/config/index.js +77 -0
- package/dist/config/index.js.map +1 -0
- package/dist/constants-D1fTEbbM.d.cts +330 -0
- package/dist/constants-D1fTEbbM.d.ts +330 -0
- package/dist/event-graph/index.cjs +895 -0
- package/dist/event-graph/index.cjs.map +1 -0
- package/dist/event-graph/index.d.cts +53 -0
- package/dist/event-graph/index.d.ts +53 -0
- package/dist/event-graph/index.js +855 -0
- package/dist/event-graph/index.js.map +1 -0
- package/dist/index.cjs +1309 -312
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +1271 -306
- package/dist/index.js.map +1 -1
- package/dist/step-machine/index.cjs +513 -0
- package/dist/step-machine/index.cjs.map +1 -0
- package/dist/step-machine/index.d.cts +77 -0
- package/dist/step-machine/index.d.ts +77 -0
- package/dist/step-machine/index.js +502 -0
- package/dist/step-machine/index.js.map +1 -0
- package/dist/stores/file.cjs.map +1 -1
- package/dist/stores/file.d.cts +4 -4
- package/dist/stores/file.d.ts +4 -4
- package/dist/stores/file.js.map +1 -1
- package/dist/stores/index.cjs +232 -0
- package/dist/stores/index.cjs.map +1 -0
- package/dist/stores/index.d.cts +4 -0
- package/dist/stores/index.d.ts +4 -0
- package/dist/stores/index.js +228 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/stores/localStorage.cjs.map +1 -1
- package/dist/stores/localStorage.d.cts +4 -4
- package/dist/stores/localStorage.d.ts +4 -4
- package/dist/stores/localStorage.js.map +1 -1
- package/dist/stores/memory.cjs.map +1 -1
- package/dist/stores/memory.d.cts +4 -4
- package/dist/stores/memory.d.ts +4 -4
- package/dist/stores/memory.js.map +1 -1
- package/dist/types-FZ_eyErS.d.cts +115 -0
- package/dist/types-FZ_eyErS.d.ts +115 -0
- package/package.json +26 -6
- package/dist/core/index.cjs +0 -557
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -102
- package/dist/core/index.d.ts +0 -102
- package/dist/core/index.js +0 -549
- package/dist/core/index.js.map +0 -1
- package/dist/types-BoWndaAJ.d.cts +0 -237
- package/dist/types-BoWndaAJ.d.ts +0 -237
package/README.md
CHANGED
|
@@ -1,225 +1,504 @@
|
|
|
1
1
|
# yaml-flow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Two workflow engines in one package. Pick the model that fits your problem.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/yaml-flow)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
```
|
|
9
|
+
npm install yaml-flow
|
|
10
|
+
```
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
- **Declarative Flows** — Define workflows in YAML with JSON Schema validation
|
|
12
|
-
- **Pure Function Handlers** — Step handlers are simple `(input, context) => result` functions
|
|
13
|
-
- **Pluggable Storage** — Bring your own persistence (memory, localStorage, file, Redis, etc.)
|
|
14
|
-
- **Zero Core Dependencies** — Lightweight core, optional add-ons for YAML parsing
|
|
15
|
-
- **Resumable** — Pause and resume flows from persisted state
|
|
16
|
-
- **Circuit Breakers** — Prevent infinite loops with configurable limits
|
|
17
|
-
- **Retry Logic** — Built-in exponential backoff for failed steps
|
|
18
|
-
- **Event System** — Subscribe to flow events for UI updates
|
|
12
|
+
## Which Mode Do I Need?
|
|
19
13
|
|
|
20
|
-
|
|
14
|
+
yaml-flow ships two execution models. They solve fundamentally different problems.
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
| | **Step Machine** | **Event Graph** |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| **Mental model** | Flowchart — each step decides what runs next | Dependency graph — tasks self-select when their inputs are ready |
|
|
19
|
+
| **Core function** | `applyStepResult(flow, state, step, result) → newState` | `next(graph, state) → eligibleTasks` + `apply(state, event, graph) → newState` |
|
|
20
|
+
| **Config shape** | `steps:` with `transitions:` mapping | `tasks:` with `requires:` / `provides:` arrays |
|
|
21
|
+
| **Who decides next?** | The sender (current step's result picks the transition) | The receivers (tasks become eligible when their required tokens appear) |
|
|
22
|
+
| **Has a driver?** | Yes — `StepMachine` class runs the loop for you | No — you call `next()` and `apply()` yourself (or write a 10-line loop) |
|
|
23
|
+
| **Import** | `import { StepMachine } from 'yaml-flow/step-machine'` | `import { next, apply } from 'yaml-flow/event-graph'` |
|
|
24
|
+
|
|
25
|
+
### Use Step Machine when…
|
|
26
|
+
|
|
27
|
+
- Your workflow is a **known sequence** of steps with conditional branching.
|
|
28
|
+
- You need **pause/resume** built in.
|
|
29
|
+
- The work is conversational: receive input → process → decide next step → repeat.
|
|
30
|
+
- You want to hand the library a YAML file and some handler functions and call `.run()`.
|
|
31
|
+
|
|
32
|
+
**Examples:** form wizards, approval chains, AI chat loops, order processing pipelines, ETL with linear stages.
|
|
33
|
+
|
|
34
|
+
### Use Event Graph when…
|
|
35
|
+
|
|
36
|
+
- Tasks have **complex dependencies** — diamonds, fan-out, fan-in, optional paths.
|
|
37
|
+
- You don't know the execution order up front; it **emerges from the data**.
|
|
38
|
+
- Multiple tasks can run **in parallel** once their inputs are satisfied.
|
|
39
|
+
- You need **conflict resolution** when two tasks compete to produce the same output.
|
|
40
|
+
- Your system is **event-driven**: external events inject tokens that unblock downstream tasks.
|
|
41
|
+
- You want the engine to be a **pure function** you embed in your own scheduler, serverless function, or agent loop.
|
|
42
|
+
|
|
43
|
+
**Examples:** CI/CD pipelines, agent tool orchestration, research workflows (fetch → analyse A | analyse B → merge), build systems, multi-model AI routing, eligibility engines.
|
|
25
44
|
|
|
26
|
-
|
|
45
|
+
### When in doubt
|
|
27
46
|
|
|
28
|
-
|
|
47
|
+
Start with **Step Machine** if your workflow diagram is a straight line with branches.
|
|
48
|
+
Start with **Event Graph** if your workflow diagram has diamonds or parallel lanes.
|
|
49
|
+
|
|
50
|
+
Both are pure `f(state, input) → newState` at their core. You can always call the reducer directly without the driver class.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Step Machine — Quick Start
|
|
55
|
+
|
|
56
|
+
### 1. Define a flow
|
|
29
57
|
|
|
30
58
|
```yaml
|
|
31
|
-
#
|
|
59
|
+
# support-ticket.yaml
|
|
32
60
|
settings:
|
|
33
|
-
start_step:
|
|
34
|
-
max_total_steps:
|
|
61
|
+
start_step: classify
|
|
62
|
+
max_total_steps: 20
|
|
35
63
|
|
|
36
64
|
steps:
|
|
37
|
-
|
|
38
|
-
produces_data:
|
|
39
|
-
|
|
65
|
+
classify:
|
|
66
|
+
produces_data: [category, priority]
|
|
67
|
+
transitions:
|
|
68
|
+
billing: handle_billing
|
|
69
|
+
technical: handle_technical
|
|
70
|
+
general: handle_general
|
|
71
|
+
|
|
72
|
+
handle_billing:
|
|
73
|
+
expects_data: [category]
|
|
74
|
+
produces_data: [resolution]
|
|
40
75
|
transitions:
|
|
41
|
-
|
|
42
|
-
|
|
76
|
+
resolved: close_ticket
|
|
77
|
+
escalate: escalate_ticket
|
|
78
|
+
|
|
79
|
+
handle_technical:
|
|
80
|
+
expects_data: [category]
|
|
81
|
+
produces_data: [resolution]
|
|
82
|
+
transitions:
|
|
83
|
+
resolved: close_ticket
|
|
84
|
+
escalate: escalate_ticket
|
|
85
|
+
retry:
|
|
86
|
+
max_attempts: 2
|
|
87
|
+
delay_ms: 1000
|
|
88
|
+
|
|
89
|
+
handle_general:
|
|
90
|
+
expects_data: [category]
|
|
91
|
+
produces_data: [resolution]
|
|
92
|
+
transitions:
|
|
93
|
+
resolved: close_ticket
|
|
94
|
+
|
|
95
|
+
escalate_ticket:
|
|
96
|
+
expects_data: [category, priority]
|
|
97
|
+
produces_data: [escalation_id]
|
|
98
|
+
transitions:
|
|
99
|
+
done: close_ticket
|
|
43
100
|
|
|
44
101
|
terminal_states:
|
|
45
|
-
|
|
46
|
-
return_intent:
|
|
47
|
-
return_artifacts:
|
|
48
|
-
|
|
49
|
-
error:
|
|
50
|
-
return_intent: error
|
|
51
|
-
return_artifacts: false
|
|
102
|
+
close_ticket:
|
|
103
|
+
return_intent: resolved
|
|
104
|
+
return_artifacts: [resolution]
|
|
52
105
|
```
|
|
53
106
|
|
|
54
|
-
### 2.
|
|
107
|
+
### 2. Write handlers and run
|
|
55
108
|
|
|
56
109
|
```typescript
|
|
57
|
-
import {
|
|
110
|
+
import { createStepMachine, loadStepFlow } from 'yaml-flow/step-machine';
|
|
111
|
+
import { MemoryStore } from 'yaml-flow/stores/memory';
|
|
112
|
+
|
|
113
|
+
const flow = await loadStepFlow('./support-ticket.yaml');
|
|
58
114
|
|
|
59
115
|
const handlers = {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
116
|
+
classify: async (input) => {
|
|
117
|
+
const category = detectCategory(input.message);
|
|
118
|
+
return { result: category, data: { category, priority: 'high' } };
|
|
119
|
+
},
|
|
120
|
+
handle_billing: async (input, ctx) => {
|
|
121
|
+
const answer = await ctx.components.ai.ask(`Billing issue: ${input.category}`);
|
|
122
|
+
return { result: 'resolved', data: { resolution: answer } };
|
|
123
|
+
},
|
|
124
|
+
handle_technical: async (input, ctx) => {
|
|
125
|
+
const answer = await ctx.components.ai.ask(`Tech issue: ${input.category}`);
|
|
126
|
+
return answer.confidence > 0.7
|
|
127
|
+
? { result: 'resolved', data: { resolution: answer.text } }
|
|
128
|
+
: { result: 'escalate' };
|
|
129
|
+
},
|
|
130
|
+
handle_general: async (input) => {
|
|
131
|
+
return { result: 'resolved', data: { resolution: 'See FAQ.' } };
|
|
132
|
+
},
|
|
133
|
+
escalate_ticket: async (input, ctx) => {
|
|
134
|
+
const id = await ctx.components.ticketSystem.escalate(input.category);
|
|
135
|
+
return { result: 'done', data: { escalation_id: id } };
|
|
136
|
+
},
|
|
66
137
|
};
|
|
138
|
+
|
|
139
|
+
const machine = createStepMachine(flow, handlers, {
|
|
140
|
+
store: new MemoryStore(),
|
|
141
|
+
components: { ai: myAIClient, ticketSystem: myTicketAPI },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const result = await machine.run({ message: 'I was double-charged' });
|
|
145
|
+
// result.intent → 'resolved'
|
|
146
|
+
// result.data → { resolution: '...' }
|
|
147
|
+
// result.stepHistory → ['classify', 'handle_billing']
|
|
67
148
|
```
|
|
68
149
|
|
|
69
|
-
###
|
|
150
|
+
### Step Machine features at a glance
|
|
151
|
+
|
|
152
|
+
| Feature | Config |
|
|
153
|
+
|---|---|
|
|
154
|
+
| Transitions | `transitions: { success: next_step, failure: error_step }` |
|
|
155
|
+
| Retry | `retry: { max_attempts: 3, delay_ms: 1000, backoff_multiplier: 2 }` |
|
|
156
|
+
| Circuit breaker | `circuit_breaker: { max_iterations: 5, on_open: fallback }` |
|
|
157
|
+
| Pause / resume | `await machine.pause(runId)` / `await machine.resume(runId)` |
|
|
158
|
+
| Cancellation | Pass `signal: AbortController.signal` in options |
|
|
159
|
+
| Events | `machine.on('step:complete', fn)` — also `flow:start`, `flow:complete`, `transition`, etc. |
|
|
160
|
+
| Data flow | `expects_data` filters what a handler receives; `produces_data` documents what it returns |
|
|
161
|
+
|
|
162
|
+
### Using the pure reducer directly (no driver)
|
|
70
163
|
|
|
71
164
|
```typescript
|
|
72
|
-
|
|
73
|
-
|
|
165
|
+
import { createInitialState, applyStepResult, checkCircuitBreaker, computeStepInput } from 'yaml-flow/step-machine';
|
|
166
|
+
|
|
167
|
+
let state = createInitialState(flow, 'run-1');
|
|
74
168
|
|
|
75
|
-
|
|
76
|
-
|
|
169
|
+
// Your own loop
|
|
170
|
+
while (true) {
|
|
171
|
+
const cb = checkCircuitBreaker(flow, state, state.currentStep);
|
|
172
|
+
if (cb.broken) { state = cb.newState; continue; }
|
|
173
|
+
state = cb.newState;
|
|
174
|
+
|
|
175
|
+
const input = computeStepInput(flow, state.currentStep, allData);
|
|
176
|
+
const stepResult = await handlers[state.currentStep](input, context);
|
|
177
|
+
const { newState, isTerminal } = applyStepResult(flow, state, state.currentStep, stepResult);
|
|
178
|
+
state = newState;
|
|
179
|
+
if (isTerminal) break;
|
|
180
|
+
}
|
|
77
181
|
```
|
|
78
182
|
|
|
79
|
-
|
|
183
|
+
---
|
|
80
184
|
|
|
81
|
-
|
|
185
|
+
## Event Graph — Quick Start
|
|
186
|
+
|
|
187
|
+
The event graph has no driver class. You call two pure functions in a loop:
|
|
188
|
+
|
|
189
|
+
- **`next(graph, state)`** → tells you which tasks are eligible right now
|
|
190
|
+
- **`apply(state, event, graph)`** → applies an event and returns the new state
|
|
191
|
+
|
|
192
|
+
You decide how to actually execute the tasks (call an API, run a function, ask an LLM, etc.).
|
|
193
|
+
|
|
194
|
+
### 1. Define a graph
|
|
82
195
|
|
|
83
196
|
```yaml
|
|
197
|
+
# research-pipeline.yaml
|
|
84
198
|
settings:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
199
|
+
completion: goal-reached
|
|
200
|
+
goal: [final-report]
|
|
201
|
+
conflict_strategy: parallel-all
|
|
202
|
+
|
|
203
|
+
tasks:
|
|
204
|
+
fetch_sources:
|
|
205
|
+
provides: [raw-sources]
|
|
206
|
+
|
|
207
|
+
analyse_sentiment:
|
|
208
|
+
requires: [raw-sources]
|
|
209
|
+
provides: [sentiment-result]
|
|
210
|
+
|
|
211
|
+
analyse_entities:
|
|
212
|
+
requires: [raw-sources]
|
|
213
|
+
provides: [entity-result]
|
|
214
|
+
|
|
215
|
+
merge_analysis:
|
|
216
|
+
requires: [sentiment-result, entity-result]
|
|
217
|
+
provides: [merged-analysis]
|
|
218
|
+
|
|
219
|
+
generate_report:
|
|
220
|
+
requires: [merged-analysis]
|
|
221
|
+
provides: [final-report]
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`fetch_sources` runs first (no requires). Once it completes, both `analyse_sentiment` and `analyse_entities` become eligible in parallel. `merge_analysis` waits for both. `generate_report` waits for the merge. Done when `final-report` appears.
|
|
225
|
+
|
|
226
|
+
### 2. Write a driver loop
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { next, apply, createInitialExecutionState } from 'yaml-flow/event-graph';
|
|
230
|
+
import { parse } from 'yaml';
|
|
231
|
+
import { readFileSync } from 'fs';
|
|
232
|
+
|
|
233
|
+
const graph = parse(readFileSync('./research-pipeline.yaml', 'utf8'));
|
|
234
|
+
let state = createInitialExecutionState(graph, 'exec-1');
|
|
235
|
+
|
|
236
|
+
while (true) {
|
|
237
|
+
const schedule = next(graph, state);
|
|
238
|
+
|
|
239
|
+
if (schedule.isComplete) {
|
|
240
|
+
console.log('Done!', state.availableOutputs);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
if (schedule.stuckDetection.is_stuck) {
|
|
244
|
+
console.error('Stuck:', schedule.stuckDetection.stuck_description);
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Execute all eligible tasks (in parallel)
|
|
249
|
+
const results = await Promise.all(
|
|
250
|
+
schedule.eligibleTasks.map(async (taskName) => {
|
|
251
|
+
state = apply(state, { type: 'task-started', taskName, timestamp: new Date().toISOString() }, graph);
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const output = await executeTask(taskName, state);
|
|
255
|
+
return { taskName, success: true, result: output };
|
|
256
|
+
} catch (err) {
|
|
257
|
+
return { taskName, success: false, error: err.message };
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Feed results back into the reducer
|
|
263
|
+
for (const r of results) {
|
|
264
|
+
if (r.success) {
|
|
265
|
+
state = apply(state, { type: 'task-completed', taskName: r.taskName, result: r.result, timestamp: new Date().toISOString() }, graph);
|
|
266
|
+
} else {
|
|
267
|
+
state = apply(state, { type: 'task-failed', taskName: r.taskName, error: r.error, timestamp: new Date().toISOString() }, graph);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
That's the entire integration. ~30 lines. The engine is pure; your loop owns the I/O.
|
|
274
|
+
|
|
275
|
+
### Event Graph features at a glance
|
|
276
|
+
|
|
277
|
+
| Feature | Config | What it does |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| Dependencies | `requires: [a, b]` / `provides: [c]` | Task runs when all required tokens are available |
|
|
280
|
+
| Conditional routing | `on: { positive: [pos-result], negative: [neg-result] }` | Different outputs based on task result |
|
|
281
|
+
| Failure tokens | `on_failure: [data-unavailable]` | Inject tokens on failure so downstream alternatives can activate |
|
|
282
|
+
| Retry | `retry: { max_attempts: 3 }` | Auto-retry on failure (task resets to not-started) |
|
|
283
|
+
| Repeatable tasks | `repeatable: true` or `repeatable: { max: 5 }` | Task can re-execute when its inputs refresh |
|
|
284
|
+
| Circuit breaker | `circuit_breaker: { max_executions: 10, on_break: [stop-token] }` | Inject tokens after N executions |
|
|
285
|
+
| External events | `apply(state, { type: 'inject-tokens', tokens: ['user-approved'] })` | Unblock tasks waiting on external input |
|
|
286
|
+
| Dynamic tasks | `apply(state, { type: 'task-creation', taskName: 'new', taskConfig: {...} })` | Add tasks at runtime |
|
|
287
|
+
| Completion strategies | `completion: all-tasks-done \| all-outputs-done \| goal-reached \| manual` | When is the graph "done"? |
|
|
288
|
+
| Conflict resolution | `conflict_strategy: alphabetical \| priority-first \| parallel-all \| ...` | What happens when two tasks produce the same output? |
|
|
289
|
+
| Stuck detection | Automatic via `next()` | Returns `is_stuck: true` with description when no progress is possible |
|
|
290
|
+
|
|
291
|
+
### Completion strategies explained
|
|
292
|
+
|
|
293
|
+
| Strategy | Meaning |
|
|
294
|
+
|---|---|
|
|
295
|
+
| `all-tasks-done` | Every task has completed (or failed/inactivated) |
|
|
296
|
+
| `all-outputs-done` | Every `provides` token from every task is available |
|
|
297
|
+
| `goal-reached` | Specific tokens listed in `settings.goal` are available |
|
|
298
|
+
| `only-resolved` | All non-failed tasks have completed |
|
|
299
|
+
| `manual` | Never auto-completes; you decide when to stop |
|
|
300
|
+
|
|
301
|
+
### Conflict resolution strategies
|
|
302
|
+
|
|
303
|
+
When multiple eligible tasks produce the same output token, only one should run (unless you want parallel-all). The `conflict_strategy` setting controls the selection:
|
|
304
|
+
|
|
305
|
+
| Strategy | Behaviour |
|
|
306
|
+
|---|---|
|
|
307
|
+
| `alphabetical` | Pick the alphabetically first task name (default, deterministic) |
|
|
308
|
+
| `priority-first` | Pick the task with the highest `priority` value |
|
|
309
|
+
| `duration-first` | Pick the task with the lowest `estimatedDuration` |
|
|
310
|
+
| `cost-optimized` | Pick the task with the lowest `estimatedCost` |
|
|
311
|
+
| `resource-aware` | Pick the task with the lowest total resource requirements |
|
|
312
|
+
| `round-robin` | Rotate among competing tasks across scheduler calls |
|
|
313
|
+
| `random-select` | Pick one at random |
|
|
314
|
+
| `parallel-all` | Run all competing tasks (no conflict resolution) |
|
|
315
|
+
| `user-choice` | Return all candidates; let the caller decide |
|
|
316
|
+
| `skip-conflicts` | Skip all tasks involved in a conflict |
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Practical Patterns
|
|
321
|
+
|
|
322
|
+
### Pattern: AI Agent Tool Orchestration (Event Graph)
|
|
323
|
+
|
|
324
|
+
An agent needs to gather evidence from multiple sources, then synthesize.
|
|
325
|
+
|
|
326
|
+
```yaml
|
|
327
|
+
settings:
|
|
328
|
+
completion: goal-reached
|
|
329
|
+
goal: [final-answer]
|
|
330
|
+
conflict_strategy: parallel-all
|
|
331
|
+
|
|
332
|
+
tasks:
|
|
333
|
+
search_web:
|
|
334
|
+
provides: [web-results]
|
|
335
|
+
search_database:
|
|
336
|
+
provides: [db-results]
|
|
337
|
+
search_documents:
|
|
338
|
+
provides: [doc-results]
|
|
339
|
+
|
|
340
|
+
synthesize:
|
|
341
|
+
requires: [web-results, db-results, doc-results]
|
|
342
|
+
provides: [draft-answer]
|
|
343
|
+
|
|
344
|
+
verify:
|
|
345
|
+
requires: [draft-answer]
|
|
346
|
+
provides: [final-answer]
|
|
347
|
+
on:
|
|
348
|
+
verified: [final-answer]
|
|
349
|
+
rejected: [needs-revision]
|
|
350
|
+
on_failure: [verification-skipped]
|
|
351
|
+
|
|
352
|
+
revise:
|
|
353
|
+
requires: [needs-revision]
|
|
354
|
+
provides: [draft-answer]
|
|
355
|
+
repeatable: { max: 3 }
|
|
88
356
|
```
|
|
89
357
|
|
|
90
|
-
|
|
358
|
+
The three searches run in parallel. `synthesize` waits for all three. `verify` can produce different token sets depending on its result. If rejected, `revise` picks up and feeds back into `verify` (up to 3 times). If verify itself fails, `verification-skipped` unblocks any downstream task waiting on it.
|
|
359
|
+
|
|
360
|
+
### Pattern: Order Processing Pipeline (Step Machine)
|
|
91
361
|
|
|
92
362
|
```yaml
|
|
363
|
+
settings:
|
|
364
|
+
start_step: validate
|
|
365
|
+
max_total_steps: 15
|
|
366
|
+
|
|
93
367
|
steps:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
368
|
+
validate:
|
|
369
|
+
produces_data: [validated_order]
|
|
370
|
+
transitions:
|
|
371
|
+
valid: charge_payment
|
|
372
|
+
invalid: reject
|
|
373
|
+
|
|
374
|
+
charge_payment:
|
|
375
|
+
expects_data: [validated_order]
|
|
376
|
+
produces_data: [payment_id]
|
|
377
|
+
transitions:
|
|
378
|
+
success: ship
|
|
379
|
+
declined: reject
|
|
380
|
+
retry:
|
|
104
381
|
max_attempts: 3
|
|
105
|
-
delay_ms:
|
|
382
|
+
delay_ms: 2000
|
|
106
383
|
backoff_multiplier: 2
|
|
107
|
-
circuit_breaker: # Optional loop protection
|
|
108
|
-
max_iterations: 5
|
|
109
|
-
on_open: fallback_step
|
|
110
|
-
```
|
|
111
384
|
|
|
112
|
-
|
|
385
|
+
ship:
|
|
386
|
+
expects_data: [validated_order, payment_id]
|
|
387
|
+
produces_data: [tracking_number]
|
|
388
|
+
transitions:
|
|
389
|
+
shipped: confirm
|
|
390
|
+
failure: refund
|
|
391
|
+
|
|
392
|
+
refund:
|
|
393
|
+
expects_data: [payment_id]
|
|
394
|
+
produces_data: [refund_id]
|
|
395
|
+
transitions:
|
|
396
|
+
done: reject
|
|
397
|
+
|
|
398
|
+
confirm:
|
|
399
|
+
expects_data: [tracking_number]
|
|
400
|
+
transitions:
|
|
401
|
+
done: complete
|
|
113
402
|
|
|
114
|
-
```yaml
|
|
115
403
|
terminal_states:
|
|
116
|
-
|
|
117
|
-
return_intent:
|
|
118
|
-
return_artifacts:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
error:
|
|
123
|
-
return_intent: "error"
|
|
404
|
+
complete:
|
|
405
|
+
return_intent: success
|
|
406
|
+
return_artifacts: [tracking_number, payment_id]
|
|
407
|
+
reject:
|
|
408
|
+
return_intent: rejected
|
|
124
409
|
return_artifacts: false
|
|
125
410
|
```
|
|
126
411
|
|
|
127
|
-
|
|
412
|
+
Linear with branches. The current step decides what's next. Retry on payment failures with exponential backoff.
|
|
128
413
|
|
|
129
|
-
|
|
414
|
+
### Pattern: Inject External Events (Event Graph)
|
|
130
415
|
|
|
131
416
|
```typescript
|
|
132
|
-
|
|
417
|
+
// A task is waiting for human approval
|
|
418
|
+
const graph = {
|
|
419
|
+
settings: { completion: 'goal-reached', goal: ['deployed'] },
|
|
420
|
+
tasks: {
|
|
421
|
+
build: { provides: ['build-artifact'] },
|
|
422
|
+
test: { requires: ['build-artifact'], provides: ['test-passed'] },
|
|
423
|
+
approve: { requires: ['test-passed', 'human-approval'], provides: ['approved'] },
|
|
424
|
+
deploy: { requires: ['approved'], provides: ['deployed'] },
|
|
425
|
+
},
|
|
426
|
+
};
|
|
133
427
|
|
|
134
|
-
|
|
135
|
-
[key: string]: unknown; // Data from expects_data
|
|
136
|
-
}
|
|
428
|
+
let state = createInitialExecutionState(graph, 'deploy-1');
|
|
137
429
|
|
|
138
|
-
|
|
139
|
-
runId: string; // Current run ID
|
|
140
|
-
stepName: string; // Current step name
|
|
141
|
-
components: object; // Injected dependencies
|
|
142
|
-
store: FlowStore; // Direct store access
|
|
143
|
-
signal?: AbortSignal; // Cancellation signal
|
|
144
|
-
emit: (event, data) => void; // Event emitter
|
|
145
|
-
}
|
|
430
|
+
// ... run build and test normally ...
|
|
146
431
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
432
|
+
// Later, when a human clicks "Approve" in your UI:
|
|
433
|
+
state = apply(state, {
|
|
434
|
+
type: 'inject-tokens',
|
|
435
|
+
tokens: ['human-approval'],
|
|
436
|
+
timestamp: new Date().toISOString(),
|
|
437
|
+
}, graph);
|
|
438
|
+
|
|
439
|
+
// Now next(graph, state) will return 'approve' as eligible
|
|
151
440
|
```
|
|
152
441
|
|
|
153
|
-
###
|
|
442
|
+
### Pattern: Conditional Branching in Event Graph
|
|
154
443
|
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
result: 'failure',
|
|
176
|
-
data: { error: error.message }
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
};
|
|
444
|
+
```yaml
|
|
445
|
+
tasks:
|
|
446
|
+
classify_image:
|
|
447
|
+
provides: [classification]
|
|
448
|
+
on:
|
|
449
|
+
photo: [is-photo]
|
|
450
|
+
document: [is-document]
|
|
451
|
+
screenshot: [is-screenshot]
|
|
452
|
+
|
|
453
|
+
enhance_photo:
|
|
454
|
+
requires: [is-photo]
|
|
455
|
+
provides: [processed-image]
|
|
456
|
+
|
|
457
|
+
ocr_document:
|
|
458
|
+
requires: [is-document]
|
|
459
|
+
provides: [extracted-text]
|
|
460
|
+
|
|
461
|
+
crop_screenshot:
|
|
462
|
+
requires: [is-screenshot]
|
|
463
|
+
provides: [processed-image]
|
|
181
464
|
```
|
|
182
465
|
|
|
466
|
+
Only one downstream path activates based on the classifier result. This is the event-graph equivalent of a switch statement.
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
183
470
|
## Storage Adapters
|
|
184
471
|
|
|
185
|
-
|
|
472
|
+
All three stores work with both modes. Step Machine uses them for run state persistence. Event Graph state is plain JSON — serialize it yourself or use a store.
|
|
186
473
|
|
|
187
|
-
|
|
188
|
-
import { MemoryStore } from 'yaml-flow';
|
|
474
|
+
### Memory (default, all environments)
|
|
189
475
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
});
|
|
476
|
+
```typescript
|
|
477
|
+
import { MemoryStore } from 'yaml-flow/stores/memory';
|
|
193
478
|
```
|
|
194
479
|
|
|
195
|
-
### LocalStorage (
|
|
480
|
+
### LocalStorage (browser)
|
|
196
481
|
|
|
197
482
|
```typescript
|
|
198
483
|
import { LocalStorageStore } from 'yaml-flow/stores/localStorage';
|
|
199
|
-
|
|
200
|
-
const engine = createEngine(flow, handlers, {
|
|
201
|
-
store: new LocalStorageStore({ prefix: 'myapp' })
|
|
202
|
-
});
|
|
484
|
+
new LocalStorageStore({ prefix: 'myapp' });
|
|
203
485
|
```
|
|
204
486
|
|
|
205
487
|
### File System (Node.js)
|
|
206
488
|
|
|
207
489
|
```typescript
|
|
208
490
|
import { FileStore } from 'yaml-flow/stores/file';
|
|
209
|
-
|
|
210
|
-
const engine = createEngine(flow, handlers, {
|
|
211
|
-
store: new FileStore({ directory: './flow-data' })
|
|
212
|
-
});
|
|
491
|
+
new FileStore({ directory: './flow-data' });
|
|
213
492
|
```
|
|
214
493
|
|
|
215
494
|
### Custom Store
|
|
216
495
|
|
|
217
|
-
Implement the `
|
|
496
|
+
Implement the `StepMachineStore` interface:
|
|
218
497
|
|
|
219
498
|
```typescript
|
|
220
|
-
interface
|
|
221
|
-
saveRunState(runId: string, state:
|
|
222
|
-
loadRunState(runId: string): Promise<
|
|
499
|
+
interface StepMachineStore {
|
|
500
|
+
saveRunState(runId: string, state: StepMachineState): Promise<void>;
|
|
501
|
+
loadRunState(runId: string): Promise<StepMachineState | null>;
|
|
223
502
|
deleteRunState(runId: string): Promise<void>;
|
|
224
503
|
setData(runId: string, key: string, value: unknown): Promise<void>;
|
|
225
504
|
getData(runId: string, key: string): Promise<unknown>;
|
|
@@ -228,152 +507,353 @@ interface FlowStore {
|
|
|
228
507
|
}
|
|
229
508
|
```
|
|
230
509
|
|
|
231
|
-
|
|
510
|
+
---
|
|
232
511
|
|
|
233
|
-
|
|
512
|
+
## Batch Processing
|
|
513
|
+
|
|
514
|
+
yaml-flow includes a `batch()` utility for running multiple items through a flow concurrently. It works with both Step Machine and Event Graph — you provide the processor, it manages concurrency.
|
|
515
|
+
|
|
516
|
+
### Quick Start
|
|
234
517
|
|
|
235
518
|
```typescript
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
519
|
+
import { batch } from 'yaml-flow/batch';
|
|
520
|
+
import { createStepMachine } from 'yaml-flow/step-machine';
|
|
521
|
+
|
|
522
|
+
const tickets = [
|
|
523
|
+
{ id: 'T-001', message: 'Billing error' },
|
|
524
|
+
{ id: 'T-002', message: 'App crashes on login' },
|
|
525
|
+
{ id: 'T-003', message: 'Password reset help' },
|
|
526
|
+
];
|
|
527
|
+
|
|
528
|
+
const result = await batch(tickets, {
|
|
529
|
+
concurrency: 3,
|
|
530
|
+
processor: async (ticket) => {
|
|
531
|
+
const machine = createStepMachine(flow, handlers);
|
|
532
|
+
return machine.run({ message: ticket.message });
|
|
533
|
+
},
|
|
534
|
+
onProgress: (p) => console.log(`${p.percent}% done`),
|
|
243
535
|
});
|
|
244
536
|
|
|
245
|
-
|
|
246
|
-
const handlers = {
|
|
247
|
-
async fetchData(input, ctx) {
|
|
248
|
-
const result = await ctx.components.api.get('/data');
|
|
249
|
-
return { result: 'success', data: { fetched: result } };
|
|
250
|
-
}
|
|
251
|
-
};
|
|
537
|
+
console.log(`${result.completed} succeeded, ${result.failed} failed`);
|
|
252
538
|
```
|
|
253
539
|
|
|
254
|
-
|
|
540
|
+
### Options
|
|
541
|
+
|
|
542
|
+
| Option | Type | Default | Description |
|
|
543
|
+
|---|---|---|---|
|
|
544
|
+
| `concurrency` | `number` | `5` | Max parallel processors |
|
|
545
|
+
| `processor` | `(item, index) => Promise<TResult>` | *required* | Async function to process each item |
|
|
546
|
+
| `signal` | `AbortSignal` | — | Cancel remaining items |
|
|
547
|
+
| `onItemComplete` | `(item, result, index) => void` | — | Called when an item succeeds |
|
|
548
|
+
| `onItemError` | `(item, error, index) => void` | — | Called when an item fails |
|
|
549
|
+
| `onProgress` | `(progress) => void` | — | Called after each item with `{ completed, failed, active, pending, total, percent, elapsedMs }` |
|
|
255
550
|
|
|
256
|
-
|
|
551
|
+
### Result Shape
|
|
257
552
|
|
|
258
553
|
```typescript
|
|
259
|
-
|
|
554
|
+
{
|
|
555
|
+
items: BatchItemResult[]; // Per-item: { item, index, status, result?, error?, durationMs }
|
|
556
|
+
completed: number; // Items that succeeded
|
|
557
|
+
failed: number; // Items that threw
|
|
558
|
+
total: number;
|
|
559
|
+
durationMs: number; // Wall-clock time for entire batch
|
|
560
|
+
}
|
|
561
|
+
```
|
|
260
562
|
|
|
261
|
-
|
|
262
|
-
const unsubscribe = engine.on('step:complete', (event) => {
|
|
263
|
-
console.log(`Step ${event.data.step} completed with ${event.data.result}`);
|
|
264
|
-
});
|
|
563
|
+
### Works with Event Graph too
|
|
265
564
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
565
|
+
```typescript
|
|
566
|
+
import { batch } from 'yaml-flow/batch';
|
|
567
|
+
import { next, apply, createInitialExecutionState } from 'yaml-flow/event-graph';
|
|
568
|
+
|
|
569
|
+
const result = await batch(items, {
|
|
570
|
+
concurrency: 5,
|
|
571
|
+
processor: async (item) => {
|
|
572
|
+
let state = createInitialExecutionState(graph, `run-${item.id}`);
|
|
573
|
+
state = apply(state, { type: 'inject-tokens', tokens: [item.readyToken], timestamp: Date.now() }, graph);
|
|
574
|
+
// ... drive graph loop with next() + apply()
|
|
575
|
+
return state;
|
|
576
|
+
},
|
|
577
|
+
});
|
|
270
578
|
```
|
|
271
579
|
|
|
272
|
-
|
|
580
|
+
---
|
|
273
581
|
|
|
274
|
-
|
|
275
|
-
// Start flow
|
|
276
|
-
const result = engine.run({ data: 'value' });
|
|
582
|
+
## Config Utilities
|
|
277
583
|
|
|
278
|
-
|
|
279
|
-
await engine.pause(runId);
|
|
584
|
+
Pure pre-processing transforms you apply before passing config to the engine. They never touch engine state — just config in, config out.
|
|
280
585
|
|
|
281
|
-
|
|
282
|
-
const resumed = await engine.resume(runId);
|
|
283
|
-
```
|
|
586
|
+
### Variable Interpolation
|
|
284
587
|
|
|
285
|
-
|
|
588
|
+
Replace `${KEY}` patterns in any config object. Works with both GraphConfig and StepFlowConfig.
|
|
286
589
|
|
|
287
590
|
```typescript
|
|
288
|
-
|
|
591
|
+
import { resolveVariables } from 'yaml-flow/config';
|
|
289
592
|
|
|
290
|
-
const
|
|
291
|
-
|
|
593
|
+
const resolved = resolveVariables(graphConfig, {
|
|
594
|
+
ENTITY_ID: 'ticket-42',
|
|
595
|
+
TOOLS_DIR: '/opt/tools',
|
|
596
|
+
WORKDIR: '/data/workdata',
|
|
292
597
|
});
|
|
598
|
+
// Every ${ENTITY_ID} in task configs, cmd-args, etc. → replaced
|
|
599
|
+
```
|
|
293
600
|
|
|
294
|
-
|
|
295
|
-
const resultPromise = engine.run();
|
|
601
|
+
### Config Templates
|
|
296
602
|
|
|
297
|
-
|
|
298
|
-
|
|
603
|
+
DRY reusable config blocks. Tasks reference a named template via `config-template`; the function deep-merges template + task overrides and removes the reference.
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
import { resolveConfigTemplates } from 'yaml-flow/config';
|
|
607
|
+
|
|
608
|
+
const config = {
|
|
609
|
+
configTemplates: { // or 'config-templates' (kebab-case)
|
|
610
|
+
PYTHON_TOOL: { cmd: 'python', timeout: 30000, cwd: '/workdata' },
|
|
611
|
+
NODE_CMD: { cmd: 'node', timeout: 60000 },
|
|
612
|
+
},
|
|
613
|
+
tasks: {
|
|
614
|
+
analyze: { provides: ['analysis'], config: { 'config-template': 'PYTHON_TOOL', 'cmd-args': 'analyze.py' } },
|
|
615
|
+
build: { provides: ['build'], config: { 'config-template': 'NODE_CMD', script: 'build.js' } },
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const resolved = resolveConfigTemplates(config);
|
|
620
|
+
// analyze.config → { cmd: 'python', timeout: 30000, cwd: '/workdata', 'cmd-args': 'analyze.py' }
|
|
621
|
+
// configTemplates key removed from output
|
|
299
622
|
```
|
|
300
623
|
|
|
301
|
-
|
|
624
|
+
### Composing Both
|
|
302
625
|
|
|
303
|
-
|
|
626
|
+
Templates first (expands references), then variables (fills in `${...}` placeholders):
|
|
304
627
|
|
|
305
628
|
```typescript
|
|
306
|
-
import {
|
|
629
|
+
import { resolveConfigTemplates, resolveVariables } from 'yaml-flow/config';
|
|
307
630
|
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
631
|
+
const raw = loadYaml('pipeline.yaml'); // has configTemplates + ${VAR} refs
|
|
632
|
+
const resolved = resolveVariables(
|
|
633
|
+
resolveConfigTemplates(raw),
|
|
634
|
+
{ ENTITY_ID: 'url-42', TOOLS_DIR: '/opt/tools' },
|
|
635
|
+
);
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## Graph-of-Graphs Pattern
|
|
641
|
+
|
|
642
|
+
Real-world pipelines are often **layered**: an outer orchestration graph where some tasks are themselves entire sub-workflows — each processing a batch of items through their own DAG or step flow. yaml-flow doesn't bake this into the engine (the pure scheduler stays simple), but the primitives compose cleanly.
|
|
643
|
+
|
|
644
|
+
### The Shape
|
|
314
645
|
|
|
315
|
-
const engine = createEngine(flow, handlers);
|
|
316
646
|
```
|
|
647
|
+
Outer graph (event-graph)
|
|
648
|
+
├── prep-workdata → plain task
|
|
649
|
+
├── copy-input-files → plain task
|
|
650
|
+
├── evidence-gathering → batch × inner event-graph (N items, 5 concurrent)
|
|
651
|
+
├── grade-synthesis → batch × inner step-machine (N items, 3 concurrent)
|
|
652
|
+
├── analyze-mismatches → plain task
|
|
653
|
+
├── [health-check ∥ report]→ parallel plain tasks
|
|
654
|
+
└── archive-results → waits for both
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
The outer graph sequences coarse stages. Some stages fan out into batches where each item runs through its own sub-workflow. The sub can be either mode.
|
|
658
|
+
|
|
659
|
+
### How to Wire It
|
|
317
660
|
|
|
318
|
-
|
|
661
|
+
Each "sub-graph task" in the outer graph is just a handler that composes `resolveConfigTemplates` → `resolveVariables` → `batch` → engine:
|
|
319
662
|
|
|
320
663
|
```typescript
|
|
321
|
-
import {
|
|
664
|
+
import { next, apply, createInitialExecutionState } from 'yaml-flow/event-graph';
|
|
665
|
+
import { createStepMachine } from 'yaml-flow/step-machine';
|
|
666
|
+
import { batch } from 'yaml-flow/batch';
|
|
667
|
+
import { resolveVariables, resolveConfigTemplates } from 'yaml-flow/config';
|
|
668
|
+
|
|
669
|
+
// Outer graph handler for a sub-graph task (event-graph sub)
|
|
670
|
+
async function runEvidenceBatch(items, rawSubConfig) {
|
|
671
|
+
return batch(items, {
|
|
672
|
+
concurrency: 5,
|
|
673
|
+
processor: async (item) => {
|
|
674
|
+
// Resolve config per-item (each item gets its own ENTITY_ID)
|
|
675
|
+
const config = resolveVariables(
|
|
676
|
+
resolveConfigTemplates(rawSubConfig),
|
|
677
|
+
{ ENTITY_ID: item.id, TOOLS_DIR: '/opt/tools' },
|
|
678
|
+
);
|
|
679
|
+
// Drive the inner event-graph
|
|
680
|
+
let state = createInitialExecutionState(config, `run-${item.id}`);
|
|
681
|
+
while (true) {
|
|
682
|
+
const { eligibleTasks, isComplete } = next(config, state);
|
|
683
|
+
if (isComplete) break;
|
|
684
|
+
for (const task of eligibleTasks) {
|
|
685
|
+
state = apply(state, { type: 'task-started', taskName: task, timestamp: new Date().toISOString() }, config);
|
|
686
|
+
const result = await executeTask(task, config.tasks[task], item);
|
|
687
|
+
state = apply(state, { type: 'task-completed', taskName: task, result, timestamp: new Date().toISOString() }, config);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return state;
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
}
|
|
322
694
|
|
|
323
|
-
|
|
324
|
-
|
|
695
|
+
// Outer graph handler for a sub-graph task (step-machine sub)
|
|
696
|
+
async function runGradeBatch(items, flowConfig, handlers) {
|
|
697
|
+
return batch(items, {
|
|
698
|
+
concurrency: 3,
|
|
699
|
+
processor: async (item) => {
|
|
700
|
+
const machine = createStepMachine(flowConfig, handlers);
|
|
701
|
+
return machine.run({ entityId: item.id, evidence: item.evidence });
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
}
|
|
325
705
|
```
|
|
326
706
|
|
|
327
|
-
|
|
707
|
+
### Driving the Outer Graph
|
|
328
708
|
|
|
329
|
-
|
|
709
|
+
The outer graph itself is an event-graph. Each handler maps to a task:
|
|
330
710
|
|
|
331
|
-
```
|
|
332
|
-
|
|
711
|
+
```typescript
|
|
712
|
+
const outerHandlers = {
|
|
713
|
+
'prep-workdata': async () => { /* setup */ },
|
|
714
|
+
'copy-input-files': async () => { /* parse CSV, return items */ },
|
|
715
|
+
'evidence-batch': async (ctx) => runEvidenceBatch(ctx.items, evidenceConfig),
|
|
716
|
+
'grade-batch': async (ctx) => runGradeBatch(ctx.items, gradeFlow, gradeHandlers),
|
|
717
|
+
'analyze-mismatches':async (ctx) => { /* compare grades */ },
|
|
718
|
+
'health-check': async (ctx) => { /* validate */ },
|
|
719
|
+
'generate-report': async (ctx) => { /* summarize */ },
|
|
720
|
+
'archive': async (ctx) => { /* move outputs */ },
|
|
721
|
+
};
|
|
333
722
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
723
|
+
// Simple outer loop
|
|
724
|
+
let state = createInitialExecutionState(outerGraph, 'pipeline-run-1');
|
|
725
|
+
while (true) {
|
|
726
|
+
const { eligibleTasks, isComplete } = next(outerGraph, state);
|
|
727
|
+
if (isComplete) break;
|
|
728
|
+
await Promise.all(eligibleTasks.map(async (taskName) => {
|
|
729
|
+
state = apply(state, { type: 'task-started', taskName, timestamp: now() }, outerGraph);
|
|
730
|
+
try {
|
|
731
|
+
await outerHandlers[taskName](context);
|
|
732
|
+
state = apply(state, { type: 'task-completed', taskName, timestamp: now() }, outerGraph);
|
|
733
|
+
} catch (err) {
|
|
734
|
+
state = apply(state, { type: 'task-failed', taskName, error: err.message, timestamp: now() }, outerGraph);
|
|
735
|
+
}
|
|
736
|
+
}));
|
|
737
|
+
}
|
|
337
738
|
```
|
|
338
739
|
|
|
339
|
-
|
|
740
|
+
### Why Not Bake It Into the Engine?
|
|
340
741
|
|
|
341
|
-
|
|
742
|
+
- The pure scheduler (`next`/`apply`) stays a simple `f(state, event) → newState`.
|
|
743
|
+
- Sub-graph execution involves file I/O, process spawning, HTTP calls — all driver concerns.
|
|
744
|
+
- Every deployment customizes how sub-tasks execute: in-process, `execSync`, HTTP, serverless.
|
|
745
|
+
- The primitives (`batch` + `resolveVariables` + `resolveConfigTemplates` + both engines) compose without coupling.
|
|
342
746
|
|
|
343
|
-
|
|
747
|
+
See the [examples/graph-of-graphs/](./examples/graph-of-graphs/) directory for complete runnable examples.
|
|
344
748
|
|
|
345
|
-
|
|
749
|
+
---
|
|
346
750
|
|
|
347
|
-
|
|
751
|
+
## Package Exports
|
|
348
752
|
|
|
349
|
-
|
|
753
|
+
```typescript
|
|
754
|
+
// Everything (both modes + stores + batch)
|
|
755
|
+
import { StepMachine, next, apply, MemoryStore, batch } from 'yaml-flow';
|
|
350
756
|
|
|
351
|
-
|
|
757
|
+
// Step Machine only
|
|
758
|
+
import { StepMachine, createStepMachine, loadStepFlow } from 'yaml-flow/step-machine';
|
|
759
|
+
import { applyStepResult, checkCircuitBreaker, createInitialState } from 'yaml-flow/step-machine';
|
|
352
760
|
|
|
353
|
-
|
|
761
|
+
// Event Graph only
|
|
762
|
+
import { next, apply, applyAll, getCandidateTasks } from 'yaml-flow/event-graph';
|
|
763
|
+
import { createInitialExecutionState, isExecutionComplete, detectStuckState } from 'yaml-flow/event-graph';
|
|
764
|
+
import { TASK_STATUS, COMPLETION_STRATEGIES, CONFLICT_STRATEGIES } from 'yaml-flow/event-graph';
|
|
354
765
|
|
|
355
|
-
|
|
766
|
+
// Stores
|
|
767
|
+
import { MemoryStore, LocalStorageStore, FileStore } from 'yaml-flow/stores';
|
|
356
768
|
|
|
357
|
-
|
|
769
|
+
// Batch
|
|
770
|
+
import { batch } from 'yaml-flow/batch';
|
|
771
|
+
import type { BatchOptions, BatchResult, BatchItemResult, BatchProgress } from 'yaml-flow/batch';
|
|
358
772
|
|
|
359
|
-
|
|
773
|
+
// Config utilities
|
|
774
|
+
import { resolveVariables, resolveConfigTemplates } from 'yaml-flow/config';
|
|
360
775
|
|
|
361
|
-
|
|
776
|
+
// Backward compatibility (v1 names → v2)
|
|
777
|
+
import { FlowEngine, createEngine } from 'yaml-flow'; // aliases for StepMachine, createStepMachine
|
|
778
|
+
```
|
|
362
779
|
|
|
363
|
-
|
|
780
|
+
---
|
|
364
781
|
|
|
365
|
-
|
|
782
|
+
## API Reference
|
|
366
783
|
|
|
367
|
-
|
|
784
|
+
### Step Machine
|
|
785
|
+
|
|
786
|
+
| Export | Description |
|
|
787
|
+
|---|---|
|
|
788
|
+
| `createStepMachine(flow, handlers, options?)` | Create and validate a StepMachine instance |
|
|
789
|
+
| `StepMachine.run(initialData?)` | Execute flow from start, returns `StepMachineResult` |
|
|
790
|
+
| `StepMachine.pause(runId)` | Pause a running flow |
|
|
791
|
+
| `StepMachine.resume(runId)` | Resume a paused flow |
|
|
792
|
+
| `StepMachine.on(event, listener)` | Subscribe to events (`step:start`, `step:complete`, `flow:complete`, `transition`, etc.) |
|
|
793
|
+
| `loadStepFlow(path)` | Load + validate a YAML/JSON flow file |
|
|
794
|
+
| `applyStepResult(flow, state, step, result)` | Pure reducer: apply a step result to state |
|
|
795
|
+
| `checkCircuitBreaker(flow, state, step)` | Pure: check/increment circuit breaker |
|
|
796
|
+
| `computeStepInput(flow, step, allData)` | Pure: filter data to what a step expects |
|
|
797
|
+
| `createInitialState(flow, runId)` | Pure: create starting state |
|
|
798
|
+
|
|
799
|
+
### Event Graph
|
|
800
|
+
|
|
801
|
+
| Export | Description |
|
|
802
|
+
|---|---|
|
|
803
|
+
| `next(graph, state)` | Pure scheduler: returns `{ eligibleTasks, isComplete, stuckDetection, conflicts }` |
|
|
804
|
+
| `apply(state, event, graph)` | Pure reducer: apply one event, returns new state |
|
|
805
|
+
| `applyAll(state, events, graph)` | Apply multiple events sequentially |
|
|
806
|
+
| `createInitialExecutionState(graph, executionId)` | Create starting state for a graph |
|
|
807
|
+
| `getCandidateTasks(graph, state)` | Low-level: just the eligible task list |
|
|
808
|
+
| `isExecutionComplete(graph, state)` | Check completion against configured strategy |
|
|
809
|
+
| `detectStuckState({graph, state, ...})` | Check if execution is stuck |
|
|
810
|
+
| `addDynamicTask(graph, name, config)` | Immutably add a task to a graph config |
|
|
811
|
+
|
|
812
|
+
### Event Types (for `apply()`)
|
|
813
|
+
|
|
814
|
+
| Event | Fields | Effect |
|
|
815
|
+
|---|---|---|
|
|
816
|
+
| `task-started` | `taskName` | Sets task status to `running` |
|
|
817
|
+
| `task-completed` | `taskName`, `result?` | Marks completed, adds `provides` tokens (or `on[result]` tokens) |
|
|
818
|
+
| `task-failed` | `taskName`, `error` | Retries or marks failed, injects `on_failure` tokens |
|
|
819
|
+
| `task-progress` | `taskName`, `message?`, `progress?` | Updates progress/messages |
|
|
820
|
+
| `inject-tokens` | `tokens[]` | Adds tokens to available outputs (unblocks waiting tasks) |
|
|
821
|
+
| `agent-action` | `action: start\|stop\|pause\|resume` | Controls execution lifecycle |
|
|
822
|
+
| `task-creation` | `taskName`, `taskConfig` | Adds a new task to execution state |
|
|
823
|
+
|
|
824
|
+
---
|
|
368
825
|
|
|
369
826
|
## Examples
|
|
370
827
|
|
|
371
|
-
See the [examples](./examples) directory:
|
|
828
|
+
See the [examples/](./examples) directory:
|
|
829
|
+
|
|
830
|
+
| Example | Mode | Demonstrates |
|
|
831
|
+
|---|---|---|
|
|
832
|
+
| [Simple Greeting](./examples/node/simple-greeting.ts) | Step Machine | Basic flow with file store |
|
|
833
|
+
| [AI Conversation](./examples/node/ai-conversation.ts) | Step Machine | Retry, circuit breakers, component injection |
|
|
834
|
+
| [Research Pipeline](./examples/event-graph/research-pipeline.ts) | Event Graph | Parallel tasks, goal-based completion |
|
|
835
|
+
| [CI/CD Pipeline](./examples/event-graph/ci-cd-pipeline.ts) | Event Graph | External events, conditional routing, failure tokens |
|
|
836
|
+
| [Batch Tickets](./examples/batch/batch-step-machine.ts) | Batch | Concurrent processing, progress tracking |
|
|
837
|
+
| [URL Pipeline](./examples/graph-of-graphs/url-processing-pipeline.ts) | Graph-of-Graphs | Outer event-graph → batch × inner event-graph per item |
|
|
838
|
+
| [Multi-Stage ETL](./examples/graph-of-graphs/multi-stage-etl.ts) | Graph-of-Graphs | Mixed modes: event-graph outer → step-machine + event-graph subs |
|
|
839
|
+
| [Order Processing](./examples/flows/order-processing.yaml) | Step Machine | YAML flow definition |
|
|
840
|
+
| [Browser Demo](./examples/browser/index.html) | Step Machine | In-browser usage |
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
## Migrating from v1
|
|
845
|
+
|
|
846
|
+
v2 is backward compatible. The old names still work:
|
|
847
|
+
|
|
848
|
+
```typescript
|
|
849
|
+
// v1 (still works)
|
|
850
|
+
import { FlowEngine, createEngine } from 'yaml-flow';
|
|
851
|
+
|
|
852
|
+
// v2 (preferred)
|
|
853
|
+
import { StepMachine, createStepMachine } from 'yaml-flow/step-machine';
|
|
854
|
+
```
|
|
372
855
|
|
|
373
|
-
|
|
374
|
-
- [AI Conversation (Node.js)](./examples/node/ai-conversation.ts)
|
|
375
|
-
- [Browser Demo](./examples/browser/index.html)
|
|
376
|
-
- [Order Processing (Flow)](./examples/flows/order-processing.yaml)
|
|
856
|
+
The `FlowStore` interface is now `StepMachineStore` (same shape). `RunState` is now `StepMachineState` (same shape). Both old names resolve to the new types.
|
|
377
857
|
|
|
378
858
|
## License
|
|
379
859
|
|