flowneer 0.7.1 → 0.9.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.
Files changed (40) hide show
  1. package/README.md +223 -761
  2. package/dist/{FlowBuilder-DJkzGH5l.d.ts → FlowBuilder-B_BIRk3f.d.ts} +42 -52
  3. package/dist/errors-u-hq7p5N.d.ts +16 -0
  4. package/dist/index.d.ts +3 -2
  5. package/dist/index.js +58 -43
  6. package/dist/plugins/agent/index.d.ts +57 -144
  7. package/dist/plugins/agent/index.js +2 -833
  8. package/dist/plugins/config/index.d.ts +26 -0
  9. package/dist/plugins/config/index.js +159 -0
  10. package/dist/plugins/dev/index.d.ts +87 -2
  11. package/dist/plugins/dev/index.js +131 -0
  12. package/dist/plugins/eval/index.d.ts +1 -1
  13. package/dist/plugins/graph/index.d.ts +1 -1
  14. package/dist/plugins/index.d.ts +147 -3
  15. package/dist/plugins/index.js +522 -673
  16. package/dist/plugins/llm/index.d.ts +18 -2
  17. package/dist/plugins/llm/index.js +16 -11
  18. package/dist/plugins/memory/index.d.ts +1 -1
  19. package/dist/plugins/messaging/index.d.ts +6 -1
  20. package/dist/plugins/observability/index.d.ts +1 -1
  21. package/dist/plugins/output/index.d.ts +1 -1
  22. package/dist/plugins/persistence/index.d.ts +1 -1
  23. package/dist/plugins/resilience/index.d.ts +1 -1
  24. package/dist/plugins/telemetry/index.d.ts +1 -1
  25. package/dist/plugins/tools/index.d.ts +1 -1
  26. package/dist/presets/agent/index.d.ts +435 -0
  27. package/dist/presets/agent/index.js +956 -0
  28. package/dist/presets/config/index.d.ts +64 -0
  29. package/dist/presets/config/index.js +879 -0
  30. package/dist/presets/index.d.ts +7 -0
  31. package/dist/presets/index.js +1317 -0
  32. package/dist/presets/pipeline/index.d.ts +88 -0
  33. package/dist/presets/pipeline/index.js +620 -0
  34. package/dist/presets/rag/index.d.ts +77 -0
  35. package/dist/presets/rag/index.js +617 -0
  36. package/dist/schema-DFNzcQP5.d.ts +72 -0
  37. package/dist/src/index.d.ts +4 -18
  38. package/dist/src/index.js +58 -43
  39. package/package.json +9 -2
  40. package/dist/patterns-CCtG27Gv.d.ts +0 -195
package/README.md CHANGED
@@ -9,10 +9,30 @@
9
9
  <a href="https://www.npmjs.com/package/flowneer"><img src="https://img.shields.io/npm/d18m/flowneer" /></a>
10
10
  <a href="https://deepwiki.com/Fanna1119/flowneer"><img src="https://deepwiki.com/badge.svg" /></a>
11
11
  <a href="https://github.com/Fanna1119/flowneer"><img src="https://img.shields.io/badge/GitHub-%23121011.svg?logo=github&logoColor=white)" /></a>
12
- <a href="https://context7.com/fanna1119/flowneer"><img src="https://img.shields.io/badge/-Context7-black?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMjgiIGhlaWdodD0iMjgiIHZpZXdCb3g9IjAgMCAyOCAyOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI4IiBoZWlnaHQ9IjI4IiByeD0iNCIgZmlsbD0iIzA1OTY2OSIvPgo8cGF0aCBkPSJNMTAuNTcyNCAxNS4yNTY1QzEwLjU3MjQgMTcuNTAyNSA5LjY2MTMgMTkuMzc3OCA4LjE3ODA1IDIxLjEwNDdIMTEuNjMxOUwxMS42MzE5IDIyLjc3ODZINi4zMzQ1OVYyMS4xODk1QzcuOTU1NTcgMTkuMzU2NiA4LjU4MDY1IDE3Ljg2MjggOC41ODA2NSAxNS4yNTY1TDEwLjU3MjQgMTUuMjU2NVoiIGZpbGw9IndoaXRlIi8%2BCjxwYXRoIGQ9Ik0xNy40Mjc2IDE1LjI1NjVDMTcuNDI3NiAxNy41MDI1IDE4LjMzODcgMTkuMzc3OCAxOS44MjIgMjEuMTA0N0gxNi4zNjgxVjIyLjc3ODZIMjEuNjY1NFYyMS4xODk1QzIwLjA0NDQgMTkuMzU2NiAxOS40MTk0IDE3Ljg2MjggMTkuNDE5NCAxNS4yNTY1SDE3LjQyNzZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTAuNTcyNCAxMi43NDM1QzEwLjU3MjQgMTAuNDk3NSA5LjY2MTMxIDguNjIyMjQgOC4xNzgwNyA2Ljg5NTMyTDExLjYzMTkgNi44OTUzMlY1LjIyMTM3TDYuMzM0NjEgNS4yMjEzN1Y2LjgxMDU2QzcuOTU1NTggOC42NDM0MyA4LjU4MDY2IDEwLjEzNzMgOC41ODA2NiAxMi43NDM1TDEwLjU3MjQgMTIuNzQzNVoiIGZpbGw9IndoaXRlIi8%2BCjxwYXRoIGQ9Ik0xNy40Mjc2IDEyLjc0MzVDMTcuNDI3NiAxMC40OTc1IDE4LjMzODcgOC42MjIyNCAxOS44MjIgNi44OTUzMkwxNi4zNjgxIDYuODk1MzJMMTYuMzY4MSA1LjIyMTM4TDIxLjY2NTQgNS4yMjEzOFY2LjgxMDU2QzIwLjA0NDQgOC42NDM0MyAxOS40MTk0IDEwLjEzNzMgMTkuNDE5NCAxMi43NDM1SDE3LjQyNzZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K" alt="Badge"></a>
12
+ <a href="https://context7.com/fanna1119/flowneer"><img src="https://img.shields.io/badge/-Context7-black?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMjgiIGhlaWdodD0iMjgiIHZpZXdCb3g9IjAgMCAyOCAyOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI4IiBoZWlnaHQ9IjI4IiByeD0iNCIgZmlsbD0iIzA1OTY2OSIvPgo8cGF0aCBkPSJNMTAuNTcyNCAxNS4yNTY1QzEwLjU3MjQgMTcuNTAyNSA5LjY2MTMgMTkuMzc3OCA4LjE3ODA1IDIxLjEwNDdIMTEuNjMxOUwxMS42MzE5IDIyLjc3ODZINi4zMzQ1OVYyMS4xODk1QzcuOTU1NTcgMTkuMzU2NiA4LjU4MDY1IDE3Ljg2MjggOC41ODA2NSAxNS4yNTY1TDEwLjU3MjQgMTUuMjU2NVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xNy40Mjc2IDE1LjI1NjVDMTcuNDI3NiAxNy41MDI1IDE4LjMzODcgMTkuMzc3OCAxOS44MjIgMjEuMTA0N0gxNi4zNjgxVjIyLjc3ODZIMjEuNjY1NFYyMS4xODk1QzIwLjA0NDQgMTkuMzU2NiAxOS40MTk0IDE3Ljg2MjggMTkuNDE5NCAxNS4yNTY1SDE3LjQyNzZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTAuNTcyNCAxMi43NDM1QzEwLjU3MjQgMTAuNDk3NSA5LjY2MTMxIDguNjIyMjQgOC4xNzgwNyA2Ljg5NTMyTDExLjYzMTkgNi44OTUzMlY1LjIyMTM3TDYuMzM0NjEgNS4yMjEzN1Y2LjgxMDU2QzcuOTU1NTggOC42NDM0MyA4LjU4MDY2IDEwLjEzNzMgOC41ODA2NiAxMi43NDM1TDEwLjU3MjQgMTIuNzQzNVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xNy40Mjc2IDEyLjc0MzVDMTcuNDI3NiAxMC40OTc1IDE4LjMzODcgOC42MjIyNCAxOS44MjIgNi44OTUzMkwxNi4zNjgxIDYuODk1MzJMMTYuMzY4MSA1LjIyMTM4TDIxLjY2NTQgNS4yMjEzOFY2LjgxMDU2QzIwLjA0NDQgOC42NDM0MyAxOS40MTk0IDEwLjEzNzMgMTkuNDE5NCAxMi43NDM1SDE3LjQyNzZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K" alt="Badge"></a>
13
13
  </p>
14
14
 
