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 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://badges.ws/npm/v/flowneer" /></a>
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://badges.ws/npm/l/flowneer" /></a>
9
- <a href="https://www.npmjs.com/package/flowneer"><img src="https://badges.ws/npm/dt/flowneer" /></a>
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 | Hook(s) used |
511
- | ---------------------------- | ----------------------------- | ----------------------------------- |
512
- | Observability / tracing | `withHistory`, `withTiming` | `beforeStep` + `afterStep` |
513
- | Persistence / checkpointing | `withCheckpoint` | `afterStep` |
514
- | Versioned persistence | `withVersionedCheckpoint` | `beforeFlow` + `afterStep` |
515
- | Step/execution skip | `withDryRun`, `withReplay` | `wrapStep` |
516
- | Safe parallel isolation | `withAtomicUpdates` | `wrapParallelFn` (via core reducer) |
517
- | Human-in-the-loop / approval | `withInterrupts` | `then()` + `InterruptError` |
518
- | Message passing | `withChannels` | `beforeFlow` |
519
- | Infinite-loop protection | `withCycles`, `withStepLimit` | `afterStep` / `beforeStep` |
520
- | Cleanup / teardown | custom | `afterFlow` |
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 };