flowneer 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,6 +22,10 @@ A tiny, zero-dependency fluent flow builder for TypeScript. Chain steps, branch
22
22
  bun add flowneer
23
23
  ```
24
24
 
25
+ ## For LLM Agents
26
+ [llms.txt](https://fanna1119.github.io/flowneer/llms.txt)
27
+ [llms-full.txt](https://fanna1119.github.io/flowneer/llms-full.txt)
28
+
25
29
  ## Quick start
26
30
 
27
31
  ```typescript
@@ -0,0 +1,390 @@
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
+ /** Human-readable label for this step, shown in error messages and hook metadata. */
57
+ label?: string;
58
+ }
59
+ interface RunOptions {
60
+ signal?: AbortSignal;
61
+ }
62
+ /** Metadata exposed to hooks — intentionally minimal to avoid coupling. */
63
+ interface StepMeta {
64
+ index: number;
65
+ type: "fn" | "branch" | "loop" | "batch" | "parallel" | "anchor";
66
+ label?: string;
67
+ }
68
+ /**
69
+ * Narrows which steps a hook fires for.
70
+ *
71
+ * - **String array** — only steps whose `label` matches are affected.
72
+ * Entries are exact matches unless they contain `*`, which acts as a
73
+ * wildcard matching any substring (glob-style).
74
+ * e.g. `["llm:*"]` matches `"llm:summarise"`, `"llm:embed"`, etc.
75
+ * - **Predicate** — full control; return `true` to match.
76
+ *
77
+ * Unmatched `wrapStep`/`wrapParallelFn` hooks still call `next()` automatically
78
+ * so the middleware chain is never broken.
79
+ *
80
+ * @example
81
+ * // Exact labels
82
+ * flow.withRateLimit({ intervalMs: 1000 }, ["callLlm", "callEmbeddings"]);
83
+ *
84
+ * // Wildcard — any step whose label starts with "llm:"
85
+ * flow.withRateLimit({ intervalMs: 1000 }, ["llm:*"]);
86
+ *
87
+ * // Custom predicate
88
+ * flow.addHooks({ beforeStep: log }, (meta) => meta.type === "fn");
89
+ */
90
+ type StepFilter = string[] | ((meta: StepMeta) => boolean);
91
+ /** Lifecycle hooks that plugins can register. */
92
+ interface FlowHooks<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
93
+ /** Fires once before the first step runs. */
94
+ beforeFlow?: (shared: S, params: P) => void | Promise<void>;
95
+ beforeStep?: (meta: StepMeta, shared: S, params: P) => void | Promise<void>;
96
+ /**
97
+ * Wraps step execution — call `next()` to invoke the step body.
98
+ * Omitting `next()` skips execution (dry-run, mock, etc.).
99
+ * Multiple `wrapStep` registrations are composed innermost-first.
100
+ */
101
+ wrapStep?: (meta: StepMeta, next: () => Promise<void>, shared: S, params: P) => Promise<void>;
102
+ afterStep?: (meta: StepMeta, shared: S, params: P) => void | Promise<void>;
103
+ /**
104
+ * Wraps individual functions within a `.parallel()` step.
105
+ * `fnIndex` is the position within the fns array.
106
+ */
107
+ wrapParallelFn?: (meta: StepMeta, fnIndex: number, next: () => Promise<void>, shared: S, params: P) => Promise<void>;
108
+ onError?: (meta: StepMeta, error: unknown, shared: S, params: P) => void;
109
+ afterFlow?: (shared: S, params: P) => void | Promise<void>;
110
+ }
111
+ /**
112
+ * A plugin is an object whose keys become methods on `FlowBuilder.prototype`.
113
+ * Each method receives the builder as `this` and should return `this` for chaining.
114
+ *
115
+ * Use declaration merging to get type-safe access:
116
+ * ```ts
117
+ * declare module "flowneer" {
118
+ * interface FlowBuilder<S, P> { withTracing(fn: TraceCallback): this; }
119
+ * }
120
+ * ```
121
+ */
122
+ type FlowneerPlugin = Record<string, (this: FlowBuilder<any, any>, ...args: any[]) => any>;
123
+ /**
124
+ * An instance-scoped plugin. Called immediately when passed to `flow.use()`;
125
+ * registers hooks and/or performs setup on the specific `FlowBuilder` instance.
126
+ *
127
+ * Write plugins as factories so settings are captured in a closure:
128
+ * ```ts
129
+ * function withTiming(): InstancePlugin<any> {
130
+ * return (flow) => {
131
+ * const starts = new Map<number, number>();
132
+ * flow.addHooks({
133
+ * beforeStep: (meta) => { starts.set(meta.index, Date.now()); },
134
+ * afterStep: (meta, shared) => { shared.__timings ??= {}; shared.__timings[meta.index] = Date.now() - starts.get(meta.index)!; },
135
+ * });
136
+ * };
137
+ * }
138
+ *
139
+ * const flow = new FlowBuilder<MyState>()
140
+ * .with([withTiming(), withRateLimit({ rps: 10 })])
141
+ * .then(myStep);
142
+ * ```
143
+ *
144
+ * Order matters: plugins are applied left-to-right, so earlier plugins wrap
145
+ * later ones (the first plugin's `wrapStep` runs outermost).
146
+ */
147
+ type InstancePlugin<S = any, P extends Record<string, unknown> = Record<string, unknown>> = (flow: FlowBuilder<S, P>) => void;
148
+
149
+ interface FnStep<S, P extends Record<string, unknown>> {
150
+ type: "fn";
151
+ fn: NodeFn<S, P>;
152
+ retries: NumberOrFn<S, P>;
153
+ delaySec: NumberOrFn<S, P>;
154
+ timeoutMs: NumberOrFn<S, P>;
155
+ label?: string;
156
+ }
157
+ interface BranchStep<S, P extends Record<string, unknown>> {
158
+ type: "branch";
159
+ router: NodeFn<S, P>;
160
+ branches: Record<string, NodeFn<S, P>>;
161
+ retries: NumberOrFn<S, P>;
162
+ delaySec: NumberOrFn<S, P>;
163
+ timeoutMs: NumberOrFn<S, P>;
164
+ label?: string;
165
+ }
166
+ interface LoopStep<S, P extends Record<string, unknown>> {
167
+ type: "loop";
168
+ condition: (shared: S, params: P) => Promise<boolean> | boolean;
169
+ body: CoreFlowBuilder<S, P>;
170
+ label?: string;
171
+ }
172
+ interface BatchStep<S, P extends Record<string, unknown>> {
173
+ type: "batch";
174
+ itemsExtractor: (shared: S, params: P) => Promise<any[]> | any[];
175
+ processor: CoreFlowBuilder<S, P>;
176
+ key: string;
177
+ label?: string;
178
+ }
179
+ interface ParallelStep<S, P extends Record<string, unknown>> {
180
+ type: "parallel";
181
+ fns: NodeFn<S, P>[];
182
+ retries: NumberOrFn<S, P>;
183
+ delaySec: NumberOrFn<S, P>;
184
+ timeoutMs: NumberOrFn<S, P>;
185
+ reducer?: (shared: S, drafts: S[]) => void;
186
+ label?: string;
187
+ }
188
+ interface AnchorStep {
189
+ type: "anchor";
190
+ name: string;
191
+ /** Maximum number of times a goto may jump to this anchor per run. */
192
+ maxVisits?: number;
193
+ }
194
+ type Step<S, P extends Record<string, unknown>> = FnStep<S, P> | BranchStep<S, P> | LoopStep<S, P> | BatchStep<S, P> | ParallelStep<S, P> | AnchorStep;
195
+
196
+ type ResolvedHooks<S, P extends Record<string, unknown>> = {
197
+ beforeFlow: NonNullable<FlowHooks<S, P>["beforeFlow"]>[];
198
+ beforeStep: NonNullable<FlowHooks<S, P>["beforeStep"]>[];
199
+ wrapStep: NonNullable<FlowHooks<S, P>["wrapStep"]>[];
200
+ afterStep: NonNullable<FlowHooks<S, P>["afterStep"]>[];
201
+ wrapParallelFn: NonNullable<FlowHooks<S, P>["wrapParallelFn"]>[];
202
+ onError: NonNullable<FlowHooks<S, P>["onError"]>[];
203
+ afterFlow: NonNullable<FlowHooks<S, P>["afterFlow"]>[];
204
+ };
205
+ /**
206
+ * Runtime context passed to every registered step handler.
207
+ * Full access to the builder so handlers can run sub-flows etc.
208
+ */
209
+ interface StepContext<S, P extends Record<string, unknown> = Record<string, unknown>> {
210
+ shared: S;
211
+ params: P;
212
+ signal?: AbortSignal;
213
+ hooks: ResolvedHooks<S, P>;
214
+ meta: StepMeta;
215
+ builder: CoreFlowBuilder<S, P>;
216
+ }
217
+ /**
218
+ * Signature for step type execution handlers registered via
219
+ * `CoreFlowBuilder.registerStepType()`.
220
+ *
221
+ * Return a goto anchor name (without `#`) to jump, or `undefined` to continue.
222
+ */
223
+ type StepHandler = (step: any, ctx: StepContext<any, any>) => Promise<string | undefined>;
224
+ /**
225
+ * Minimal orchestration engine. Knows nothing about specific step types —
226
+ * those are registered as plugins via `CoreFlowBuilder.registerStepType()`.
227
+ *
228
+ * Use this as a base when you want a completely fresh, zero-assumption builder.
229
+ * The exported `FlowBuilder` extends this with all primitive step types
230
+ * (fn, branch, loop, batch, parallel, anchor) pre-registered.
231
+ */
232
+ declare class CoreFlowBuilder<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
233
+ /** @internal Ordered list of step descriptors. */
234
+ steps: Step<S, P>[];
235
+ /** @internal Cached anchor-name → index map; null when stale. */
236
+ protected _anchorMap: Map<string, number> | null;
237
+ private _hooksList;
238
+ private _hooksCache;
239
+ private static _stepHandlers;
240
+ /**
241
+ * Register a handler for a custom step type.
242
+ * Called once at module load time from each step plugin.
243
+ *
244
+ * @example
245
+ * CoreFlowBuilder.registerStepType("myStep", async (step, ctx) => {
246
+ * await doWork(ctx.shared);
247
+ * return undefined;
248
+ * });
249
+ */
250
+ static registerStepType(type: string, handler: StepHandler): void;
251
+ private _hooks;
252
+ /**
253
+ * Register lifecycle hooks (called by plugin methods, not by consumers).
254
+ * Returns a dispose function that removes these hooks when called.
255
+ */
256
+ protected _setHooks(hooks: Partial<FlowHooks<S, P>>, filter?: StepFilter): () => void;
257
+ /**
258
+ * Register a plugin — copies its methods onto `FlowBuilder.prototype`.
259
+ */
260
+ static use(plugin: FlowneerPlugin): void;
261
+ /**
262
+ * Apply one or more instance-scoped plugins to this builder.
263
+ * Each plugin is called immediately; plugins are applied in array order
264
+ * so the first plugin's hooks wrap the later ones.
265
+ *
266
+ * @example
267
+ * const flow = new FlowBuilder<MyState>()
268
+ * .with([withTiming(), withRateLimit({ rps: 10 })])
269
+ * .then(myStep);
270
+ */
271
+ with(plugins: InstancePlugin<S, P> | Array<InstancePlugin<S, P>>): this;
272
+ /**
273
+ * Register lifecycle hooks directly on this instance.
274
+ * Returns a dispose function that removes these hooks when called.
275
+ */
276
+ addHooks(hooks: Partial<FlowHooks<S, P>>, filter?: StepFilter): () => void;
277
+ /** Execute the flow. */
278
+ run(shared: S, params?: P, options?: RunOptions): Promise<void>;
279
+ /**
280
+ * Execute the flow and yield `StreamEvent`s as an async generator.
281
+ */
282
+ stream(shared: S, params?: P, options?: RunOptions): AsyncGenerator<StreamEvent<S>>;
283
+ /** @internal Core execution loop, bypasses beforeFlow/afterFlow hooks. */
284
+ _execute(shared: S, params: P, signal?: AbortSignal): Promise<void>;
285
+ private _runStep;
286
+ /** @internal Run a sub-flow, wrapping errors with context. */
287
+ _runSub(label: string, fn: () => Promise<void>): Promise<void>;
288
+ /** @internal Resolve NodeOptions into their stored defaults. */
289
+ _resolveOptions(options?: NodeOptions<S, P>): {
290
+ retries: NumberOrFn<S, P>;
291
+ delaySec: NumberOrFn<S, P>;
292
+ timeoutMs: NumberOrFn<S, P>;
293
+ label: string | undefined;
294
+ };
295
+ /** @internal Push a step and invalidate the anchor-map cache. */
296
+ protected _pushStep(step: Step<S, P>): void;
297
+ }
298
+
299
+ /**
300
+ * Fluent builder for composable flows.
301
+ *
302
+ * Steps execute sequentially in the order added. Call `.run(shared)` to execute.
303
+ *
304
+ * **Shared-state safety**: all steps operate on the same shared object.
305
+ * Mutate it directly; avoid spreading/replacing the entire object.
306
+ *
307
+ * Built on top of {@link CoreFlowBuilder} which exposes the raw engine for
308
+ * advanced use cases (custom step types, zero-assumption base class, etc.).
309
+ */
310
+ declare class FlowBuilder<S = any, P extends Record<string, unknown> = Record<string, unknown>> extends CoreFlowBuilder<S, P> {
311
+ /**
312
+ * Register a plugin — copies its methods onto `FlowBuilder.prototype`.
313
+ *
314
+ * Plugins should also call `FlowBuilder.use()` (not just `CoreFlowBuilder.use()`)
315
+ * so TypeScript module-augmentation on `interface FlowBuilder` resolves
316
+ * correctly and the method appears on `FlowBuilder` instances specifically.
317
+ */
318
+ static use(plugin: FlowneerPlugin): void;
319
+ /** Set the first step, resetting any prior chain. */
320
+ startWith(fn: NodeFn<S, P>, options?: NodeOptions<S, P>): this;
321
+ /** Append a sequential step. */
322
+ then(fn: NodeFn<S, P>, options?: NodeOptions<S, P>): this;
323
+ /**
324
+ * Splice all steps from a `Fragment` into this flow at the current position.
325
+ *
326
+ * Fragments are reusable partial flows created with the `fragment()` factory.
327
+ *
328
+ * @example
329
+ * ```ts
330
+ * const enrich = fragment<S>().then(fetchUser).then(enrichProfile);
331
+ *
332
+ * new FlowBuilder<S>().then(init).add(enrich).then(finalize).run(shared);
333
+ * ```
334
+ */
335
+ add(frag: FlowBuilder<S, P>): this;
336
+ /**
337
+ * Append a routing step.
338
+ * `router` returns a key; the matching branch executes, then the chain continues.
339
+ */
340
+ branch(router: NodeFn<S, P>, branches: Record<string, NodeFn<S, P>>, options?: NodeOptions<S, P>): this;
341
+ /**
342
+ * Append a looping step.
343
+ * Repeatedly runs `body` while `condition` returns true.
344
+ */
345
+ loop(condition: (shared: S, params: P) => Promise<boolean> | boolean, body: (b: FlowBuilder<S, P>) => void, options?: {
346
+ label?: string;
347
+ }): this;
348
+ /**
349
+ * Append a batch step.
350
+ * Runs `processor` once per item extracted by `items`, setting
351
+ * `shared[key]` each time (defaults to `"__batchItem"`).
352
+ *
353
+ * Use a unique `key` when nesting batches so each level has its own namespace.
354
+ */
355
+ batch(items: (shared: S, params: P) => Promise<any[]> | any[], processor: (b: FlowBuilder<S, P>) => void, options?: {
356
+ key?: string;
357
+ label?: string;
358
+ }): this;
359
+ /**
360
+ * Append a parallel step.
361
+ * Runs all `fns` concurrently against the same shared state.
362
+ *
363
+ * When `reducer` is provided each fn receives its own shallow clone of
364
+ * `shared`; after all fns complete the reducer merges the drafts back
365
+ * into the original shared object — preventing concurrent mutation races.
366
+ */
367
+ parallel(fns: NodeFn<S, P>[], options?: NodeOptions<S, P>, reducer?: (shared: S, drafts: S[]) => void): this;
368
+ /**
369
+ * Insert a named anchor. Anchors are no-op markers that can be jumped to
370
+ * from any `NodeFn` by returning `"#anchorName"`.
371
+ *
372
+ * @param name - Anchor identifier, referenced as `"#name"` in goto returns.
373
+ * @param maxVisits - Optional cycle guard: throws after this many gotos to
374
+ * this anchor per `run()`. Replaces the need for a separate `withCycles`
375
+ * plugin call.
376
+ *
377
+ * @example
378
+ * ```ts
379
+ * flow
380
+ * .anchor("refine", 5) // allow up to 5 refinement iterations
381
+ * .then(generateDraft)
382
+ * .then(s => s.score < 0.9 ? "#refine" : undefined)
383
+ * .then(publish);
384
+ * ```
385
+ */
386
+ anchor(name: string, maxVisits?: number): this;
387
+ private _addFn;
388
+ }
389
+
390
+ export { type AnchorStep as A, type BatchStep as B, CoreFlowBuilder as C, FlowBuilder as F, type InstancePlugin as I, type LoopStep as L, type NodeFn as N, type ParallelStep as P, type ResolvedHooks as R, type StepFilter as S, type Validator as V, type FlowneerPlugin as a, type NodeOptions as b, type FlowHooks as c, type StepMeta as d, type BranchStep as e, type FnStep as f, type NumberOrFn as g, type RunOptions as h, type Step as i, type StepContext as j, type StepHandler as k, type StreamEvent as l };
@@ -0,0 +1,16 @@
1
+ /** Wraps step failures with context about which step failed. */
2
+ declare class FlowError extends Error {
3
+ readonly step: string;
4
+ readonly cause: unknown;
5
+ constructor(step: string, cause: unknown);
6
+ }
7
+ /**
8
+ * Thrown by `interruptIf` to pause a flow.
9
+ * Catch this in your runner to save `savedShared` and resume later.
10
+ */
11
+ declare class InterruptError extends Error {
12
+ readonly savedShared: unknown;
13
+ constructor(shared: unknown);
14
+ }
15
+
16
+ export { FlowError as F, InterruptError as I };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export { F as FlowBuilder, c as FlowHooks, a as FlowneerPlugin, N as NodeFn, b as NodeOptions, f as NumberOrFn, R as RunOptions, S as StepMeta, h as StreamEvent, V as Validator } from './FlowBuilder-C3B91Fzo.js';
2
- export { FlowError, Fragment, InterruptError, fragment } from './src/index.js';
1
+ export { F as FlowBuilder, c as FlowHooks, a as FlowneerPlugin, N as NodeFn, b as NodeOptions, g as NumberOrFn, h as RunOptions, d as StepMeta, l as StreamEvent, V as Validator } from './FlowBuilder-B0SMJ4um.js';
2
+ export { Fragment, fragment } from './src/index.js';
3
+ export { F as FlowError, I as InterruptError } from './errors-u-hq7p5N.js';