15
- A tiny, zero-dependency fluent flow builder for TypeScript. Chain steps, branch on conditions, loop, batch-process, and run tasks in parallel — all through a single `FlowBuilder` class. Extend it with plugins for tool calling, ReAct agent loops, human-in-the-loop, memory, structured output, streaming, graph-based flow composition, eval, and more.
15
+ **Flowneer** is a tiny (~3 kB gzipped), zero-dependency TypeScript flow builder that gives you full control over deterministic, stateful LLM agents and workflows.
16
+
17
+ ### Why Flowneer?
18
+
19
+ - **Ultra-lightweight** — ~3 kB gzipped core, zero dependencies
20
+ - **Fluent & composable** — Chain steps with shared mutable state
21
+ - **Full control flow primitives** — `.startWith()`, `.then()`, `.branch()`, `.loop()`, `.parallel()`, `.batch()`, `.anchor()` jumps
22
+ - **Streaming-first** — Real-time `.stream()` with event/chunk yielding
23
+ - **Precise extensibility** — Subclass with `.extend([plugins])` and scope hooks/plugins exactly where needed (via `StepFilter` globs/predicates)
24
+ - **Production-ready patterns** — Built-in presets for ReAct, sequential crews, supervisor-workers, round-robin debate, refinement loops
25
+
26
+ ### Plugins unlock what you actually need
27
+
28
+ - Tool calling & registries
29
+ - ReAct / reasoning loops
30
+ - Memory (buffer, summary, KV)
31
+ - Human-in-the-loop interrupts
32
+ - Structured output parsing
33
+ - Rate limiting, retries, timeouts, tracing, eval, graph export/import
34
+
35
+ No forced abstractions. No monolith. Just a fast, deterministic builder that stays out of your way while giving you structured concurrency, cancellation, observability, and agentic power.
16
36
 
17
37
  > Flowneer is currently under heavy development with ongoing pattern exploration and architectural refinement. Breaking changes are expected frequently, potentially on a daily basis, as the core design is actively evolving.
18
38
 
@@ -23,6 +43,7 @@ bun add flowneer
23
43
  ```
24
44
 
25
45
  ## For LLM Agents
46
+
26
47
  [llms.txt](https://fanna1119.github.io/flowneer/llms.txt)
27
48
  [llms-full.txt](https://fanna1119.github.io/flowneer/llms-full.txt)
28
49
 
@@ -50,6 +71,8 @@ await new FlowBuilder<State>()
50
71
 
51
72
  Every step receives a **shared state object** (`s`) that you mutate directly. That's the whole data model.
52
73
 
74
+ ---
75
+
53
76
  ## API
54
77
 
55
78
  ### `startWith(fn, options?)`
@@ -65,12 +88,7 @@ Append a sequential step.
65
88
  Route to a named branch based on the return value of `router`.
66
89
 
67
90
  ```typescript
68
- interface AuthState {
69
- role: string;
70
- message: string;
71
- }
72
-
73
- await new FlowBuilder<AuthState>()
91
+ await new FlowBuilder<{ role: string; message: string }>()
74
92
  .startWith(async (s) => {
75
93
  s.role = "admin";
76
94
  })
@@ -84,7 +102,7 @@ await new FlowBuilder<AuthState>()
84
102
  })
85
103
  .then(async (s) => console.log(s.message))
86
104
  .run({ role: "", message: "" });
87
- // Welcome, admin!
105
+ // -> Welcome, admin!
88
106
  ```
89
107
 
90
108
  ### `loop(condition, body)`
@@ -92,11 +110,7 @@ await new FlowBuilder<AuthState>()
92
110
  Repeat a sub-flow while `condition` returns `true`.
93
111
 
94
112
  ```typescript
95
- interface TickState {
96
- ticks: number;
97
- }
98
-
99
- await new FlowBuilder<TickState>()
113
+ await new FlowBuilder<{ ticks: number }>()
100
114
  .startWith(async (s) => {
101
115
  s.ticks = 0;
102
116
  })
@@ -109,21 +123,19 @@ await new FlowBuilder<TickState>()
109
123
  )
110
124
  .then(async (s) => console.log("done, ticks =", s.ticks))
111
125
  .run({ ticks: 0 });
112
- // done, ticks = 3
126
+ // -> done, ticks = 3
113
127
  ```
114
128
 
115
129
  ### `batch(items, processor, options?)`
116
130
 
117
- Run a sub-flow once per item. The current item is written to `shared.__batchItem` by default.
131
+ Run a sub-flow once per item. The current item is written to `shared.__batchItem` by default. Pass a `{ key }` option to name the item slot — required for nested batches.
118
132
 
119
133
  ```typescript
120
- interface SumState {
134
+ await new FlowBuilder<{
121
135
  numbers: number[];
122
136
  results: number[];
123
137
  __batchItem?: number;
124
- }
125
-
126
- await new FlowBuilder<SumState>()
138
+ }>()
127
139
  .startWith(async (s) => {
128
140
  s.results = [];
129
141
  })
@@ -136,119 +148,33 @@ await new FlowBuilder<SumState>()
136
148
  )
137
149
  .then(async (s) => console.log(s.results))
138
150
  .run({ numbers: [1, 2, 3], results: [] });
139
- // [2, 4, 6]
140
- ```
141
-
142
- **Nested batches** — pass a `{ key }` option to give each level its own property name, so inner and outer items don't overwrite each other:
143
-
144
- ```typescript
145
- interface NestedState {
146
- groups: { name: string; members: string[] }[];
147
- results: string[];
148
- __group?: { name: string; members: string[] };
149
- __member?: string;
150
- }
151
-
152
- await new FlowBuilder<NestedState>()
153
- .batch(
154
- (s) => s.groups,
155
- (b) =>
156
- b
157
- .startWith((s) => {
158
- // s.__group is the current group
159
- })
160
- .batch(
161
- (s) => s.__group!.members,
162
- (inner) =>
163
- inner.startWith((s) => {
164
- // both s.__group and s.__member are accessible
165
- s.results.push(`${s.__group!.name}:${s.__member!}`);
166
- }),
167
- { key: "__member" },
168
- ),
169
- { key: "__group" },
170
- )
171
- .run({ groups: [{ name: "A", members: ["a1", "a2"] }], results: [] });
172
- // → results: ["A:a1", "A:a2"]
151
+ // -> [2, 4, 6]
173
152
  ```
174
153
 
175
154
  ### `parallel(fns, options?, reducer?)`
176
155
 
177
- Run multiple functions concurrently against the same shared state.
156
+ Run multiple functions concurrently against the same shared state. When a `reducer` is provided, each fn receives its own shallow clone and the reducer merges results back.
178
157
 
179
158
  ```typescript
180
- interface FetchState {
181
- posts?: any[];
182
- users?: any[];
183
- }
184
-
185
- await new FlowBuilder<FetchState>()
159
+ await new FlowBuilder<{ posts?: any[]; users?: any[] }>()
186
160
  .parallel([
187
161
  async (s) => {
188
- const res = await fetch("https://jsonplaceholder.typicode.com/posts");
189
- s.posts = await res.json();
162
+ s.posts = await fetch("/posts").then((r) => r.json());
190
163
  },
191
164
  async (s) => {
192
- const res = await fetch("https://jsonplaceholder.typicode.com/users");
193
- s.users = await res.json();
165
+ s.users = await fetch("/users").then((r) => r.json());
194
166
  },
195
167
  ])
196
- .then(async (s) => {
197
- console.log(
198
- "Fetched",
199
- s.posts?.length,
200
- "posts and",
201
- s.users?.length,
202
- "users",
203
- );
204
- })
168
+ .then(async (s) => console.log(s.posts?.length, s.users?.length))
205
169
  .run({});
206
- // → Fetched 100 posts and 10 users
207
170
  ```
208
171
 
209
- When a `reducer` is provided each fn receives its own **shallow clone** of `shared`, preventing concurrent write races. After all fns settle the reducer merges the drafts back into the original:
210
-
211
- ```typescript
212
- interface ScoreState {
213
- value: number;
214
- }
215
-
216
- await new FlowBuilder<ScoreState>()
217
- .parallel(
218
- [
219
- async (s) => {
220
- s.value += 10;
221
- },
222
- async (s) => {
223
- s.value += 20;
224
- },
225
- ],
226
- undefined,
227
- (original, drafts) => {
228
- // drafts[0].value === 10, drafts[1].value === 20 (each started at 0)
229
- original.value = drafts.reduce((sum, d) => sum + d.value, 0);
230
- },
231
- )
232
- .run({ value: 0 });
233
- // original.value === 30
234
- ```
235
-
236
- See [`withAtomicUpdates`](#withatomicupdates) for the plugin shorthand.
237
-
238
172
  ### `anchor(name)`
239
173
 
240
- Insert a named marker in the step chain. Anchors are no-ops during normal execution they exist only as jump targets.
241
-
242
- Any `NodeFn` can return `"#anchorName"` to jump back (or forward) to that anchor, enabling iterative refinement and reflection loops without nesting:
174
+ Insert a named marker in the step chain. Any `NodeFn` can return `"#anchorName"` to jump to that anchor, enabling iterative refinement loops without nesting.
243
175
 
244
176
  ```typescript
