flowneer 0.7.1 → 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.
@@ -65,6 +65,29 @@ interface StepMeta {
65
65
  type: "fn" | "branch" | "loop" | "batch" | "parallel" | "anchor";
66
66
  label?: string;
67
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);
68
91
  /** Lifecycle hooks that plugins can register. */
69
92
  interface FlowHooks<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
70
93
  /** Fires once before the first step runs. */
@@ -230,7 +253,7 @@ declare class CoreFlowBuilder<S = any, P extends Record<string, unknown> = Recor
230
253
  * Register lifecycle hooks (called by plugin methods, not by consumers).
231
254
  * Returns a dispose function that removes these hooks when called.
232
255
  */
233
- protected _setHooks(hooks: Partial<FlowHooks<S, P>>): () => void;
256
+ protected _setHooks(hooks: Partial<FlowHooks<S, P>>, filter?: StepFilter): () => void;
234
257
  /**
235
258
  * Register a plugin — copies its methods onto `FlowBuilder.prototype`.
236
259
  */
@@ -250,7 +273,7 @@ declare class CoreFlowBuilder<S = any, P extends Record<string, unknown> = Recor
250
273
  * Register lifecycle hooks directly on this instance.
251
274
  * Returns a dispose function that removes these hooks when called.
252
275
  */
253
- addHooks(hooks: Partial<FlowHooks<S, P>>): () => void;
276
+ addHooks(hooks: Partial<FlowHooks<S, P>>, filter?: StepFilter): () => void;
254
277
  /** Execute the flow. */
255
278
  run(shared: S, params?: P, options?: RunOptions): Promise<void>;
256
279
  /**
@@ -364,4 +387,4 @@ declare class FlowBuilder<S = any, P extends Record<string, unknown> = Record<st
364
387
  private _addFn;
365
388
  }
366
389
 
367
- 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 StepMeta as S, type Validator as V, type FlowneerPlugin as a, type NodeOptions as b, type FlowHooks as c, type BranchStep as d, type FnStep as e, type NumberOrFn as f, type RunOptions as g, type Step as h, type StepContext as i, type StepHandler as j, type StreamEvent as k };
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, g as RunOptions, S as StepMeta, k as StreamEvent, V as Validator } from './FlowBuilder-DJkzGH5l.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';
package/dist/index.js CHANGED
@@ -74,6 +74,35 @@ function buildAnchorMap(steps) {
74
74
  }
75
75
 
76
76
  // src/core/CoreFlowBuilder.ts
77
+ function matchesFilter(filter, meta) {
78
+ if (!Array.isArray(filter)) return filter(meta);
79
+ if (meta.label === void 0) return false;
80
+ const label = meta.label;
81
+ return filter.some((pattern) => {
82
+ if (!pattern.includes("*")) return pattern === label;
83
+ const re = new RegExp(
84
+ "^" + pattern.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
85
+ );
86
+ return re.test(label);
87
+ });
88
+ }
89
+ function applyStepFilter(hooks, filter) {
90
+ const match = (meta) => matchesFilter(filter, meta);
91
+ const wrap = (key, fallback = () => {
92
+ }) => {
93
+ const orig = hooks[key];
94
+ if (!orig) return;
95
+ hooks[key] = ((...args) => match(args[0]) ? orig(...args) : fallback(
96
+ ...args
97
+ ));
98
+ };
99
+ wrap("beforeStep");
100
+ wrap("afterStep");
101
+ wrap("onError");
102
+ wrap("wrapStep", (_m, next) => next());
103
+ wrap("wrapParallelFn", (_m, _fi, next) => next());
104
+ return hooks;
105
+ }
77
106
  function buildHookCache(list) {
78
107
  const pick = (key) => list.map((h) => h[key]).filter(Boolean);
79
108
  return {
@@ -120,11 +149,12 @@ var CoreFlowBuilder = class _CoreFlowBuilder {
120
149
  * Register lifecycle hooks (called by plugin methods, not by consumers).
121
150
  * Returns a dispose function that removes these hooks when called.
122
151
  */
