flowneer 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +349 -16
- package/dist/FlowBuilder-YJyCqXMa.d.ts +187 -0
- package/dist/index.d.ts +2 -179
- package/dist/index.js +97 -30
- package/dist/plugins/agent/index.d.ts +195 -0
- package/dist/plugins/agent/index.js +573 -0
- package/dist/plugins/dev/index.d.ts +1 -1
- package/dist/plugins/eval/index.d.ts +86 -0
- package/dist/plugins/eval/index.js +95 -0
- package/dist/plugins/graph/index.d.ts +47 -0
- package/dist/plugins/graph/index.js +136 -0
- package/dist/plugins/index.d.ts +8 -1
- package/dist/plugins/index.js +1276 -13
- package/dist/plugins/llm/index.d.ts +56 -2
- package/dist/plugins/llm/index.js +35 -0
- package/dist/plugins/memory/index.d.ts +133 -0
- package/dist/plugins/memory/index.js +162 -0
- package/dist/plugins/messaging/index.d.ts +1 -1
- package/dist/plugins/observability/index.d.ts +41 -2
- package/dist/plugins/observability/index.js +54 -1
- package/dist/plugins/output/index.d.ts +82 -0
- package/dist/plugins/output/index.js +97 -0
- package/dist/plugins/persistence/index.d.ts +1 -1
- package/dist/plugins/resilience/index.d.ts +1 -1
- package/dist/plugins/telemetry/index.d.ts +71 -0
- package/dist/plugins/telemetry/index.js +114 -0
- package/dist/plugins/tools/index.d.ts +99 -0
- package/dist/plugins/tools/index.js +113 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +411 -0
- package/package.json +12 -5
package/README.md
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
</div>
|
|
4
4
|
|
|
5
5
|
<p>
|
|
6
|
-
<a href="https://www.npmjs.com/package/flowneer"><img src="https://
|
|
6
|
+
<a href="https://www.npmjs.com/package/flowneer"><img src="https://img.shields.io/npm/v/flowneer" /></a>
|
|
7
7
|
<a href="https://deno.bundlejs.com/badge?q=flowneer"><img src="https://deno.bundlejs.com/badge?q=flowneer" /></a>
|
|
8
|
-
<a href="https://www.npmjs.com/package/flowneer"><img src="https://
|
|
9
|
-
<a href="https://www.npmjs.com/package/flowneer"><img src="https://
|
|
8
|
+
<a href="https://www.npmjs.com/package/flowneer"><img src="https://img.shields.io/npm/l/flowneer" /></a>
|
|
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
|
</p>
|
|
12
12
|
|
|
13
|
-
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.
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
>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.
|
|
14
17
|
|
|
15
18
|
## Install
|
|
16
19
|
|
|
@@ -328,6 +331,40 @@ const controller = new AbortController();
|
|
|
328
331
|
await flow.run(shared, undefined, { signal: controller.signal });
|
|
329
332
|
```
|
|
330
333
|
|
|
334
|
+
### `stream(shared, params?, options?)`
|
|
335
|
+
|
|
336
|
+
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.
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import type { StreamEvent } from "flowneer";
|
|
340
|
+
|
|
341
|
+
for await (const event of flow.stream(shared)) {
|
|
342
|
+
if (event.type === "step:before") console.log("→ step", event.meta.index);
|
|
343
|
+
if (event.type === "step:after") console.log("✓ step", event.meta.index);
|
|
344
|
+
if (event.type === "chunk") process.stdout.write(event.chunk as string);
|
|
345
|
+
if (event.type === "error") console.error(event.error);
|
|
346
|
+
if (event.type === "done") break;
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Steps emit chunks by assigning to `shared.__stream`; each assignment yields a `"chunk"` event:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
.then(async (s) => {
|
|
354
|
+
for await (const token of llmStream()) {
|
|
355
|
+
s.__stream = token; // → yields { type: "chunk", chunk: token, meta }
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
| Event type | Extra fields | When emitted |
|
|
361
|
+
| ------------- | ---------------- | --------------------------------------- |
|
|
362
|
+
| `step:before` | `meta` | Before each step |
|
|
363
|
+
| `step:after` | `meta`, `shared` | After each step completes |
|
|
364
|
+
| `chunk` | `meta`, `chunk` | When a step writes to `shared.__stream` |
|
|
365
|
+
| `error` | `meta`, `error` | When a step throws |
|
|
366
|
+
| `done` | `shared` | After the flow finishes |
|
|
367
|
+
|
|
331
368
|
### Options
|
|
332
369
|
|
|
333
370
|
Any step that accepts `options` supports:
|
|
@@ -399,6 +436,7 @@ A plugin is an object of functions that get copied onto `FlowBuilder.prototype`.
|
|
|
399
436
|
| | `withTiming` | `.withTiming()` | Records wall-clock duration (ms) of each step in `shared.__timings[index]` |
|
|
400
437
|
| | `withVerbose` | `.withVerbose()` | Prints the full `shared` object to stdout after each step |
|
|
401
438
|
| | `withInterrupts` | `.interruptIf(condition)` | Pauses the flow by throwing an `InterruptError` (with a deep-clone of `shared`) when condition is true |
|
|
439
|
+
| | `withCallbacks` | `.withCallbacks(handlers)` | LangChain-style lifecycle callbacks dispatched by step label prefix (`llm:*`, `tool:*`, `agent:*`) |
|
|
402
440
|
| **Persistence** | `withCheckpoint` | `.withCheckpoint(store)` | Saves `shared` to a store after each successful step |
|
|
403
441
|
| | `withAuditLog` | `.withAuditLog(store)` | Writes an immutable deep-clone audit entry to a store after every step (success and error) |
|
|
404
442
|
| | `withReplay` | `.withReplay(fromStep)` | Skips all steps before `fromStep`; combine with `.withCheckpoint()` to resume a failed flow |
|
|
@@ -409,9 +447,17 @@ A plugin is an object of functions that get copied onto `FlowBuilder.prototype`.
|
|
|
409
447
|
| | `withTimeout` | `.withTimeout(ms)` | Aborts any step that exceeds `ms` milliseconds with a descriptive error |
|
|
410
448
|
| | `withCycles` | `.withCycles(n, anchor?)` | Throws after `n` anchor jumps globally, or after `n` visits to a named anchor — guards against infinite goto loops |
|
|
411
449
|
| **Messaging** | `withChannels` | `.withChannels()` | Initialises a `Map`-based message-channel system on `shared.__channels` |
|
|
450
|
+
| | `withStream` | `.withStream()` | Enables real-time chunk streaming via `shared.__stream` (see `.stream()`) |
|
|
412
451
|
| **LLM** | `withCostTracker` | `.withCostTracker()` | Accumulates per-step `shared.__stepCost` values into `shared.__cost` after each step |
|
|
413
452
|
| | `withRateLimit` | `.withRateLimit({ intervalMs })` | Enforces a minimum gap of `intervalMs` ms between steps to avoid hammering rate-limited APIs |
|
|
414
453
|
| | `withTokenBudget` | `.withTokenBudget(limit)` | Aborts the flow before any step where `shared.tokensUsed >= limit` |
|
|
454
|
+
| | `withStructuredOutput` | `.withStructuredOutput(opts)` | Parses and validates a step's LLM output (`shared.__llmOutput`) into a typed object via a Zod-compatible validator |
|
|
455
|
+
| **Tools** | `withTools` | `.withTools(registry)` | Attaches a `ToolRegistry` to `shared.__tools`; call `registry.execute()` or helpers from any step |
|
|
456
|
+
| **Agent** | `withReActLoop` | `.withReActLoop(opts)` | Built-in ReAct loop: think → tool-call → observe, with configurable `maxIterations` and `onObservation` |
|
|
457
|
+
| | `withHumanNode` | `.humanNode(opts?)` | Inserts a human-in-the-loop pause; pair with `resumeFlow()` to continue after receiving input |
|
|
458
|
+
| **Memory** | `withMemory` | `.withMemory(instance)` | Attaches a `Memory` instance to `shared.__memory`; choose `BufferWindowMemory`, `SummaryMemory`, or `KVMemory` |
|
|
459
|
+
| **Graph** | `withGraph` | `.withGraph()` | Describe a flow as a DAG with `.addNode()` / `.addEdge()`, then `.compile()` to a `FlowBuilder` chain |
|
|
460
|
+
| **Telemetry** | `withTelemetry` | `.withTelemetry(opts?)` | Structured span telemetry via `TelemetryDaemon`; accepts `consoleExporter`, `otlpExporter`, or a custom exporter |
|
|
415
461
|
| **Dev** | `withDryRun` | `.withDryRun()` | Skips all step bodies while still firing hooks — useful for validating observability wiring |
|
|
416
462
|
| | `withMocks` | `.withMocks(map)` | Replaces step bodies at specified indices with mock functions; all other steps run normally |
|
|
417
463
|
| | `withStepLimit` | `.withStepLimit(max?)` | Throws after `max` total step executions (default 1000); counter resets on each `run()` call |
|
|
@@ -507,20 +553,278 @@ Multiple `wrapStep` (or `wrapParallelFn`) registrations compose — the first re
|
|
|
507
553
|
|
|
508
554
|
### What plugins are for
|
|
509
555
|
|
|
510
|
-
| Concern | Plugin / hook
|
|
511
|
-
| ---------------------------- |
|
|
512
|
-
| Observability / tracing | `withHistory`, `withTiming`
|
|
513
|
-
|
|
|
514
|
-
|
|
|
515
|
-
|
|
|
516
|
-
|
|
|
517
|
-
|
|
|
518
|
-
|
|
|
519
|
-
|
|
|
520
|
-
|
|
|
556
|
+
| Concern | Plugin / hook | Hook(s) used |
|
|
557
|
+
| ---------------------------- | --------------------------------- | ---------------------------------------- |
|
|
558
|
+
| Observability / tracing | `withHistory`, `withTiming` | `beforeStep` + `afterStep` |
|
|
559
|
+
| Lifecycle callbacks | `withCallbacks` | `beforeStep` + `afterStep` + `onError` |
|
|
560
|
+
| Persistence / checkpointing | `withCheckpoint` | `afterStep` |
|
|
561
|
+
| Versioned persistence | `withVersionedCheckpoint` | `beforeFlow` + `afterStep` |
|
|
562
|
+
| Step/execution skip | `withDryRun`, `withReplay` | `wrapStep` |
|
|
563
|
+
| Safe parallel isolation | `withAtomicUpdates` | `wrapParallelFn` (via core reducer) |
|
|
564
|
+
| Human-in-the-loop / approval | `withInterrupts`, `withHumanNode` | `then()` + `InterruptError` |
|
|
565
|
+
| Message passing | `withChannels` | `beforeFlow` |
|
|
566
|
+
| Real-time streaming | `withStream` / `.stream()` | `afterStep` (chunk injection) |
|
|
567
|
+
| Infinite-loop protection | `withCycles`, `withStepLimit` | `afterStep` / `beforeStep` |
|
|
568
|
+
| Tool calling | `withTools` | `beforeFlow` |
|
|
569
|
+
| Agent loops | `withReActLoop` | `then()` + `loop()` |
|
|
570
|
+
| Memory management | `withMemory` | `beforeFlow` |
|
|
571
|
+
| Structured output | `withStructuredOutput` | `afterStep` |
|
|
572
|
+
| Graph-based composition | `withGraph` | DSL compiler (pre-run) |
|
|
573
|
+
| Telemetry / spans | `withTelemetry` | `beforeStep` + `afterStep` + `afterFlow` |
|
|
574
|
+
| Cleanup / teardown | custom | `afterFlow` |
|
|
521
575
|
|
|
522
576
|
See [examples/observePlugin.ts](examples/observePlugin.ts) and [examples/persistPlugin.ts](examples/persistPlugin.ts) for complete implementations.
|
|
523
577
|
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## Tool calling
|
|
581
|
+
|
|
582
|
+
Register typed tools and call them from any step:
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
import { withTools, ToolRegistry, executeTool } from "flowneer/plugins/tools";
|
|
586
|
+
FlowBuilder.use(withTools);
|
|
587
|
+
|
|
588
|
+
const tools = new ToolRegistry([
|
|
589
|
+
{
|
|
590
|
+
name: "search",
|
|
591
|
+
description: "Search the web",
|
|
592
|
+
params: { query: { type: "string", description: "Query", required: true } },
|
|
593
|
+
execute: async ({ query }) => fetchSearchResults(query),
|
|
594
|
+
},
|
|
595
|
+
]);
|
|
596
|
+
|
|
597
|
+
const flow = new FlowBuilder<State>().withTools(tools).startWith(async (s) => {
|
|
598
|
+
const result = await s.__tools.execute({
|
|
599
|
+
name: "search",
|
|
600
|
+
args: { query: s.question },
|
|
601
|
+
});
|
|
602
|
+
s.searchResult = result;
|
|
603
|
+
});
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
`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.
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
## ReAct agent loop
|
|
611
|
+
|
|
612
|
+
`.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:
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
import { withReActLoop } from "flowneer/plugins/agent";
|
|
616
|
+
FlowBuilder.use(withReActLoop);
|
|
617
|
+
|
|
618
|
+
const flow = new FlowBuilder<State>().withTools(tools).withReActLoop({
|
|
619
|
+
maxIterations: 8,
|
|
620
|
+
think: async (s) => {
|
|
621
|
+
const res = await llm(s.messages);
|
|
622
|
+
return res.toolCalls.length
|
|
623
|
+
? { action: "tool", calls: res.toolCalls }
|
|
624
|
+
: { action: "finish", output: res.text };
|
|
625
|
+
},
|
|
626
|
+
onObservation: (results, s) => {
|
|
627
|
+
s.messages.push({ role: "tool", content: JSON.stringify(results) });
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// After run: s.__reactOutput holds the final answer
|
|
632
|
+
// s.__reactExhausted === true if maxIterations was reached
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## Human-in-the-loop with `humanNode`
|
|
638
|
+
|
|
639
|
+
`.humanNode()` is a higher-level alternative to `interruptIf`. The `resumeFlow` helper merges human edits back into the saved state and re-runs:
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
import { withHumanNode, resumeFlow } from "flowneer/plugins/agent";
|
|
643
|
+
FlowBuilder.use(withHumanNode);
|
|
644
|
+
|
|
645
|
+
const flow = new FlowBuilder<DraftState>()
|
|
646
|
+
.startWith(generateDraft)
|
|
647
|
+
.humanNode({ prompt: "Please review the draft." })
|
|
648
|
+
.then(publishDraft);
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
await flow.run(state);
|
|
652
|
+
} catch (e) {
|
|
653
|
+
if (e instanceof InterruptError) {
|
|
654
|
+
const feedback = await showReviewUI(e.savedShared);
|
|
655
|
+
await resumeFlow(flow, e.savedShared, { feedback });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
---
|
|
661
|
+
|
|
662
|
+
## Multi-agent patterns
|
|
663
|
+
|
|
664
|
+
Four factory functions compose flows into common multi-agent topologies:
|
|
665
|
+
|
|
666
|
+
```typescript
|
|
667
|
+
import {
|
|
668
|
+
supervisorCrew,
|
|
669
|
+
sequentialCrew,
|
|
670
|
+
hierarchicalCrew,
|
|
671
|
+
roundRobinDebate,
|
|
672
|
+
} from "flowneer/plugins/agent";
|
|
673
|
+
|
|
674
|
+
// Supervisor → parallel workers → optional aggregator
|
|
675
|
+
const crew = supervisorCrew<State>(
|
|
676
|
+
(s) => {
|
|
677
|
+
s.plan = makePlan(s);
|
|
678
|
+
},
|
|
679
|
+
[researchAgent, codeAgent, reviewAgent],
|
|
680
|
+
{
|
|
681
|
+
post: (s) => {
|
|
682
|
+
s.report = compile(s);
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
);
|
|
686
|
+
await crew.run(state);
|
|
687
|
+
|
|
688
|
+
// Round-robin debate across agents for N rounds
|
|
689
|
+
const debate = roundRobinDebate<State>([agentA, agentB, agentC], 3);
|
|
690
|
+
await debate.run(state);
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
All factory functions return a plain `FlowBuilder` and compose with every other plugin.
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
697
|
+
## Memory
|
|
698
|
+
|
|
699
|
+
Three memory classes let you manage conversation history. All implement the same `Memory` interface (`add / get / clear / toContext`):
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
import {
|
|
703
|
+
BufferWindowMemory,
|
|
704
|
+
SummaryMemory,
|
|
705
|
+
KVMemory,
|
|
706
|
+
withMemory,
|
|
707
|
+
} from "flowneer/plugins/memory";
|
|
708
|
+
FlowBuilder.use(withMemory);
|
|
709
|
+
|
|
710
|
+
const memory = new BufferWindowMemory({ maxMessages: 20 });
|
|
711
|
+
|
|
712
|
+
const flow = new FlowBuilder<State>()
|
|
713
|
+
.withMemory(memory) // attaches to shared.__memory
|
|
714
|
+
.startWith(async (s) => {
|
|
715
|
+
s.__memory.add({ role: "user", content: s.userInput });
|
|
716
|
+
const history = s.__memory.toContext();
|
|
717
|
+
s.response = await llm(history);
|
|
718
|
+
s.__memory.add({ role: "assistant", content: s.response });
|
|
719
|
+
});
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
| Class | Behaviour |
|
|
723
|
+
| -------------------- | ----------------------------------------------------------------- |
|
|
724
|
+
| `BufferWindowMemory` | Keeps the last `maxMessages` messages (sliding window) |
|
|
725
|
+
| `SummaryMemory` | Compresses oldest messages via a user-supplied `summarize()` fn |
|
|
726
|
+
| `KVMemory` | Key-value store; supports `toJSON()` / `fromJSON()` serialisation |
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## Output parsers
|
|
731
|
+
|
|
732
|
+
Four pure functions parse structured data from LLM text. No plugin registration needed:
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
import {
|
|
736
|
+
parseJsonOutput,
|
|
737
|
+
parseListOutput,
|
|
738
|
+
parseMarkdownTable,
|
|
739
|
+
parseRegexOutput,
|
|
740
|
+
} from "flowneer/plugins/output";
|
|
741
|
+
|
|
742
|
+
const obj = parseJsonOutput(llmText); // raw JSON, fenced, or embedded in prose
|
|
743
|
+
const items = parseListOutput(llmText); // dash, *, •, numbered, or newline-separated
|
|
744
|
+
const rows = parseMarkdownTable(llmText); // GFM table → Record<string,string>[]
|
|
745
|
+
const match = parseRegexOutput(llmText, /(?<id>\d+)/); // named or positional capture groups
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
All parsers accept an optional `Validator<T>` (Zod-compatible) as the last argument.
|
|
749
|
+
|
|
750
|
+
---
|
|
751
|
+
|
|
752
|
+
## Structured output
|
|
753
|
+
|
|
754
|
+
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()`:
|
|
755
|
+
|
|
756
|
+
```typescript
|
|
757
|
+
import { withStructuredOutput } from "flowneer/plugins/llm";
|
|
758
|
+
FlowBuilder.use(withStructuredOutput);
|
|
759
|
+
|
|
760
|
+
const flow = new FlowBuilder<State>()
|
|
761
|
+
.withStructuredOutput({ parse: JSON.parse, validator: myZodSchema })
|
|
762
|
+
.startWith(callLlm); // step must write to shared.__llmOutput
|
|
763
|
+
|
|
764
|
+
// s.__structuredOutput — parsed & validated result
|
|
765
|
+
// s.__validationError — set if parsing or validation failed
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
770
|
+
## Eval harness
|
|
771
|
+
|
|
772
|
+
Run a flow against a labelled dataset and collect per-item scores:
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
import { runEvalSuite, exactMatch, f1Score } from "flowneer/plugins/eval";
|
|
776
|
+
|
|
777
|
+
const { results, summary } = await runEvalSuite(
|
|
778
|
+
[{ question: "What is 2+2?", expected: "4" }, ...],
|
|
779
|
+
myFlow,
|
|
780
|
+
{
|
|
781
|
+
accuracy: (item, s) => exactMatch(s.answer, item.expected),
|
|
782
|
+
f1: (item, s) => f1Score(s.answer, item.expected),
|
|
783
|
+
},
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
console.log(summary.accuracy.mean, summary.f1.mean);
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
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.
|
|
790
|
+
|
|
791
|
+
---
|
|
792
|
+
|
|
793
|
+
## Graph-based flow composition
|
|
794
|
+
|
|
795
|
+
Describe a flow as a directed graph and let Flowneer compile the execution order:
|
|
796
|
+
|
|
797
|
+
```typescript
|
|
798
|
+
import { withGraph } from "flowneer/plugins/graph";
|
|
799
|
+
FlowBuilder.use(withGraph);
|
|
800
|
+
|
|
801
|
+
const flow = (new FlowBuilder<State>() as any)
|
|
802
|
+
.withGraph()
|
|
803
|
+
.addNode("fetch", (s) => {
|
|
804
|
+
s.data = fetch(s.url);
|
|
805
|
+
})
|
|
806
|
+
.addNode("parse", (s) => {
|
|
807
|
+
s.parsed = parse(s.data);
|
|
808
|
+
})
|
|
809
|
+
.addNode("validate", (s) => {
|
|
810
|
+
s.valid = validate(s.parsed);
|
|
811
|
+
})
|
|
812
|
+
.addNode("retry", (s) => {
|
|
813
|
+
s.url = nextUrl(s);
|
|
814
|
+
})
|
|
815
|
+
.addEdge("fetch", "parse")
|
|
816
|
+
.addEdge("parse", "validate")
|
|
817
|
+
.addEdge("validate", "retry", (s) => !s.valid) // conditional back-edge → loop
|
|
818
|
+
.addEdge("retry", "fetch")
|
|
819
|
+
.compile(); // returns a ready-to-run FlowBuilder
|
|
820
|
+
|
|
821
|
+
await flow.run({ url: "https://..." });
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
`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.
|
|
825
|
+
|
|
826
|
+
---
|
|
827
|
+
|
|
524
828
|
## AI agent example
|
|
525
829
|
|
|
526
830
|
Flowneer's primitives map directly to common agent patterns:
|
|
@@ -667,7 +971,7 @@ try {
|
|
|
667
971
|
## Project structure
|
|
668
972
|
|
|
669
973
|
```
|
|
670
|
-
Flowneer.ts Core — FlowBuilder, FlowError, InterruptError, types
|
|
974
|
+
Flowneer.ts Core — FlowBuilder, FlowError, InterruptError, Validator, StreamEvent, types
|
|
671
975
|
index.ts Public exports
|
|
672
976
|
plugins/
|
|
673
977
|
observability/
|
|
@@ -675,6 +979,7 @@ plugins/
|
|
|
675
979
|
withTiming.ts Per-step wall-clock timing
|
|
676
980
|
withVerbose.ts Stdout logging
|
|
677
981
|
withInterrupts.ts Human-in-the-loop / approval gates
|
|
982
|
+
withCallbacks.ts LangChain-style lifecycle callbacks (llm:/tool:/agent: prefixes)
|
|
678
983
|
persistence/
|
|
679
984
|
withCheckpoint.ts Post-step state saves
|
|
680
985
|
withAuditLog.ts Immutable audit trail
|
|
@@ -689,8 +994,34 @@ plugins/
|
|
|
689
994
|
withCostTracker.ts
|
|
690
995
|
withRateLimit.ts
|
|
691
996
|
withTokenBudget.ts
|
|
997
|
+
withStructuredOutput.ts Parse + validate LLM output via Zod-compatible validator
|
|
692
998
|
messaging/
|
|
693
999
|
withChannels.ts Map-based message channels (sendTo / receiveFrom)
|
|
1000
|
+
withStream.ts Real-time chunk streaming via shared.__stream
|
|
1001
|
+
tools/
|
|
1002
|
+
withTools.ts ToolRegistry + withTools plugin + helper functions
|
|
1003
|
+
agent/
|
|
1004
|
+
withReActLoop.ts Built-in ReAct think → tool-call → observe loop
|
|
1005
|
+
withHumanNode.ts humanNode() pause + resumeFlow() helper
|
|
1006
|
+
patterns.ts supervisorCrew / sequentialCrew / hierarchicalCrew / roundRobinDebate
|
|
1007
|
+
memory/
|
|
1008
|
+
types.ts Memory interface + MemoryMessage type
|
|
1009
|
+
bufferWindowMemory.ts Sliding-window conversation memory
|
|
1010
|
+
summaryMemory.ts Auto-summarising memory (user-supplied summarize fn)
|
|
1011
|
+
kvMemory.ts Key-value memory with JSON serialisation
|
|
1012
|
+
withMemory.ts Plugin that attaches memory to shared.__memory
|
|
1013
|
+
output/
|
|
1014
|
+
parseJson.ts Parse raw / fenced / embedded JSON from LLM output
|
|
1015
|
+
parseList.ts Parse dash / numbered / bullet / newline-separated lists
|
|
1016
|
+
parseTable.ts Parse GFM markdown tables to Record<string,string>[]
|
|
1017
|
+
parseRegex.ts Extract named or positional regex capture groups
|
|
1018
|
+
eval/
|
|
1019
|
+
index.ts Scoring functions + runEvalSuite
|
|
1020
|
+
graph/
|
|
1021
|
+
index.ts withGraph plugin — DAG compiler (addNode / addEdge / compile)
|
|
1022
|
+
telemetry/
|
|
1023
|
+
telemetry.ts TelemetryDaemon, consoleExporter, otlpExporter
|
|
1024
|
+
index.ts withTelemetry plugin wrapper
|
|
694
1025
|
dev/
|
|
695
1026
|
withDryRun.ts
|
|
696
1027
|
withMocks.ts
|
|
@@ -700,6 +1031,8 @@ examples/
|
|
|
700
1031
|
assistantFlow.ts Interactive LLM assistant with branching
|
|
701
1032
|
observePlugin.ts Tracing plugin example
|
|
702
1033
|
persistPlugin.ts Checkpoint plugin example
|
|
1034
|
+
clawneer.ts Full ReAct agent with tool calling
|
|
1035
|
+
streamingServer.ts SSE streaming server example
|
|
703
1036
|
```
|
|
704
1037
|
|
|
705
1038
|
## License
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic validator interface — structurally compatible with Zod, ArkType,
|
|
3
|
+
* Valibot, or any custom implementation that exposes `.parse(input)`.
|
|
4
|
+
* Used by `withStructuredOutput` and output parsers.
|
|
5
|
+
*/
|
|
6
|
+
interface Validator<T = unknown> {
|
|
7
|
+
parse(input: unknown): T;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Events yielded by `FlowBuilder.stream()`.
|
|
11
|
+
*/
|
|
12
|
+
type StreamEvent<S = any> = {
|
|
13
|
+
type: "step:before";
|
|
14
|
+
meta: StepMeta;
|
|
15
|
+
} | {
|
|
16
|
+
type: "step:after";
|
|
17
|
+
meta: StepMeta;
|
|
18
|
+
shared: S;
|
|
19
|
+
} | {
|
|
20
|
+
type: "chunk";
|
|
21
|
+
data: unknown;
|
|
22
|
+
} | {
|
|
23
|
+
type: "error";
|
|
24
|
+
error: unknown;
|
|
25
|
+
} | {
|
|
26
|
+
type: "done";
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Function signature for all step logic.
|
|
30
|
+
* Return an action string to route, or undefined/void to continue.
|
|
31
|
+
*
|
|
32
|
+
* Steps may also be declared as `async function*` generators — each `yield`
|
|
33
|
+
* forwards its value to the active stream consumer as a `chunk` event.
|
|
34
|
+
* The generator's final `return` value is still routed normally, so
|
|
35
|
+
* `return "#anchorName"` works exactly as it does in plain steps.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* .then(async function* (s) {
|
|
39
|
+
* for await (const token of llmStream(s.prompt)) {
|
|
40
|
+
* s.response += token;
|
|
41
|
+
* yield token; // → chunk event on flow.stream()
|
|
42
|
+
* }
|
|
43
|
+
* })
|
|
44
|
+
*/
|
|
45
|
+
type NodeFn<S = any, P extends Record<string, unknown> = Record<string, unknown>> = (shared: S, params: P) => Promise<string | undefined | void> | string | undefined | void | AsyncGenerator<unknown, string | undefined | void, unknown>;
|
|
46
|
+
/**
|
|
47
|
+
* A numeric value or a function that computes it from the current shared state
|
|
48
|
+
* and params. Use functions for dynamic per-item behaviour, e.g.
|
|
49
|
+
* `retries: (s) => (s.__batchItem % 3 === 0 ? 3 : 1)`.
|
|
50
|
+
*/
|
|
51
|
+
type NumberOrFn<S = any, P extends Record<string, unknown> = Record<string, unknown>> = number | ((shared: S, params: P) => number);
|
|
52
|
+
interface NodeOptions<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
|
|
53
|
+
retries?: NumberOrFn<S, P>;
|
|
54
|
+
delaySec?: NumberOrFn<S, P>;
|
|
55
|
+
timeoutMs?: NumberOrFn<S, P>;
|
|
56
|
+
}
|
|
57
|
+
interface RunOptions {
|
|
58
|
+
signal?: AbortSignal;
|
|
59
|
+
}
|
|
60
|
+
/** Metadata exposed to hooks — intentionally minimal to avoid coupling. */
|
|
61
|
+
interface StepMeta {
|
|
62
|
+
index: number;
|
|
63
|
+
type: "fn" | "branch" | "loop" | "batch" | "parallel" | "anchor";
|
|
64
|
+
label?: string;
|
|
65
|
+
}
|
|
66
|
+
/** Lifecycle hooks that plugins can register. */
|
|
67
|
+
interface FlowHooks<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
|
|
68
|
+
/** Fires once before the first step runs. */
|
|
69
|
+
beforeFlow?: (shared: S, params: P) => void | Promise<void>;
|
|
70
|
+
beforeStep?: (meta: StepMeta, shared: S, params: P) => void | Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Wraps step execution — call `next()` to invoke the step body.
|
|
73
|
+
* Omitting `next()` skips execution (dry-run, mock, etc.).
|
|
74
|
+
* Multiple `wrapStep` registrations are composed innermost-first.
|
|
75
|
+
*/
|
|
76
|
+
wrapStep?: (meta: StepMeta, next: () => Promise<void>, shared: S, params: P) => Promise<void>;
|
|
77
|
+
afterStep?: (meta: StepMeta, shared: S, params: P) => void | Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Wraps individual functions within a `.parallel()` step.
|
|
80
|
+
* `fnIndex` is the position within the fns array.
|
|
81
|
+
*/
|
|
82
|
+
wrapParallelFn?: (meta: StepMeta, fnIndex: number, next: () => Promise<void>, shared: S, params: P) => Promise<void>;
|
|
83
|
+
onError?: (meta: StepMeta, error: unknown, shared: S, params: P) => void;
|
|
84
|
+
afterFlow?: (shared: S, params: P) => void | Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A plugin is an object whose keys become methods on `FlowBuilder.prototype`.
|
|
88
|
+
* Each method receives the builder as `this` and should return `this` for chaining.
|
|
89
|
+
*
|
|
90
|
+
* Use declaration merging to get type-safe access:
|
|
91
|
+
* ```ts
|
|
92
|
+
* declare module "flowneer" {
|
|
93
|
+
* interface FlowBuilder<S, P> { withTracing(fn: TraceCallback): this; }
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
type FlowneerPlugin = Record<string, (this: FlowBuilder<any, any>, ...args: any[]) => any>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Fluent builder for composable flows.
|
|
101
|
+
*
|
|
102
|
+
* Steps execute sequentially in the order added. Call `.run(shared)` to execute.
|
|
103
|
+
*
|
|
104
|
+
* **Shared-state safety**: all steps operate on the same shared object.
|
|
105
|
+
* Mutate it directly; avoid spreading/replacing the entire object.
|
|
106
|
+
*/
|
|
107
|
+
declare class FlowBuilder<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
|
|
108
|
+
private steps;
|
|
109
|
+
private _hooksList;
|
|
110
|
+
/** Cached flat arrays of present hooks — invalidated whenever a hook is added. */
|
|
111
|
+
private _cachedHooks;
|
|
112
|
+
private _getHooks;
|
|
113
|
+
/** Register a plugin — copies its methods onto `FlowBuilder.prototype`. */
|
|
114
|
+
static use(plugin: FlowneerPlugin): void;
|
|
115
|
+
/** Register lifecycle hooks (called by plugin methods, not by consumers). */
|
|
116
|
+
protected _setHooks(hooks: Partial<FlowHooks<S, P>>): void;
|
|
117
|
+
/** Set the first step, resetting any prior chain. */
|
|
118
|
+
startWith(fn: NodeFn<S, P>, options?: NodeOptions<S, P>): this;
|
|
119
|
+
/** Append a sequential step. */
|
|
120
|
+
then(fn: NodeFn<S, P>, options?: NodeOptions<S, P>): this;
|
|
121
|
+
/**
|
|
122
|
+
* Append a routing step.
|
|
123
|
+
* `router` returns a key; the matching branch flow executes, then the chain continues.
|
|
124
|
+
*/
|
|
125
|
+
branch(router: NodeFn<S, P>, branches: Record<string, NodeFn<S, P>>, options?: NodeOptions<S, P>): this;
|
|
126
|
+
/**
|
|
127
|
+
* Append a looping step.
|
|
128
|
+
* Repeatedly runs `body` while `condition` returns true.
|
|
129
|
+
*/
|
|
130
|
+
loop(condition: (shared: S, params: P) => Promise<boolean> | boolean, body: (b: FlowBuilder<S, P>) => void): this;
|
|
131
|
+
/**
|
|
132
|
+
* Append a batch step.
|
|
133
|
+
* Runs `processor` once per item extracted by `items`, setting
|
|
134
|
+
* `shared[key]` each time (defaults to `"__batchItem"`).
|
|
135
|
+
*
|
|
136
|
+
* Use a unique `key` when nesting batches so each level has its own
|
|
137
|
+
* namespace:
|
|
138
|
+
* ```ts
|
|
139
|
+
* .batch(s => s.users, b => b
|
|
140
|
+
* .startWith(s => { console.log(s.__batch_user); })
|
|
141
|
+
* .batch(s => s.__batch_user.posts, p => p
|
|
142
|
+
* .startWith(s => { console.log(s.__batch_post); })
|
|
143
|
+
* , { key: '__batch_post' })
|
|
144
|
+
* , { key: '__batch_user' })
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
batch(items: (shared: S, params: P) => Promise<any[]> | any[], processor: (b: FlowBuilder<S, P>) => void, options?: {
|
|
148
|
+
key?: string;
|
|
149
|
+
}): this;
|
|
150
|
+
/**
|
|
151
|
+
* Append a parallel step.
|
|
152
|
+
* Runs all `fns` concurrently against the same shared state.
|
|
153
|
+
*
|
|
154
|
+
* When `reducer` is provided each fn receives its own shallow clone of
|
|
155
|
+
* `shared`; after all fns complete the reducer merges the drafts back
|
|
156
|
+
* into the original shared object — preventing concurrent mutation races.
|
|
157
|
+
*/
|
|
158
|
+
parallel(fns: NodeFn<S, P>[], options?: NodeOptions<S, P>, reducer?: (shared: S, drafts: S[]) => void): this;
|
|
159
|
+
/**
|
|
160
|
+
* Insert a named anchor. Anchors are no-op markers that can be jumped to
|
|
161
|
+
* from any `NodeFn` by returning `"#anchorName"`.
|
|
162
|
+
*/
|
|
163
|
+
anchor(name: string): this;
|
|
164
|
+
/** Execute the flow. */
|
|
165
|
+
run(shared: S, params?: P, options?: RunOptions): Promise<void>;
|
|
166
|
+
/**
|
|
167
|
+
* Execute the flow and yield `StreamEvent`s as an async generator.
|
|
168
|
+
*
|
|
169
|
+
* Events include `step:before`, `step:after`, `chunk` (from `emit()`),
|
|
170
|
+
* `error`, and `done`. This is an additive API — `.run()` is unchanged.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* for await (const event of flow.stream(shared)) {
|
|
174
|
+
* if (event.type === "chunk") process.stdout.write(event.data);
|
|
175
|
+
* }
|
|
176
|
+
*/
|
|
177
|
+
stream(shared: S, params?: P, options?: RunOptions): AsyncGenerator<StreamEvent<S>>;
|
|
178
|
+
protected _execute(shared: S, params: P, signal?: AbortSignal): Promise<void>;
|
|
179
|
+
private _addFn;
|
|
180
|
+
/** Resolve a NumberOrFn value against the current shared state and params. */
|
|
181
|
+
private _res;
|
|
182
|
+
private _runSub;
|
|
183
|
+
private _retry;
|
|
184
|
+
private _withTimeout;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export { FlowBuilder as F, type NodeFn as N, type RunOptions as R, type StepMeta as S, type Validator as V, type NodeOptions as a, type FlowneerPlugin as b, type FlowHooks as c, type NumberOrFn as d, type StreamEvent as e };
|