245
- interface RefineState {
246
- draft: string;
247
- passes: number;
248
- quality: number;
249
- }
250
-
251
- await new FlowBuilder<RefineState>()
177
+ await new FlowBuilder<{ draft: string; quality: number; passes: number }>()
252
178
  .startWith(async (s) => {
253
179
  s.draft = await generateDraft(s);
254
180
  })
@@ -258,31 +184,22 @@ await new FlowBuilder<RefineState>()
258
184
  if (s.quality < 0.8) {
259
185
  s.draft = await improveDraft(s.draft);
260
186
  s.passes++;
261
- return "#refine"; // jump back to the anchor
187
+ return "#refine";
262
188
  }
263
189
  })
264
190
  .then(async (s) => console.log("Final draft after", s.passes, "passes"))
265
- .run({ draft: "", passes: 0, quality: 0 });
191
+ .run({ draft: "", quality: 0, passes: 0 });
266
192
  ```
267
193
 
268
- > **Tip:** Pair with [`withCycles`](#withcycles) to cap the maximum number of jumps.
194
+ > Pair with [`withCycles`](#resilience) to cap the maximum number of jumps.
269
195
 
270
196
  ### `fragment()` and `.add(fragment)`
271
197
 
272
- Fragments are reusable partial flows — composable step chains that can be spliced into any `FlowBuilder`. Think of them like Zod partials for flows.
273
-
274
- Create a fragment with the `fragment()` factory, chain steps on it, then embed it with `.add()`:
198
+ Fragments are reusable partial flows that can be spliced into any `FlowBuilder`.
275
199
 
276
200
  ```typescript
277
201
  import { FlowBuilder, fragment } from "flowneer";
278
202
 
279
- interface State {
280
- input: string;
281
- enriched: boolean;
282
- summary: string;
283
- }
284
-
285
- // Define reusable fragments
286
203
  const enrich = fragment<State>()
287
204
  .then(async (s) => {
288
205
  s.enriched = true;
@@ -291,128 +208,47 @@ const enrich = fragment<State>()
291
208
  s.input = s.input.trim();
292
209
  });
293
210
 
294
- const summarise = fragment<State>().loop(
295
- (s) => !s.summary,
296
- (b) =>
297
- b.then(async (s) => {
298
- s.summary = s.input.slice(0, 10);
299
- }),
300
- );
301
-
302
- // Compose into a full flow
303
211
  await new FlowBuilder<State>()
304
212
  .startWith(async (s) => {
305
- s.input = " hello world ";
213
+ s.input = " hello ";
306
214
  })
307
- .add(enrich) // splices enrich's steps inline
308
- .add(summarise) // splices summarise's steps inline
309
- .then(async (s) => console.log(s.summary))
215
+ .add(enrich)
216
+ .then(async (s) => console.log(s.input))
310
217
  .run({ input: "", enriched: false, summary: "" });
311
- // → hello worl
312
218
  ```
313
219
 
314
- Fragments support all step types — `.then()`, `.loop()`, `.batch()`, `.branch()`, `.parallel()`, `.anchor()`. They **cannot** be run directly — calling `.run()` or `.stream()` on a fragment throws.
315
-
316
- The same fragment can be reused across multiple flows:
317
-
318
- ```typescript
319
- const validate = fragment<State>().then(checkInput).then(sanitize);
320
-
321
- const flowA = new FlowBuilder<State>().add(validate).then(handleA);
322
- const flowB = new FlowBuilder<State>().add(validate).then(handleB);
323
- ```
324
-
325
- ## using with `withCycles` plugin
326
-
327
- `withCycles` guards against infinite anchor-jump loops. Each call registers one limit; multiple calls stack.
328
-
329
- **Global limit** — throws after `n` total anchor jumps across the whole flow:
330
-
331
- ```typescript
332
- import { FlowBuilder } from "flowneer";
333
- import { withCycles } from "flowneer/plugins/resilience";
334
-
335
- FlowBuilder.use(withCycles);
336
-
337
- const flow = new FlowBuilder<State>()
338
- .withCycles(5) // max 5 total anchor jumps
339
- .startWith(async (s) => {
340
- s.count = 0;
341
- })
342
- .anchor("loop")
343
- .then(async (s) => {
344
- s.count += 1;
345
- if (s.count < 3) return "#loop"; // jump back to "loop" anchor
346
- })
347
- .then(async (s) => console.log("done, count =", s.count));
348
- ```
349
-
350
- **Per-anchor limit** — pass an anchor name as the second argument to restrict visits to that specific anchor only:
351
-
352
- ```typescript
353
- const flow = new FlowBuilder<State>()
354
- .withCycles(5, "refine") // max 5 visits to the "refine" anchor
355
- .startWith(generateDraft)
356
- .anchor("refine")
357
- .then(async (s) => {
358
- s.quality = await score(s.draft);
359
- if (s.quality < 0.8) {
360
- s.draft = await improve(s.draft);
361
- return "#refine";
362
- }
363
- });
364
- ```
365
-
366
- **Mixed** — combine a global cap with independent per-anchor limits by chaining calls. Each limit is evaluated independently:
367
-
368
- ```typescript
369
- const flow = new FlowBuilder<State>()
370
- .withCycles(100) // global: max 100 total anchor jumps
371
- .withCycles(5, "fast") // "fast" anchor: max 5 visits
372
- .withCycles(10, "retry") // "retry" anchor: max 10 visits
373
- ...
374
- ```
375
-
376
- Unlisted anchors are unaffected by per-anchor limits. The global limit (if set) still counts every jump regardless of which anchor was targeted.
220
+ Fragments support all step types. They cannot be run directly — calling `.run()` on a fragment throws.
377
221
 
378
222
  ### `run(shared, params?, options?)`
379
223
 
380
- Execute the flow. Optionally pass a `params` object that every step receives as a second argument.
224
+ Execute the flow. Optionally pass a `params` object that every step receives as a second argument, and an `AbortSignal` to cancel between steps.
381
225
 
382
226
  ```typescript
383
- // Basic
384
227
  await flow.run(shared);
385
-
386
- // With params
387
228
  await flow.run(shared, { userId: "123" });
388
229
 
389
- // With AbortSignal — cancels between steps when the signal fires
390
230
  const controller = new AbortController();
391
231
  await flow.run(shared, undefined, { signal: controller.signal });
392
232
  ```
393
233
 
394
234
  ### `stream(shared, params?, options?)`
395
235
 
396
- An async-generator alternative to `run()` that yields `StreamEvent` values as the flow executes. Useful for pushing incremental updates to a UI or SSE endpoint.
236
+ An async-generator alternative to `run()` that yields `StreamEvent` values as the flow executes.
397
237
 
398
238
  ```typescript
399
- import type { StreamEvent } from "flowneer";
400
-
401
239
  for await (const event of flow.stream(shared)) {
402
- if (event.type === "step:before") console.log("→ step", event.meta.index);
403
- if (event.type === "step:after") console.log("✓ step", event.meta.index);
240
+ if (event.type === "step:before") console.log("->", event.meta.index);
404
241
  if (event.type === "chunk") process.stdout.write(event.chunk as string);
405
- if (event.type === "error") console.error(event.error);
406
242
  if (event.type === "done") break;
407
243
  }
408
244
  ```
409
245
 
410
- Steps emit chunks by assigning to `shared.__stream`; each assignment yields a `"chunk"` event:
246
+ Steps emit chunks by assigning to `shared.__stream`:
411
247
 
412
248
  ```typescript
413
249
  .then(async (s) => {
414
250
  for await (const token of llmStream()) {
415
- s.__stream = token; // yields { type: "chunk", chunk: token, meta }
251
+ s.__stream = token; // -> yields { type: "chunk", chunk: token }
416
252
  }
417
253
  })
418
254
  ```
@@ -425,7 +261,7 @@ Steps emit chunks by assigning to `shared.__stream`; each assignment yields a `"
425
261
  | `error` | `meta`, `error` | When a step throws |
426
262
  | `done` | `shared` | After the flow finishes |
427
263
 
428
- ### Options
264
+ ### Step options
429
265
 
430
266
  Any step that accepts `options` supports:
431
267
 
@@ -435,6 +271,8 @@ Any step that accepts `options` supports:
435
271
  | `delaySec` | `0` | Seconds to wait between retries |