123
- _setHooks(hooks) {
124
- this._hooksList.push(hooks);
152
+ _setHooks(hooks, filter) {
153
+ const resolved = filter ? applyStepFilter(hooks, filter) : hooks;
154
+ this._hooksList.push(resolved);
125
155
  this._hooksCache = null;
126
156
  return () => {
127
- const idx = this._hooksList.indexOf(hooks);
157
+ const idx = this._hooksList.indexOf(resolved);
128
158
  if (idx !== -1) this._hooksList.splice(idx, 1);
129
159
  this._hooksCache = null;
130
160
  };
@@ -156,8 +186,8 @@ var CoreFlowBuilder = class _CoreFlowBuilder {
156
186
  * Register lifecycle hooks directly on this instance.
157
187
  * Returns a dispose function that removes these hooks when called.
158
188
  */
159
- addHooks(hooks) {
160
- return this._setHooks(hooks);
189
+ addHooks(hooks, filter) {
190
+ return this._setHooks(hooks, filter);
161
191
  }
162
192
  // -------------------------------------------------------------------------
163
193
  // Execution API
@@ -1,4 +1,4 @@
1
- import { a as FlowneerPlugin, F as FlowBuilder, N as NodeFn } from './FlowBuilder-DJkzGH5l.js';
1
+ import { a as FlowneerPlugin, F as FlowBuilder, N as NodeFn } from './FlowBuilder-B0SMJ4um.js';
2
2
  import { ToolCall, ToolResult } from './plugins/tools/index.js';
3
3
 
4
4
  /**
@@ -1,5 +1,5 @@
1
- export { H as HumanNodeOptions, R as ReActLoopOptions, T as ThinkResult, h as hierarchicalCrew, r as resumeFlow, a as roundRobinDebate, s as sequentialCrew, b as supervisorCrew, w as withHumanNode, c as withReActLoop } from '../../patterns-CCtG27Gv.js';
2
- import { F as FlowBuilder } from '../../FlowBuilder-DJkzGH5l.js';
1
+ export { H as HumanNodeOptions, R as ReActLoopOptions, T as ThinkResult, h as hierarchicalCrew, r as resumeFlow, a as roundRobinDebate, s as sequentialCrew, b as supervisorCrew, w as withHumanNode, c as withReActLoop } from '../../patterns-1gFxWo6a.js';
2
+ import { F as FlowBuilder } from '../../FlowBuilder-B0SMJ4um.js';
3
3
  import { ToolRegistry, ToolResult, Tool, ToolCall, ToolParam } from '../tools/index.js';
4
4
 
5
5
  /**
@@ -157,6 +157,35 @@ function buildAnchorMap(steps) {
157
157
  }
158
158
 
159
159
  // src/core/CoreFlowBuilder.ts
160
+ function matchesFilter(filter, meta) {
161
+ if (!Array.isArray(filter)) return filter(meta);
162
+ if (meta.label === void 0) return false;
163
+ const label = meta.label;
164
+ return filter.some((pattern) => {
165
+ if (!pattern.includes("*")) return pattern === label;
166
+ const re = new RegExp(
167
+ "^" + pattern.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
168
+ );
169
+ return re.test(label);
170
+ });
171
+ }
172
+ function applyStepFilter(hooks, filter) {
173
+ const match = (meta) => matchesFilter(filter, meta);
174
+ const wrap = (key, fallback = () => {
175
+ }) => {
176
+ const orig = hooks[key];
177
+ if (!orig) return;
178
+ hooks[key] = ((...args) => match(args[0]) ? orig(...args) : fallback(
179
+ ...args
180
+ ));
181
+ };
182
+ wrap("beforeStep");
183
+ wrap("afterStep");
184
+ wrap("onError");
185
+ wrap("wrapStep", (_m, next) => next());
186
+ wrap("wrapParallelFn", (_m, _fi, next) => next());
187
+ return hooks;
188
+ }
160
189
  function buildHookCache(list) {
161
190
  const pick = (key) => list.map((h) => h[key]).filter(Boolean);
162
191
  return {
@@ -203,11 +232,12 @@ var CoreFlowBuilder = class _CoreFlowBuilder {
203
232
  * Register lifecycle hooks (called by plugin methods, not by consumers).
204
233
  * Returns a dispose function that removes these hooks when called.
205
234
  */
206
- _setHooks(hooks) {
207
- this._hooksList.push(hooks);
235
+ _setHooks(hooks, filter) {
236
+ const resolved = filter ? applyStepFilter(hooks, filter) : hooks;
237
+ this._hooksList.push(resolved);
208
238
  this._hooksCache = null;
209
239
  return () => {
210
- const idx = this._hooksList.indexOf(hooks);
240
+ const idx = this._hooksList.indexOf(resolved);
211
241
  if (idx !== -1) this._hooksList.splice(idx, 1);
212
242
  this._hooksCache = null;
213
243
  };
@@ -239,8 +269,8 @@ var CoreFlowBuilder = class _CoreFlowBuilder {
239
269
  * Register lifecycle hooks directly on this instance.
240
270
  * Returns a dispose function that removes these hooks when called.
241
271
  */
242
- addHooks(hooks) {
243
- return this._setHooks(hooks);
272
+ addHooks(hooks, filter) {
273
+ return this._setHooks(hooks, filter);
244
274
  }
245
275
  // -------------------------------------------------------------------------
246
276
  // Execution API
@@ -1,4 +1,4 @@
1
- import { a as FlowneerPlugin, N as NodeFn, b as NodeOptions } from '../../FlowBuilder-DJkzGH5l.js';
1
+ import { a as FlowneerPlugin, N as NodeFn, b as NodeOptions } from '../../FlowBuilder-B0SMJ4um.js';
2
2
 
3
3
  declare module "../../Flowneer" {
4
4
  interface FlowBuilder<S, P> {
@@ -45,4 +45,89 @@ declare module "../../Flowneer" {
45
45
  */
46
46
  declare const withAtomicUpdates: FlowneerPlugin;
47
47
 
48
- export { withAtomicUpdates, withDryRun, withMocks, withStepLimit };
48
+ /**
49
+ * A single node in the static path tree.
50
+ */
51
+ interface PathNode {
52
+ /** Stable id: `"fn_0"`, `"branch_2"`, `"anchor:refine"`, etc. */
53
+ id: string;
54
+ type: "fn" | "branch" | "loop" | "batch" | "parallel" | "anchor";
55
+ label?: string;
56
+ /** Branch arms — keys are the branch names, values are the arm subtrees. */
57
+ branches?: Record<string, PathNode[]>;
58
+ /** Inline body steps (loop / batch). */
59
+ body?: PathNode[];
60
+ /** One lane per parallel fn. */
61
+ parallel?: PathNode[][];
62
+ }
63
+ /**
64
+ * The result of `.analyzeFlow()`.
65
+ */
66
+ interface PathMap {
67
+ nodes: PathNode[];
68
+ /** All anchor names declared in this flow (and nested sub-flows). */
69
+ anchors: string[];
70
+ /**
71
+ * True if any `fn` step exists — those can dynamically return goto targets
72
+ * (`"#anchorName"`) that cannot be resolved without running the flow.
73
+ * Static analysis is therefore necessarily conservative for those edges.
74
+ */
75
+ hasDynamicGotos: boolean;
76
+ }
77
+ interface TraceEvent {
78
+ stepIndex: number;
79
+ type: string;
80
+ label?: string;
81
+ durationMs: number;
82
+ }
83
+ interface TraceReport {
84
+ events: TraceEvent[];
85
+ totalDurationMs: number;
86
+ /** Human-readable ordered list of visited step labels (unlabelled steps are skipped). */
87
+ pathSummary: string[];
88
+ }
89
+ interface TraceHandle {
90
+ /** Returns the trace collected so far. Safe to call mid-run. */
91
+ getTrace(): TraceReport;
92
+ /** Removes the installed hooks. */
93
+ dispose(): void;
94
+ }
95
+ declare module "../../Flowneer" {
96
+ interface FlowBuilder<S, P> {
97
+ /**
98
+ * Statically analyze this flow and return a `PathMap` describing all
99
+ * possible nodes and anchors without executing anything.
100
+ *
101
+ * `hasDynamicGotos` is true whenever `fn` steps are present — those may
102
+ * return goto targets at runtime that cannot be resolved statically.
103
+ *
104
+ * @example
105
+ * const map = flow.analyzeFlow();
106
+ * console.log("anchors:", map.anchors);
107
+ * console.log("has dynamic gotos:", map.hasDynamicGotos);
108
+ */
109
+ analyzeFlow(): PathMap;
110
+ /**
111
+ * Install execution-trace hooks on this flow.
112
+ *
113
+ * Records every step's index, type, label, and wall-clock duration.
114
+ * Call `getTrace()` after (or during) the run to inspect results.
115
+ * Call `dispose()` to remove the hooks when no longer needed.
116
+ *
117
+ * Composable with `withDryRun` — trace structure without side effects:
118
+ * ```ts
119
+ * flow.withDryRun().withTrace()
120
+ * ```
121
+ *
122
+ * @example
123
+ * const trace = flow.withTrace();
124
+ * await flow.run(shared);
125
+ * console.log(trace.getTrace().pathSummary);
126
+ * trace.dispose();
127
+ */
128
+ withTrace(): TraceHandle;
129
+ }
130
+ }
131
+ declare const withFlowAnalyzer: FlowneerPlugin;
132
+
133
+ export { type PathMap, type PathNode, type TraceEvent, type TraceHandle, type TraceReport, withAtomicUpdates, withDryRun, withFlowAnalyzer, withMocks, withStepLimit };
@@ -49,9 +49,140 @@ var withAtomicUpdates = {
49
49
  return this.parallel(fns, options, reducer);
50
50
  }
51
51
  };
52
+
53
+ // plugins/dev/withFlowAnalyzer.ts
54
+ function walkSteps(steps, prefix = "") {
55
+ const nodes = [];
56
+ const anchors = [];
57
+ let hasDynamicGotos = false;
58
+ for (let i = 0; i < steps.length; i++) {
59
+ const step = steps[i];
60
+ const rawId = step.type === "anchor" ? `anchor:${step.name}` : `${step.type}_${i}`;
61
+ const id = prefix ? `${prefix}:${rawId}` : rawId;
62
+ switch (step.type) {
63
+ case "fn": {
64
+ hasDynamicGotos = true;
65
+ nodes.push({
66
+ id,
67
+ type: "fn",
68
+ label: step.label ?? step.fn?.name
69
+ });
70
+ break;
71
+ }
72
+ case "anchor": {
73
+ anchors.push(step.name);
74
+ nodes.push({ id, type: "anchor", label: step.name });
75
+ break;
76
+ }
77
+ case "branch": {
78
+ const branchMap = {};
79
+ for (const [key, fn] of Object.entries(step.branches ?? {})) {
80
+ branchMap[key] = [
81
+ {
82
+ id: `${id}:arm:${key}`,
83
+ type: "fn",
84
+ label: fn?.name ?? key
85
+ }
86
+ ];
87
+ hasDynamicGotos = true;
88
+ }
89
+ nodes.push({
90
+ id,
91
+ type: "branch",
92
+ label: step.label ?? step.router?.name,
93
+ branches: branchMap
94
+ });
95
+ break;
96
+ }
97
+ case "loop": {
98
+ const bodySteps = step.body?.steps ?? [];
99
+ const inner = walkSteps(bodySteps, `${id}:body`);
100
+ anchors.push(...inner.anchors);
101
+ hasDynamicGotos = hasDynamicGotos || inner.hasDynamicGotos;
102
+ nodes.push({
103
+ id,
104
+ type: "loop",
105
+ label: step.label,
106
+ body: inner.nodes
107
+ });
108
+ break;
109
+ }
110
+ case "batch": {
111
+ const procSteps = step.processor?.steps ?? [];
112
+ const inner = walkSteps(procSteps, `${id}:each`);
113
+ anchors.push(...inner.anchors);
114
+ hasDynamicGotos = hasDynamicGotos || inner.hasDynamicGotos;
115
+ nodes.push({
116
+ id,
117
+ type: "batch",
118
+ label: step.label,
119
+ body: inner.nodes
120
+ });
121
+ break;
122
+ }
123
+ case "parallel": {
124
+ const fns = step.fns ?? [];
125
+ const lanes = fns.map((fn, fi) => [
126
+ {
127
+ id: `${id}:fn_${fi}`,
128
+ type: "fn",
129
+ label: fn?.name ?? `fn_${fi}`
130
+ }
131
+ ]);
132
+ hasDynamicGotos = hasDynamicGotos || fns.length > 0;
133
+ nodes.push({
134
+ id,
135
+ type: "parallel",
136
+ label: step.label,
137
+ parallel: lanes
138
+ });
139
+ break;
140
+ }
141
+ default:
142
+ break;
143
+ }
144
+ }
145
+ return { nodes, anchors, hasDynamicGotos };
146
+ }
147
+ var withFlowAnalyzer = {
148
+ analyzeFlow() {
149
+ const steps = this.steps ?? [];
150
+ const { nodes, anchors, hasDynamicGotos } = walkSteps(steps);
151
+ return { nodes, anchors, hasDynamicGotos };
152
+ },
153
+ withTrace() {
154
+ const events = [];
155
+ const starts = /* @__PURE__ */ new Map();
156
+ const dispose = this.addHooks({
157
+ beforeStep: (meta) => {
158
+ starts.set(meta.index, Date.now());
159
+ },
160
+ afterStep: (meta) => {
161
+ const start = starts.get(meta.index) ?? Date.now();
162
+ const durationMs = Date.now() - start;
163
+ starts.delete(meta.index);
164
+ events.push({
165
+ stepIndex: meta.index,
166
+ type: meta.type,
167
+ label: meta.label,
168
+ durationMs
169
+ });
170
+ }
171
+ });
172
+ return {
173
+ getTrace() {
174
+ const totalDurationMs = events.reduce((s, e) => s + e.durationMs, 0);
175
+ const pathSummary = events.filter((e) => e.label !== void 0).map((e) => e.label);
176
+ return { events: [...events], totalDurationMs, pathSummary };
177
+ },
178
+ dispose
179
+ };
180
+ }
181
+ };
52
182
  export {
53
183
  withAtomicUpdates,
54
184
  withDryRun,
185
+ withFlowAnalyzer,
55
186
  withMocks,
56
187
  withStepLimit
57
188
  };
@@ -1,4 +1,4 @@
1
- import { F as FlowBuilder } from '../../FlowBuilder-DJkzGH5l.js';
1
+ import { F as FlowBuilder } from '../../FlowBuilder-B0SMJ4um.js';
2
2
 
3
3
  /** Case-insensitive exact match. Returns 1.0 or 0.0. */
4
4
  declare function exactMatch(predicted: string, expected: string): number;
@@ -1,4 +1,4 @@
1
- import { a as FlowneerPlugin, N as NodeFn, b as NodeOptions } from '../../FlowBuilder-DJkzGH5l.js';
1
+ import { a as FlowneerPlugin, N as NodeFn, b as NodeOptions } from '../../FlowBuilder-B0SMJ4um.js';
2
2
 
3
3
  /** A single node entry in the exported graph. */
4
4
  interface GraphNodeExport {