436
272
  | `timeoutMs` | `0` | Milliseconds before the step is aborted (0 = no limit) |
437
273
 
274
+ ---
275
+
438
276
  ## Error handling
439
277
 
440
278
  When a step throws, the error is wrapped in a `FlowError` with the step index and type:
@@ -457,147 +295,76 @@ try {
457
295
  }
458
296
  ```
459
297
 
460
- Errors inside `loop` and `batch` sub-flows are wrapped the same way:
461
-
462
- ```
463
- FlowError: Flow failed at loop (step 1): exploded on tick 2
464
- FlowError: Flow failed at batch (step 0): bad item: 3
465
- ```
466
-
467
- ### `InterruptError`
468
-
469
- `InterruptError` is a special error that **bypasses `FlowError` wrapping** — it propagates directly to the caller. Use it for human-in-the-loop and approval patterns (via [`withInterrupts`](#withinterrupts)).
470
-
471
- ```typescript
472
- import { FlowBuilder, InterruptError } from "flowneer";
298
+ `InterruptError` is a special error that bypasses `FlowError` wrapping and propagates directly to the caller. Use it for human-in-the-loop patterns via [`withInterrupts`](#observability) or [`withHumanNode`](#agent).
473
299
 
474
- try {
475
- await flow.run(shared);
476
- } catch (err) {
477
- if (err instanceof InterruptError) {
478
- // err.savedShared is a deep clone of state at the interrupt point
479
- const approval = await askHuman(err.savedShared);
480
- if (approval) await flow.run(shared); // resume from scratch or use withReplay
481
- }
482
- }
483
- ```
300
+ ---
484
301
 
485
302
  ## Plugins
486
303
 
487
- The core is intentionally small. Use `FlowBuilder.use(plugin)` to add chain methods.
488
-
489
- A plugin is an object of functions that get copied onto `FlowBuilder.prototype`. Each function receives the builder as `this` and should return `this` for chaining.
490
-
491
- ### Available plugins
492
-
493
- | Category | Plugin | Method | Description |
494
- | ----------------- | ------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
495
- | **Observability** | `withHistory` | `.withHistory()` | Appends a shallow state snapshot after each step to `shared.__history` |
496
- | | `withTiming` | `.withTiming()` | Records wall-clock duration (ms) of each step in `shared.__timings[index]` |
497
- | | `withVerbose` | `.withVerbose()` | Prints the full `shared` object to stdout after each step |
498
- | | `withInterrupts` | `.interruptIf(condition)` | Pauses the flow by throwing an `InterruptError` (with a deep-clone of `shared`) when condition is true |
499
- | | `withCallbacks` | `.withCallbacks(handlers)` | LangChain-style lifecycle callbacks dispatched by step label prefix (`llm:*`, `tool:*`, `agent:*`) |
500
- | **Persistence** | `withCheckpoint` | `.withCheckpoint(store)` | Saves `shared` to a store after each successful step |
501
- | | `withAuditLog` | `.withAuditLog(store)` | Writes an immutable deep-clone audit entry to a store after every step (success and error) |
502
- | | `withReplay` | `.withReplay(fromStep)` | Skips all steps before `fromStep`; combine with `.withCheckpoint()` to resume a failed flow |
503
- | | `withVersionedCheckpoint` | `.withVersionedCheckpoint(store)` | Saves diff-based versioned checkpoints with parent pointers after each step that changes state |
504
- | | | `.resumeFrom(version, store)` | Resolves a version id and skips all steps up to and including the saved step index |
505
- | **Resilience** | `withCircuitBreaker` | `.withCircuitBreaker(opts?)` | Opens the circuit after `maxFailures` consecutive failures and rejects all steps until `resetMs` elapses |
506
- | | `withFallback` | `.withFallback(fn)` | Catches any step error and calls `fn` instead of propagating, allowing the flow to continue |
507
- | | `withTimeout` | `.withTimeout(ms)` | Aborts any step that exceeds `ms` milliseconds with a descriptive error |
508
- | | `withCycles` | `.withCycles(n, anchor?)` | Throws after `n` anchor jumps globally, or after `n` visits to a named anchor — guards against infinite goto loops |
509
- | **Messaging** | `withChannels` | `.withChannels()` | Initialises a `Map`-based message-channel system on `shared.__channels` |
510
- | | `withStream` | `.withStream()` | Enables real-time chunk streaming via `shared.__stream` (see `.stream()`) |
511
- | **LLM** | `withCostTracker` | `.withCostTracker()` | Accumulates per-step `shared.__stepCost` values into `shared.__cost` after each step |
512
- | | `withRateLimit` | `.withRateLimit({ intervalMs })` | Enforces a minimum gap of `intervalMs` ms between steps to avoid hammering rate-limited APIs |
513
- | | `withTokenBudget` | `.withTokenBudget(limit)` | Aborts the flow before any step where `shared.tokensUsed >= limit` |
514
- | | `withStructuredOutput` | `.withStructuredOutput(opts)` | Parses and validates a step's LLM output (`shared.__llmOutput`) into a typed object via a Zod-compatible validator |
515
- | **Tools** | `withTools` | `.withTools(registry)` | Attaches a `ToolRegistry` to `shared.__tools`; call `registry.execute()` or helpers from any step |
516
- | **Agent** | `withReActLoop` | `.withReActLoop(opts)` | Built-in ReAct loop: think → tool-call → observe, with configurable `maxIterations` and `onObservation` |
517
- | | `withHumanNode` | `.humanNode(opts?)` | Inserts a human-in-the-loop pause; pair with `resumeFlow()` to continue after receiving input |
518
- | **Memory** | `withMemory` | `.withMemory(instance)` | Attaches a `Memory` instance to `shared.__memory`; choose `BufferWindowMemory`, `SummaryMemory`, or `KVMemory` |
519
- | **Graph** | `withGraph` | `.withGraph()` | Describe a flow as a DAG with `.addNode()` / `.addEdge()`, then `.compile()` to a `FlowBuilder` chain |
520
- | **Telemetry** | `withTelemetry` | `.withTelemetry(opts?)` | Structured span telemetry via `TelemetryDaemon`; accepts `consoleExporter`, `otlpExporter`, or a custom exporter |
521
- | **Dev** | `withDryRun` | `.withDryRun()` | Skips all step bodies while still firing hooks — useful for validating observability wiring |
522
- | | `withMocks` | `.withMocks(map)` | Replaces step bodies at specified indices with mock functions; all other steps run normally |
523
- | | `withStepLimit` | `.withStepLimit(max?)` | Throws after `max` total step executions (default 1000); counter resets on each `run()` call |
524
- | | `withAtomicUpdates` | `.parallelAtomic(fns, reducer, options?)` | Sugar over `parallel()` with a reducer — each fn runs on an isolated draft, reducer merges results |
525
-
526
- Plugins are imported from `flowneer/plugins` (or their individual subpath) and registered once with `FlowBuilder.use()`:
304
+ The core is intentionally small. Use `FlowBuilder.extend([...plugins])` to create a subclass with plugins mixed in. Unlike the removed `use()`, `extend()` never mutates the base class — each call returns an isolated subclass.
305
+
306
+ ### Using a plugin
527
307
 
528
308
  ```typescript
529
- import { withTiming, withCostTracker } from "flowneer/plugins";
309
+ import { FlowBuilder } from "flowneer";
310
+ import { withTiming } from "flowneer/plugins/observability";
311
+ import { withRateLimit } from "flowneer/plugins/llm";
312
+
313
+ const AppFlow = FlowBuilder.extend([withTiming, withRateLimit]);
530
314
 
531
- FlowBuilder.use(withTiming);
532
- FlowBuilder.use(withCostTracker);
315
+ const flow = new AppFlow<State>()
316
+ .withTiming()
317
+ .withRateLimit({ intervalMs: 500 })
318
+ .startWith(step1)
319
+ .then(step2);
533
320
  ```
534
321
 
535
- Messaging utilities are standalone functions no need to register them as a plugin method:
322
+ Chain `extend()` calls to layer plugins on top of a base subclass:
536
323
 
537
324
  ```typescript
538
- import {
539
- withChannels,
540
- sendTo,
541
- receiveFrom,
542
- peekChannel,
543
- } from "flowneer/plugins/messaging";
544
-
545
- FlowBuilder.use(withChannels);
546
-
547
- const flow = new FlowBuilder()
548
- .withChannels()
549
- .startWith(async (s) => {
550
- sendTo(s, "results", { score: 42 });
551
- })
552
- .then(async (s) => {
553
- const msgs = receiveFrom(s, "results"); // [{ score: 42 }]
554
- });
325
+ const BaseFlow = FlowBuilder.extend([withTiming]);
326
+ const TracedFlow = BaseFlow.extend([withTrace]); // has both plugins
555
327
  ```
556
328
 
557
- ---
558
-
559
329
  ### Writing a plugin
560
330
 
331
+ A plugin is an object of functions that get mixed onto `FlowBuilder.prototype`. Each function receives the builder as `this` and should return `this` for chaining.
332
+
561
333
  ```typescript
562
- import type { FlowBuilder, FlowneerPlugin, StepMeta } from "flowneer";
334
+ import type {
335
+ FlowBuilder,
336
+ FlowneerPlugin,
337
+ StepFilter,
338
+ StepMeta,
339
+ } from "flowneer";
563
340
 
564
- // 1. Augment the FlowBuilder interface for type safety
565
341
  declare module "flowneer" {
566
342
  interface FlowBuilder<S, P> {
567
- withTracing(fn: (meta: StepMeta, event: string) => void): this;
343
+ withTracing(
344
+ fn: (meta: StepMeta, event: string) => void,
345
+ filter?: StepFilter,
346
+ ): this;
568
347
  }
569
348
  }
570
349
 
571
- // 2. Implement the plugin
572
- export const observePlugin: FlowneerPlugin = {
573
- withTracing(this: FlowBuilder<any, any>, fn) {
574
- (this as any)._setHooks({
575
- beforeStep: (meta: StepMeta) => fn(meta, "before"),
576
- afterStep: (meta: StepMeta) => fn(meta, "after"),
577
- onError: (meta: StepMeta) => fn(meta, "error"),
578
- });
350
+ export const tracingPlugin: FlowneerPlugin = {
351
+ withTracing(this: FlowBuilder<any, any>, fn, filter?: StepFilter) {
352
+ (this as any)._setHooks(
353
+ {
354
+ beforeStep: (meta: StepMeta) => fn(meta, "before"),
355
+ afterStep: (meta: StepMeta) => fn(meta, "after"),
356
+ onError: (meta: StepMeta) => fn(meta, "error"),
357
+ },
358
+ filter,
359
+ );
579
360
  return this;
580
361
  },
581
362
  };
582
363
  ```
583
364
 
584
- ### Using a plugin
585
-
586
- ```typescript
587
- import { FlowBuilder } from "flowneer";
588
- import { observePlugin } from "./observePlugin";
589
-
590
- FlowBuilder.use(observePlugin); // one-time registration
591
-
592
- const flow = new FlowBuilder<MyState>()
593
- .withTracing((meta, event) => console.log(event, meta.type, meta.index))
594
- .startWith(step1)
595
- .then(step2);
596
- ```
597
-
598
365
  ### Lifecycle hooks
599
366
 
600
- Plugins register hooks via `_setHooks()`. The following hook points are available:
367
+ Plugins register hooks via `_setHooks()`. Multiple registrations of the same hook compose the first registered is the outermost.
601
368
 
602
369
  | Hook | Called | Arguments |
603
370
  | ---------------- | --------------------------------------------------------- | --------------------------------------- |
@@ -609,491 +376,186 @@ Plugins register hooks via `_setHooks()`. The following hook points are availabl
609
376
  | `onError` | When a step throws (before re-throwing) | `(meta, error, shared, params)` |
610
377
  | `afterFlow` | After the flow finishes (success or failure) | `(shared, params)` |
611
378
 
612
- Multiple `wrapStep` (or `wrapParallelFn`) registrations compose the first registered is the outermost wrapper. Omitting `next()` skips the step body entirely (used by `withDryRun`, `withMocks`, `withReplay`).
613
-
614
- ### What plugins are for
615
-
616
- | Concern | Plugin / hook | Hook(s) used |
617
- | ---------------------------- | --------------------------------- | ---------------------------------------- |
618
- | Observability / tracing | `withHistory`, `withTiming` | `beforeStep` + `afterStep` |
619
- | Lifecycle callbacks | `withCallbacks` | `beforeStep` + `afterStep` + `onError` |
620
- | Persistence / checkpointing | `withCheckpoint` | `afterStep` |
621
- | Versioned persistence | `withVersionedCheckpoint` | `beforeFlow` + `afterStep` |
622
- | Step/execution skip | `withDryRun`, `withReplay` | `wrapStep` |
623
- | Safe parallel isolation | `withAtomicUpdates` | `wrapParallelFn` (via core reducer) |
624
- | Human-in-the-loop / approval | `withInterrupts`, `withHumanNode` | `then()` + `InterruptError` |
625
- | Message passing | `withChannels` | `beforeFlow` |
626
- | Real-time streaming | `withStream` / `.stream()` | `afterStep` (chunk injection) |
627
- | Infinite-loop protection | `withCycles`, `withStepLimit` | `afterStep` / `beforeStep` |
628
- | Tool calling | `withTools` | `beforeFlow` |
629
- | Agent loops | `withReActLoop` | `then()` + `loop()` |
630
- | Memory management | `withMemory` | `beforeFlow` |
631
- | Structured output | `withStructuredOutput` | `afterStep` |
632
- | Graph-based composition | `withGraph` | DSL compiler (pre-run) |
633
- | Telemetry / spans | `withTelemetry` | `beforeStep` + `afterStep` + `afterFlow` |
634
- | Cleanup / teardown | custom | `afterFlow` |
635
-
636
- See [examples/observePlugin.ts](examples/observePlugin.ts) and [examples/persistPlugin.ts](examples/persistPlugin.ts) for complete implementations.
637
-
638
- ---
639
-
640
- ## Tool calling
379
+ Step-scoped hooks (`beforeStep`, `afterStep`, `onError`, `wrapStep`, `wrapParallelFn`) accept an optional [`StepFilter`](#stepfilter) as the second argument to `_setHooks()`. `beforeFlow` / `afterFlow` are unaffected. Unmatched `wrapStep`/`wrapParallelFn` hooks always call `next()` automatically so the middleware chain is never broken.
641
380
 
642
- Register typed tools and call them from any step:
381
+ ### `StepFilter`
643
382
 
644
383
  ```typescript
645
- import { withTools, ToolRegistry, executeTool } from "flowneer/plugins/tools";
646
- FlowBuilder.use(withTools);
647
-
648
- const tools = new ToolRegistry([
649
- {
650
- name: "search",
651
- description: "Search the web",
652
- params: { query: { type: "string", description: "Query", required: true } },
653
- execute: async ({ query }) => fetchSearchResults(query),
654
- },
655
- ]);
656
-
657
- const flow = new FlowBuilder<State>().withTools(tools).startWith(async (s) => {
658
- const result = await s.__tools.execute({
659
- name: "search",
660
- args: { query: s.question },
661
- });
662
- s.searchResult = result;
663
- });
384
+ type StepFilter = string[] | ((meta: StepMeta) => boolean);
664
385
  ```
665
386
 
666
- `ToolRegistry` exposes `get`, `has`, `names`, `definitions`, `execute`, and `executeAll`. The standalone helpers `getTools(s)`, `executeTool(s, call)`, and `executeTools(s, calls)` work without the plugin method.
667
-
668
- ---
669
-
670
- ## ReAct agent loop
671
-
672
- `.withReActLoop` inserts a wired think → tool-call → observe loop. Your `think` function receives the current state (including `shared.__toolResults` from the previous round) and returns either a finish action or tool calls:
387
+ - **String array** matches steps by `label`. Supports `*` as a glob wildcard (`"llm:*"` matches `"llm:summarise"`, `"llm:embed"`, ). Steps without a label are never matched.
388
+ - **Predicate** — return `true` to match. Use this for runtime conditions or multi-criteria logic.
673
389
 
674
390
  ```typescript
675
- import { withReActLoop } from "flowneer/plugins/agent";
676
- FlowBuilder.use(withReActLoop);
677
-
678
- const flow = new FlowBuilder<State>().withTools(tools).withReActLoop({
679
- maxIterations: 8,
680
- think: async (s) => {
681
- const res = await llm(s.messages);
682
- return res.toolCalls.length
683
- ? { action: "tool", calls: res.toolCalls }
684
- : { action: "finish", output: res.text };
685
- },
686
- onObservation: (results, s) => {
687
- s.messages.push({ role: "tool", content: JSON.stringify(results) });
688
- },
689
- });
690
-
691
- // After run: s.__reactOutput holds the final answer
692
- // s.__reactExhausted === true if maxIterations was reached
693
- ```
694
-
695
- ---
696
-
697
- ## Human-in-the-loop with `humanNode`
391
+ // Array form with glob
392
+ flow.addHooks({ beforeStep: log }, ["llm:*", "embed:*"]);
698
393
 
699
- `.humanNode()` is a higher-level alternative to `interruptIf`. The `resumeFlow` helper merges human edits back into the saved state and re-runs:
700
-
701
- ```typescript
702
- import { withHumanNode, resumeFlow } from "flowneer/plugins/agent";
703
- FlowBuilder.use(withHumanNode);
704
-
705
- const flow = new FlowBuilder<DraftState>()
706
- .startWith(generateDraft)
707
- .humanNode({ prompt: "Please review the draft." })
708
- .then(publishDraft);
709
-
710
- try {
711
- await flow.run(state);
712
- } catch (e) {
713
- if (e instanceof InterruptError) {
714
- const feedback = await showReviewUI(e.savedShared);
715
- await resumeFlow(flow, e.savedShared, { feedback });
716
- }
717
- }
718
- ```
719
-
720
- ---
721
-
722
- ## Multi-agent patterns
723
-
724
- Four factory functions compose flows into common multi-agent topologies:
725
-
726
- ```typescript
727
- import {
728
- supervisorCrew,
729
- sequentialCrew,
730
- hierarchicalCrew,
731
- roundRobinDebate,
732
- } from "flowneer/plugins/agent";
733
-
734
- // Supervisor → parallel workers → optional aggregator
735
- const crew = supervisorCrew<State>(
736
- (s) => {
737
- s.plan = makePlan(s);
738
- },
739
- [researchAgent, codeAgent, reviewAgent],
740
- {
741
- post: (s) => {
742
- s.report = compile(s);
743
- },
744
- },
394
+ // Predicate form
395
+ flow.addHooks(
396
+ { beforeStep: log },
397
+ (meta) => meta.label?.startsWith("llm:") ?? false,
745
398
  );
746
- await crew.run(state);
747
-
748
- // Round-robin debate across agents for N rounds
749
- const debate = roundRobinDebate<State>([agentA, agentB, agentC], 3);
750
- await debate.run(state);
751
399
  ```
752
400
 
753
- All factory functions return a plain `FlowBuilder` and compose with every other plugin.
401
+ `addHooks(hooks, filter?)` returns a `dispose()` function to remove the hooks.
754
402
 
755
403
  ---
756
404
 
757
- ## Memory
405
+ ## Available plugins
758
406
 
759
- Three memory classes let you manage conversation history. All implement the same `Memory` interface (`add / get / clear / toContext`):
407
+ All plugins are imported from `flowneer/plugins` or their individual subpath (e.g. `flowneer/plugins/resilience`).
760
408
 
761
- ```typescript
762
- import {
763
- BufferWindowMemory,
764
- SummaryMemory,
765
- KVMemory,
766
- withMemory,
767
- } from "flowneer/plugins/memory";
768
- FlowBuilder.use(withMemory);
769
-
770
- const memory = new BufferWindowMemory({ maxMessages: 20 });
771
-
772
- const flow = new FlowBuilder<State>()
773
- .withMemory(memory) // attaches to shared.__memory
774
- .startWith(async (s) => {
775
- s.__memory.add({ role: "user", content: s.userInput });
776
- const history = s.__memory.toContext();
777
- s.response = await llm(history);
778
- s.__memory.add({ role: "assistant", content: s.response });
779
- });
780
- ```
409
+ ### Observability
781
410
 
782
- | Class | Behaviour |
783
- | -------------------- | ----------------------------------------------------------------- |
784
- | `BufferWindowMemory` | Keeps the last `maxMessages` messages (sliding window) |
785
- | `SummaryMemory` | Compresses oldest messages via a user-supplied `summarize()` fn |
786
- | `KVMemory` | Key-value store; supports `toJSON()` / `fromJSON()` serialisation |
411
+ | Plugin | Method | Description |
412
+ | ---------------- | -------------------------- | -------------------------------------------------------------------------------------------------- |
413
+ | `withHistory` | `.withHistory()` | Appends a shallow state snapshot after each step to `shared.__history` |
414
+ | `withTiming` | `.withTiming()` | Records wall-clock duration (ms) of each step in `shared.__timings[index]` |
415
+ | `withVerbose` | `.withVerbose()` | Prints the full `shared` object to stdout after each step |
416
+ | `withInterrupts` | `.interruptIf(condition)` | Throws an `InterruptError` (with a deep-clone of `shared`) when `condition` is true |
417
+ | `withCallbacks` | `.withCallbacks(handlers)` | LangChain-style lifecycle callbacks dispatched by step label prefix (`llm:*`, `tool:*`, `agent:*`) |
787
418
 
788
- ---
419
+ ### Persistence
789
420
 
790
- ## Output parsers
421
+ | Plugin | Method | Description |
422
+ | ------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------- |
423
+ | `withCheckpoint` | `.withCheckpoint(store)` | Saves `shared` to a store after each successful step |
424
+ | `withAuditLog` | `.withAuditLog(store)` | Writes an immutable deep-clone audit entry to a store after every step (success and error) |
425
+ | `withReplay` | `.withReplay(fromStep)` | Skips all steps before `fromStep`; combine with `.withCheckpoint()` to resume a failed flow |
426
+ | `withVersionedCheckpoint` | `.withVersionedCheckpoint(store)` | Diff-based versioned checkpoints with parent pointers; use `.resumeFrom(version, store)` to restore |
791
427
 
792
- Four pure functions parse structured data from LLM text. No plugin registration needed:
428
+ ### Resilience
793
429
 
794
- ```typescript
795
- import {
796
- parseJsonOutput,
797
- parseListOutput,
798
- parseMarkdownTable,
799
- parseRegexOutput,
800
- } from "flowneer/plugins/output";
801
-
802
- const obj = parseJsonOutput(llmText); // raw JSON, fenced, or embedded in prose
803
- const items = parseListOutput(llmText); // dash, *, •, numbered, or newline-separated
804
- const rows = parseMarkdownTable(llmText); // GFM table → Record<string,string>[]
805
- const match = parseRegexOutput(llmText, /(?<id>\d+)/); // named or positional capture groups
806
- ```
430
+ | Plugin | Method | Description |
431
+ | -------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------- |
432
+ | `withCircuitBreaker` | `.withCircuitBreaker(opts?)` | Opens the circuit after `maxFailures` consecutive failures and rejects all steps until `resetMs` elapses |
433
+ | `withFallback` | `.withFallback(fn)` | Catches any step error and calls `fn` instead of propagating |
434
+ | `withTimeout` | `.withTimeout(ms)` | Aborts any step that exceeds `ms` milliseconds |
435
+ | `withCycles` | `.withCycles(n, anchor?)` | Throws after `n` anchor jumps globally, or after `n` visits to a named anchor — guards against infinite loops |
807
436
 
808
- All parsers accept an optional `Validator<T>` (Zod-compatible) as the last argument.
437
+ ### Messaging
809
438
 
810
- ---
439
+ | Plugin | Method | Description |
440
+ | -------------- | ----------------- | ------------------------------------------------------------------------------------------------------- |
441
+ | `withChannels` | `.withChannels()` | `Map`-based message-channel system on `shared.__channels`; use `sendTo` / `receiveFrom` / `peekChannel` |
442
+ | `withStream` | `.withStream()` | Enables real-time chunk streaming via `shared.__stream` |
811
443
 
812
- ## Structured output
444
+ ### LLM
813
445
 
814
- Validate LLM output against a schema after a step runs. The plugin reads `shared.__llmOutput`, runs the optional `parse` function (e.g. `JSON.parse`), then passes the result through `validator.parse()`:
446
+ | Plugin | Method | Description |
447
+ | ---------------------- | -------------------------------- | ------------------------------------------------------------------------ |
448
+ | `withCostTracker` | `.withCostTracker()` | Accumulates per-step `shared.__stepCost` values into `shared.__cost` |
449
+ | `withRateLimit` | `.withRateLimit({ intervalMs })` | Enforces a minimum gap of `intervalMs` ms between steps |
450
+ | `withTokenBudget` | `.withTokenBudget(limit)` | Aborts the flow before any step where `shared.tokensUsed >= limit` |
451
+ | `withStructuredOutput` | `.withStructuredOutput(opts)` | Parses and validates `shared.__llmOutput` via a Zod-compatible validator |
815
452
 
816
- ```typescript
817
- import { withStructuredOutput } from "flowneer/plugins/llm";
818
- FlowBuilder.use(withStructuredOutput);
453
+ ### Tools
819
454
 
820
- const flow = new FlowBuilder<State>()
821
- .withStructuredOutput({ parse: JSON.parse, validator: myZodSchema })
822
- .startWith(callLlm); // step must write to shared.__llmOutput
455
+ | Plugin | Method | Description |
456
+ | ----------- | ---------------------- | ----------------------------------------------------------------------------------------- |
457
+ | `withTools` | `.withTools(registry)` | Attaches a `ToolRegistry` to `shared.__tools`; use `executeTool` / `executeTools` helpers |
823
458
 
824
- // s.__structuredOutput — parsed & validated result
825
- // s.__validationError — set if parsing or validation failed
826
- ```
459
+ ### Agent
827
460
 
828
- ---
461
+ | Plugin | Method | Description |
462
+ | --------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- |
463
+ | `withReActLoop` | `.withReActLoop(opts)` | Built-in ReAct loop: think -> tool-call -> observe, with configurable `maxIterations` and `onObservation` |
464
+ | `withHumanNode` | `.humanNode(opts?)` | Inserts a human-in-the-loop pause; pair with `resumeFlow()` to continue after receiving input |
829
465
 
830
- ## Eval harness
466
+ ### Memory
831
467
 
832
- Run a flow against a labelled dataset and collect per-item scores:
468
+ | Plugin | Method | Description |
469
+ | ------------ | ----------------------- | -------------------------------------------------------------------------------------------------------------- |
470
+ | `withMemory` | `.withMemory(instance)` | Attaches a `Memory` instance to `shared.__memory`; choose `BufferWindowMemory`, `SummaryMemory`, or `KVMemory` |
833
471
 
834
- ```typescript
835
- import { runEvalSuite, exactMatch, f1Score } from "flowneer/plugins/eval";
836
-
837
- const { results, summary } = await runEvalSuite(
838
- [{ question: "What is 2+2?", expected: "4" }, ...],
839
- myFlow,
840
- {
841
- accuracy: (item, s) => exactMatch(s.answer, item.expected),
842
- f1: (item, s) => f1Score(s.answer, item.expected),
843
- },
844
- );
472
+ ### Output
845
473
 
846
- console.log(summary.accuracy.mean, summary.f1.mean);
847
- ```
474
+ Pure parsing helpers — no plugin registration needed. Import from `flowneer/plugins/output`.
848
475
 
849
- Available scorers: `exactMatch`, `containsMatch`, `f1Score`, `retrievalPrecision`, `retrievalRecall`, `answerRelevance`. Each dataset item runs in a deep-cloned state — no bleed between items. Errors are captured per-item rather than aborting the suite.
476
+ | Function | Description |
477
+ | -------------------- | ------------------------------------------------------------- |
478
+ | `parseJsonOutput` | Parse raw JSON, fenced, or embedded JSON from LLM text |
479
+ | `parseListOutput` | Parse dash, `*`, bullet, numbered, or newline-separated lists |
480
+ | `parseMarkdownTable` | Parse GFM tables to `Record<string, string>[]` |
481
+ | `parseRegexOutput` | Extract named or positional regex capture groups |
850
482
 
851
- ---
483
+ ### Eval
852
484
 
853
- ## Graph-based flow composition
485
+ | Export | Description |
486
+ | ---------------------------------------- | ----------------------------------------------------------------- |
487
+ | `runEvalSuite` | Run a flow against a labelled dataset and collect per-item scores |
488
+ | `exactMatch` | Scorer: exact string equality |
489
+ | `containsMatch` | Scorer: substring containment |
490
+ | `f1Score` | Scorer: token-level F1 |
491
+ | `retrievalPrecision` / `retrievalRecall` | Scorer: retrieval quality metrics |
492
+ | `answerRelevance` | Scorer: relevance signal |
854
493
 
855
- Describe a flow as a directed graph and let Flowneer compile the execution order:
494
+ ### Graph
856
495
 
857
- ```typescript
858
- import { withGraph } from "flowneer/plugins/graph";
859
- FlowBuilder.use(withGraph);
496
+ | Plugin | Method | Description |
497
+ | ----------- | -------------- | ------------------------------------------------------------------------------------------------------------ |
498
+ | `withGraph` | `.withGraph()` | Describe a flow as a DAG with `.addNode()` / `.addEdge()`, then `.compile()` to a ready-to-run `FlowBuilder` |
860
499
 
861
- const flow = (new FlowBuilder<State>() as any)
862
- .withGraph()
863
- .addNode("fetch", (s) => {
864
- s.data = fetch(s.url);
865
- })
866
- .addNode("parse", (s) => {
867
- s.parsed = parse(s.data);
868
- })
869
- .addNode("validate", (s) => {
870
- s.valid = validate(s.parsed);
871
- })
872
- .addNode("retry", (s) => {
873
- s.url = nextUrl(s);
874
- })
875
- .addEdge("fetch", "parse")
876
- .addEdge("parse", "validate")
877
- .addEdge("validate", "retry", (s) => !s.valid) // conditional back-edge → loop
878
- .addEdge("retry", "fetch")
879
- .compile(); // returns a ready-to-run FlowBuilder
880
-
881
- await flow.run({ url: "https://..." });
882
- ```
883
-
884
- `compile()` runs Kahn's topological sort on unconditional edges, classifies conditional edges as forward jumps or back-edges, inserts `anchor` markers for back-edge targets, and emits the matching `FlowBuilder` chain. Throws descriptively on empty graphs, duplicate node names, unknown edge targets, or unconditional cycles.
885
-
886
- ---
887
-
888
- ## AI agent example
889
-
890
- Flowneer's primitives map directly to common agent patterns:
891
-
892
- ```typescript
893
- import { FlowBuilder } from "flowneer";
894
-
895
- interface AgentState {
896
- question: string;
897
- history: Message[];
898
- intent?: string;
899
- answer?: string;
900
- }
901
-
902
- const agent = new FlowBuilder<AgentState>()
903
- .startWith(classifyIntent)
904
- .branch((s) => s.intent, {
905
- weather: fetchWeather,
906
- joke: tellJoke,
907
- default: generalAnswer,
908
- })
909
- .then(formatAndRespond);
910
-
911
- await agent.run({ question: "What's the weather in Paris?", history: [] });
912
- ```
913
-
914
- A ReAct-style loop:
915
-
916
- ```typescript
917
- const reactAgent = new FlowBuilder<AgentState>()
918
- .startWith(think)
919
- .loop(
920
- (s) => !s.done,
921
- (b) =>
922
- b
923
- .startWith(selectTool)
924
- .branch(routeTool, {
925
- search: webSearch,
926
- code: runCode,
927
- default: respond,
928
- })
929
- .then(observe),
930
- )
931
- .then(formatOutput);
932
- ```
933
-
934
- See [examples/assistantFlow.ts](examples/assistantFlow.ts) for a full interactive agent.
935
-
936
- ### Agent-to-agent delegation
937
-
938
- There is no special primitive for sub-agents — just call `anotherFlow.run(shared)` inside a `then`. Since `shared` is passed by reference, the sub-agent reads and writes the same state seamlessly:
939
-
940
- ```typescript
941
- const researchAgent = new FlowBuilder<ReportState>()
942
- .startWith(searchWeb)
943
- .then(summariseSources);
500
+ ### Telemetry
944
501
 
945
- const writeAgent = new FlowBuilder<ReportState>()
946
- .startWith(draftReport)
947
- .then(formatMarkdown);
502
+ | Plugin | Method | Description |
503
+ | --------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------- |
504
+ | `withTelemetry` | `.withTelemetry(opts?)` | Structured span telemetry via `TelemetryDaemon`; accepts `consoleExporter`, `otlpExporter`, or a custom exporter |
948
505
 
949
- const orchestrator = new FlowBuilder<ReportState>()
950
- .startWith(async (s) => {
951
- s.query = "LLM benchmarks 2025";
952
- })
953
- .then(async (s) => researchAgent.run(s)) // delegate → sub-agent mutates s
954
- .then(async (s) => writeAgent.run(s)) // delegate → sub-agent mutates s
955
- .then(async (s) => console.log(s.report));
956
- ```
957
-
958
- Any number of flows can be composed this way. Each sub-agent is itself a `FlowBuilder`, so it can have its own retries, branches, and plugins.
959
-
960
- ### Parallel sub-agents
961
-
962
- Use `parallel` when sub-agents are independent and can run concurrently:
963
-
964
- ```typescript
965
- const sentimentAgent = new FlowBuilder<AnalysisState>()
966
- .startWith(classifySentiment)
967
- .then(scoreSentiment);
506
+ ### Dev
968
507
 
969
- const summaryAgent = new FlowBuilder<AnalysisState>()
970
- .startWith(extractKeyPoints)
971
- .then(writeSummary);
508
+ | Plugin | Method | Description |
509
+ | ------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------- |
510
+ | `withDryRun` | `.withDryRun()` | Skips all step bodies while still firing hooks — useful for validating observability wiring |
511
+ | `withMocks` | `.withMocks(map)` | Replaces step bodies at specified indices with mock functions |
512
+ | `withStepLimit` | `.withStepLimit(max?)` | Throws after `max` total step executions (default 1000) |
513
+ | `withAtomicUpdates` | `.parallelAtomic(fns, reducer, options?)` | Sugar over `parallel()` with a reducer — each fn runs on an isolated draft |
972
514
 
973
- const toxicityAgent = new FlowBuilder<AnalysisState>().startWith(checkToxicity);
974
-
975
- const orchestrator = new FlowBuilder<AnalysisState>()
976
- .startWith(async (s) => {
977
- s.text = "...input text...";
978
- })
979
- .parallel([
980
- (s) => sentimentAgent.run(s), // writes s.sentiment
981
- (s) => summaryAgent.run(s), // writes s.summary
982
- (s) => toxicityAgent.run(s), // writes s.toxicity
983
- ])
984
- .then(async (s) => {
985
- console.log(s.sentiment, s.summary, s.toxicity);
986
- });
987
- ```
515
+ ---
988
516
 
989
- All three sub-agents share the same `shared` object and run concurrently. Avoid writing to the same key from parallel sub-agents — writes are not synchronised.
517
+ ## Presets
990
518
 
991
- To eliminate race conditions entirely, pass a `reducer` as the third argument to `.parallel()`, or use `.parallelAtomic()` from [`withAtomicUpdates`](#withatomicupdates). Each sub-agent then operates on its own isolated draft and the reducer decides how to merge.
519
+ Presets are ready-made `FlowBuilder` factories for common patterns. Import from `flowneer/presets` or their individual subpath.
992
520
 
993
- ### Iterative refinement with `anchor` + goto
521
+ ### Agent presets (`flowneer/presets/agent`)
994
522
 
995
- Use `anchor` / `#anchor` return values for reflection loops that don't need nesting:
523
+ | Preset | Description |
524
+ | -------------------- | ---------------------------------------------------------------------------------- |
525
+ | `createAgent` | LangChain-style factory — wire up tools and an LLM adapter to get a runnable agent |
526
+ | `withReActLoop` | ReAct think -> tool-call -> observe loop with configurable max iterations |
527
+ | `supervisorCrew` | Supervisor dispatches to parallel worker agents, with an optional aggregator step |
528
+ | `sequentialCrew` | Agents run in sequence, each receiving the output of the previous |
529
+ | `hierarchicalCrew` | Tree-structured multi-agent delegation |
530
+ | `roundRobinDebate` | Agents take turns responding for N rounds |
531
+ | `planAndExecute` | Planner LLM produces a step-by-step plan; executor LLM carries out each step |
532
+ | `reflexionAgent` | Generate -> critique -> revise loop (Reflexion paper) |
533
+ | `critiqueAndRevise` | Two-agent generate -> critique -> revise loop |
534
+ | `evaluatorOptimizer` | DSPy-style generate -> evaluate -> improve loop |
535
+ | `selfConsistency` | Parallel sampling + majority-vote aggregation |
536
+ | `tool` | Minimal tool-calling agent helper |
996
537
 
997
- ```typescript
998
- const reactAgent = new FlowBuilder<AgentState>()
999
- .startWith(think)
1000
- .anchor("act")
1001
- .then(async (s) => {
1002
- const result = await callTool(s.toolCall);
1003
- s.observations.push(result);
1004
- s.done = await shouldStop(s);
1005
- if (!s.done) return "#act";
1006
- })
1007
- .then(formatOutput);
1008
- ```
538
+ ### Pipeline presets (`flowneer/presets/pipeline`)
1009
539
 
1010
- ### Human-in-the-loop with `interruptIf`
540
+ | Preset | Description |
541
+ | -------------------- | ------------------------------------------------------------------------ |
542
+ | `generateUntilValid` | Generate -> validate -> retry with error context until output passes |
543
+ | `mapReduceLlm` | Batch LLM calls across N items, then reduce results into a single output |
1011
544
 
1012
- ```typescript
1013
- import { withInterrupts, InterruptError } from "flowneer/plugins/observability";
1014
- FlowBuilder.use(withInterrupts);
545
+ ### RAG presets (`flowneer/presets/rag`)
1015
546
 
1016
- const flow = new FlowBuilder<DraftState>()
1017
- .startWith(generateDraft)
1018
- .interruptIf((s) => s.requiresApproval) // pauses here
1019
- .then(publishDraft);
547
+ | Preset | Description |
548
+ | -------------- | --------------------------------------------------------- |
549
+ | `ragPipeline` | Standard retrieve -> augment -> generate pipeline |
550
+ | `iterativeRag` | RAG with follow-up retrieval loop for multi-hop questions |
1020
551
 
1021
- try {
1022
- await flow.run(state);
1023
- } catch (err) {
1024
- if (err instanceof InterruptError) {
1025
- // err.savedShared holds state at the pause point
1026
- await showReviewUI(err.savedShared);
1027
- }
1028
- }
1029
- ```
552
+ ### Config presets (`flowneer/presets/config`)
1030
553
 
1031
- ## Project structure
554
+ | Preset | Description |
555
+ | ------- | ---------------------------------------------------------------- |
556
+ | `build` | Compile a `FlowConfig` JSON/object into a runnable `FlowBuilder` |
1032
557
 
1033
- ```
1034
- Flowneer.ts Core — FlowBuilder, FlowError, InterruptError, Validator, StreamEvent, types
1035
- index.ts Public exports
1036
- plugins/
1037
- observability/
1038
- withHistory.ts State snapshot history
1039
- withTiming.ts Per-step wall-clock timing
1040
- withVerbose.ts Stdout logging
1041
- withInterrupts.ts Human-in-the-loop / approval gates
1042
- withCallbacks.ts LangChain-style lifecycle callbacks (llm:/tool:/agent: prefixes)
1043
- persistence/
1044
- withCheckpoint.ts Post-step state saves
1045
- withAuditLog.ts Immutable audit trail
1046
- withReplay.ts Skip-to-step for crash recovery
1047
- withVersionedCheckpoint.ts Diff-based versioned saves + resumeFrom
1048
- resilience/
1049
- withCircuitBreaker.ts
1050
- withFallback.ts
1051
- withTimeout.ts
1052
- withCycles.ts Guard against infinite goto loops
1053
- llm/
1054
- withCostTracker.ts
1055
- withRateLimit.ts
1056
- withTokenBudget.ts
1057
- withStructuredOutput.ts Parse + validate LLM output via Zod-compatible validator
1058
- messaging/
1059
- withChannels.ts Map-based message channels (sendTo / receiveFrom)
1060
- withStream.ts Real-time chunk streaming via shared.__stream
1061
- tools/
1062
- withTools.ts ToolRegistry + withTools plugin + helper functions
1063
- agent/
1064
- withReActLoop.ts Built-in ReAct think → tool-call → observe loop
1065
- withHumanNode.ts humanNode() pause + resumeFlow() helper
1066
- patterns.ts supervisorCrew / sequentialCrew / hierarchicalCrew / roundRobinDebate
1067
- memory/
1068
- types.ts Memory interface + MemoryMessage type
1069
- bufferWindowMemory.ts Sliding-window conversation memory
1070
- summaryMemory.ts Auto-summarising memory (user-supplied summarize fn)
1071
- kvMemory.ts Key-value memory with JSON serialisation
1072
- withMemory.ts Plugin that attaches memory to shared.__memory
1073
- output/
1074
- parseJson.ts Parse raw / fenced / embedded JSON from LLM output
1075
- parseList.ts Parse dash / numbered / bullet / newline-separated lists
1076
- parseTable.ts Parse GFM markdown tables to Record<string,string>[]
1077
- parseRegex.ts Extract named or positional regex capture groups
1078
- eval/
1079
- index.ts Scoring functions + runEvalSuite
1080
- graph/
1081
- index.ts withGraph plugin — DAG compiler (addNode / addEdge / compile)
1082
- telemetry/
1083
- telemetry.ts TelemetryDaemon, consoleExporter, otlpExporter
1084
- index.ts withTelemetry plugin wrapper
1085
- dev/
1086
- withDryRun.ts
1087
- withMocks.ts
1088
- withStepLimit.ts Cap total step executions
1089
- withAtomicUpdates.ts parallelAtomic() shorthand
1090
- examples/
1091
- assistantFlow.ts Interactive LLM assistant with branching
1092
- observePlugin.ts Tracing plugin example
1093
- persistPlugin.ts Checkpoint plugin example
1094
- clawneer.ts Full ReAct agent with tool calling
1095
- streamingServer.ts SSE streaming server example
1096
- ```
558
+ ---
1097
559
 
1098
560
  ## License
1099
561