flowneer 0.9.4 → 0.9.6

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.
@@ -86,6 +86,11 @@ interface StepMeta {
86
86
  * Entries are exact matches unless they contain `*`, which acts as a
87
87
  * wildcard matching any substring (glob-style).
88
88
  * e.g. `["llm:*"]` matches `"llm:summarise"`, `"llm:embed"`, etc.
89
+ * - **Negation** — prefix an entry with `!` to exclude matching steps.
90
+ * Negation veto always wins over a positive match in the same array.
91
+ * A negation-only array matches all labelled steps that are not excluded.
92
+ * e.g. `["!human:*"]` fires on every step _except_ `human:*` steps.
93
+ * e.g. `["!human:*", "llm:*"]` fires on `llm:*` steps only, never `human:*`.
89
94
  * - **Predicate** — full control; return `true` to match.
90
95
  *
91
96
  * Unmatched `wrapStep`/`wrapParallelFn` hooks still call `next()` automatically
@@ -98,6 +103,12 @@ interface StepMeta {
98
103
  * // Wildcard — any step whose label starts with "llm:"
99
104
  * flow.withRateLimit({ intervalMs: 1000 }, ["llm:*"]);
100
105
  *
106
+ * // Negation-only — apply everywhere except human-in-loop steps
107
+ * flow.withRateLimit({ intervalMs: 1000 }, ["!human:*"]);
108
+ *
109
+ * // Mixed — apply to llm steps, but never human steps
110
+ * flow.withRateLimit({ intervalMs: 1000 }, ["!human:*", "llm:*"]);
111
+ *
101
112
  * // Custom predicate
102
113
  * flow.addHooks({ beforeStep: log }, (meta) => meta.type === "fn");
103
114
  */
@@ -106,6 +117,7 @@ type StepFilter = string[] | ((meta: StepMeta) => boolean);
106
117
  interface FlowHooks<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
107
118
  /** Fires once before the first step runs. */
108
119
  beforeFlow?: (shared: S, params: P) => void | Promise<void>;
120
+ /** Fires before each step body executes. Respects `StepFilter` when registered via `_setHooks`. */
109
121
  beforeStep?: (meta: StepMeta, shared: S, params: P) => void | Promise<void>;
110
122
  /**
111
123
  * Wraps step execution — call `next()` to invoke the step body.
@@ -113,13 +125,21 @@ interface FlowHooks<S = any, P extends Record<string, unknown> = Record<string,
113
125
  * Multiple `wrapStep` registrations are composed innermost-first.
114
126
  */
115
127
  wrapStep?: (meta: StepMeta, next: () => Promise<void>, shared: S, params: P) => Promise<void>;
128
+ /** Fires after each step body completes successfully. Respects `StepFilter` when registered via `_setHooks`. */
116
129
  afterStep?: (meta: StepMeta, shared: S, params: P) => void | Promise<void>;
117
130
  /**
118
131
  * Wraps individual functions within a `.parallel()` step.
119
132
  * `fnIndex` is the position within the fns array.
120
133
  */
121
134
  wrapParallelFn?: (meta: StepMeta, fnIndex: number, next: () => Promise<void>, shared: S, params: P) => Promise<void>;
135
+ /**
136
+ * Fires when a step throws, before the error is re-thrown.
137
+ * Use for logging or writing error details to `shared` — do not suppress the error here;
138
+ * use `wrapStep` with a try/catch for recovery instead.
139
+ * Respects `StepFilter` when registered via `_setHooks`.
140
+ */
122
141
  onError?: (meta: StepMeta, error: unknown, shared: S, params: P) => void | Promise<void>;
142
+ /** Fires once after the last step completes (or after a flow error). Not affected by `StepFilter`. */
123
143
  afterFlow?: (shared: S, params: P) => void | Promise<void>;
124
144
  /**
125
145
  * Fires after each loop iteration completes. `iteration` is zero-based.
@@ -132,6 +152,14 @@ interface FlowHooks<S = any, P extends Record<string, unknown> = Record<string,
132
152
  */
133
153
  onAnchorHit?: (anchorName: string, shared: S, params: P) => void | Promise<void>;
134
154
  }
155
+ /**
156
+ * The `this` type inside every plugin method. Extends the public `FlowBuilder`
157
+ * with `_setHooks` so plugins can register lifecycle hooks without casting to
158
+ * `any`.
159
+ */
160
+ type PluginContext = FlowBuilder<any, any> & {
161
+ _setHooks(hooks: Partial<FlowHooks<any, any>>, filter?: StepFilter): () => void;
162
+ };
135
163
  /**
136
164
  * A plugin is an object whose keys become methods on a `FlowBuilder.extend()` subclass prototype.
137
165
  * Each method receives the builder as `this` and should return `this` for chaining.
@@ -143,7 +171,7 @@ interface FlowHooks<S = any, P extends Record<string, unknown> = Record<string,
143
171
  * }
144
172
  * ```
145
173
  */
146
- type FlowneerPlugin = Record<string, (this: FlowBuilder<any, any>, ...args: any[]) => any>;
174
+ type FlowneerPlugin = Record<string, (this: PluginContext, ...args: any[]) => any>;
147
175
  type ResolvedHooks<S, P extends Record<string, unknown>> = {
148
176
  beforeFlow: NonNullable<FlowHooks<S, P>["beforeFlow"]>[];
149
177
  beforeStep: NonNullable<FlowHooks<S, P>["beforeStep"]>[];
@@ -291,6 +319,25 @@ declare class CoreFlowBuilder<S = any, P extends Record<string, unknown> = Recor
291
319
  transparent?: boolean;
292
320
  }): void;
293
321
  private _hooks;
322
+ /**
323
+ * Register lifecycle hooks on this flow instance at runtime.
324
+ *
325
+ * Unlike plugin methods (which use `_setHooks` internally), `addHooks` is
326
+ * intended for consumers — app code, tests, or request-scoped instrumentation
327
+ * that needs to observe or modify a specific flow instance without defining a
328
+ * plugin.
329
+ *
330
+ * Returns a `dispose` function that removes the registered hooks when called.
331
+ *
332
+ * @example
333
+ * const dispose = flow.addHooks(
334
+ * { beforeStep: (meta) => console.log("->", meta.label) },
335
+ * ["llm:*"],
336
+ * );
337
+ * await flow.run(shared);
338
+ * dispose(); // removes the hooks
339
+ */
340
+ addHooks(hooks: Partial<FlowHooks<S, P>>, filter?: StepFilter): () => void;
294
341
  /**
295
342
  * Register lifecycle hooks (called by plugin methods, not by consumers).
296
343
  * Returns a dispose function that removes these hooks when called.
@@ -421,4 +468,4 @@ declare class FlowBuilder<S = any, P extends Record<string, unknown> = Record<st
421
468
  private _addFn;
422
469
  }
423
470
 
424
- export { type AnchorStep as A, type BatchStep as B, CoreFlowBuilder as C, type DagStep as D, FlowBuilder as F, 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 AugmentedState as e, type BranchStep as f, type FnStep as g, type NumberOrFn as h, type RunOptions as i, type Step as j, type StepContext as k, type StepHandler as l, type StreamEvent as m };
471
+ export { type AnchorStep as A, type BatchStep as B, CoreFlowBuilder as C, type DagStep as D, FlowBuilder as F, 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 AugmentedState as e, type BranchStep as f, type FnStep as g, type NumberOrFn as h, type PluginContext as i, type RunOptions as j, type Step as k, type StepContext as l, type StepHandler as m, type StreamEvent as n };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { F as FlowBuilder, c as FlowHooks, a as FlowneerPlugin, N as NodeFn, b as NodeOptions, h as NumberOrFn, i as RunOptions, d as StepMeta, m as StreamEvent, V as Validator } from './FlowBuilder-8kwREeyD.js';
1
+ export { F as FlowBuilder, c as FlowHooks, a as FlowneerPlugin, N as NodeFn, b as NodeOptions, h as NumberOrFn, j as RunOptions, d as StepMeta, n as StreamEvent, V as Validator } from './FlowBuilder-CwBQDOEN.js';
2
2
  export { Fragment, fragment } from './src/index.js';
3
3
  export { F as FlowError, I as InterruptError } from './errors-u-hq7p5N.js';
package/dist/index.js CHANGED
@@ -21,15 +21,21 @@ var InterruptError = class extends Error {
21
21
  };
22
22
 
23
23
  // src/core/utils.ts
24
+ function matchesPattern(pattern, label) {
25
+ return pattern.includes("*") ? new RegExp(
26
+ "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
27
+ ).test(label) : pattern === label;
28
+ }
24
29
  function matchesFilter(filter, meta) {
25
30
  if (!Array.isArray(filter)) return filter(meta);
31
+ const negations = filter.filter((p) => p.startsWith("!"));
32
+ const positives = filter.filter((p) => !p.startsWith("!"));
26
33
  const label = meta.label;
27
- if (label === void 0) return false;
28
- return filter.some(
29
- (p) => p.includes("*") ? new RegExp(
30
- "^" + p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
31
- ).test(label) : p === label
32
- );
34
+ if (label !== void 0 && negations.some((p) => matchesPattern(p.slice(1), label)))
35
+ return false;
36
+ if (positives.length > 0)
37
+ return label !== void 0 && positives.some((p) => matchesPattern(p, label));
38
+ return true;
33
39
  }
34
40
  function resolveNumber(val, fallback, shared, params) {
35
41
  if (val === void 0) return fallback;
@@ -156,6 +162,27 @@ var CoreFlowBuilder = class _CoreFlowBuilder {
156
162
  _hooks() {
157
163
  return this._hooksCache ??= buildHookCache(this._hooksList);
158
164
  }
165
+ /**
166
+ * Register lifecycle hooks on this flow instance at runtime.
167
+ *
168
+ * Unlike plugin methods (which use `_setHooks` internally), `addHooks` is
169
+ * intended for consumers — app code, tests, or request-scoped instrumentation
170
+ * that needs to observe or modify a specific flow instance without defining a
171
+ * plugin.
172
+ *
173
+ * Returns a `dispose` function that removes the registered hooks when called.
174
+ *
175
+ * @example
176
+ * const dispose = flow.addHooks(
177
+ * { beforeStep: (meta) => console.log("->", meta.label) },
178
+ * ["llm:*"],
179
+ * );
180
+ * await flow.run(shared);
181
+ * dispose(); // removes the hooks
182
+ */
183
+ addHooks(hooks, filter) {
184
+ return this._setHooks(hooks, filter);
185
+ }
159
186
  /**
160
187
  * Register lifecycle hooks (called by plugin methods, not by consumers).
161
188
  * Returns a dispose function that removes these hooks when called.
@@ -1,4 +1,4 @@
1
- import { F as FlowBuilder, a as FlowneerPlugin } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { F as FlowBuilder, a as FlowneerPlugin } from '../../FlowBuilder-CwBQDOEN.js';
2
2
 
3
3
  interface HumanNodeOptions<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { a as FlowneerPlugin, N as NodeFn, b as NodeOptions, S as StepFilter } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { a as FlowneerPlugin, N as NodeFn, b as NodeOptions, S as StepFilter } from '../../FlowBuilder-CwBQDOEN.js';
2
2
 
3
3
  declare module "../../Flowneer" {
4
4
  interface FlowBuilder<S, P> {
@@ -1,4 +1,4 @@
1
- import { F as FlowBuilder } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { F as FlowBuilder } from '../../FlowBuilder-CwBQDOEN.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-8kwREeyD.js';
1
+ import { a as FlowneerPlugin, N as NodeFn, b as NodeOptions } from '../../FlowBuilder-CwBQDOEN.js';
2
2
 
3
3
  /** A single node entry in the exported graph. */
4
4
  interface GraphNodeExport {
@@ -21,15 +21,21 @@ var InterruptError = class extends Error {
21
21
  };
22
22
 
23
23
  // src/core/utils.ts
24
+ function matchesPattern(pattern, label) {
25
+ return pattern.includes("*") ? new RegExp(
26
+ "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
27
+ ).test(label) : pattern === label;
28
+ }
24
29
  function matchesFilter(filter, meta) {
25
30
  if (!Array.isArray(filter)) return filter(meta);
31
+ const negations = filter.filter((p) => p.startsWith("!"));
32
+ const positives = filter.filter((p) => !p.startsWith("!"));
26
33
  const label = meta.label;
27
- if (label === void 0) return false;
28
- return filter.some(
29
- (p) => p.includes("*") ? new RegExp(
30
- "^" + p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
31
- ).test(label) : p === label
32
- );
34
+ if (label !== void 0 && negations.some((p) => matchesPattern(p.slice(1), label)))
35
+ return false;
36
+ if (positives.length > 0)
37
+ return label !== void 0 && positives.some((p) => matchesPattern(p, label));
38
+ return true;
33
39
  }
34
40
  function resolveNumber(val, fallback, shared, params) {
35
41
  if (val === void 0) return fallback;
@@ -156,6 +162,27 @@ var CoreFlowBuilder = class _CoreFlowBuilder {
156
162
  _hooks() {
157
163
  return this._hooksCache ??= buildHookCache(this._hooksList);
158
164
  }
165
+ /**
166
+ * Register lifecycle hooks on this flow instance at runtime.
167
+ *
168
+ * Unlike plugin methods (which use `_setHooks` internally), `addHooks` is
169
+ * intended for consumers — app code, tests, or request-scoped instrumentation
170
+ * that needs to observe or modify a specific flow instance without defining a
171
+ * plugin.
172
+ *
173
+ * Returns a `dispose` function that removes the registered hooks when called.
174
+ *
175
+ * @example
176
+ * const dispose = flow.addHooks(
177
+ * { beforeStep: (meta) => console.log("->", meta.label) },
178
+ * ["llm:*"],
179
+ * );
180
+ * await flow.run(shared);
181
+ * dispose(); // removes the hooks
182
+ */
183
+ addHooks(hooks, filter) {
184
+ return this._setHooks(hooks, filter);
185
+ }
159
186
  /**
160
187
  * Register lifecycle hooks (called by plugin methods, not by consumers).
161
188
  * Returns a dispose function that removes these hooks when called.
@@ -11,7 +11,7 @@ export { parseJsonOutput, parseListOutput, parseMarkdownTable, parseRegexOutput
11
11
  export { Span, TelemetryDaemon, TelemetryExporter, TelemetryOptions, consoleExporter, otlpExporter, withTelemetry } from './telemetry/index.js';
12
12
  export { EvalResult, EvalSummary, ScoreFn, answerRelevance, containsMatch, exactMatch, f1Score, retrievalPrecision, retrievalRecall, runEvalSuite } from './eval/index.js';
13
13
  export { GraphEdge, GraphNode, withGraph } from './graph/index.js';
14
- import { S as StepFilter, a as FlowneerPlugin, d as StepMeta, F as FlowBuilder } from '../FlowBuilder-8kwREeyD.js';
14
+ import { S as StepFilter, a as FlowneerPlugin, d as StepMeta, F as FlowBuilder } from '../FlowBuilder-CwBQDOEN.js';
15
15
  import { F as FlowError } from '../errors-u-hq7p5N.js';
16
16
  export { validate } from './config/index.js';
17
17
  export { F as FlowConfig, a as FnRegistry, S as StepConfig, V as ValidationError, b as ValidationResult } from '../schema-CIqQAXqY.js';
@@ -115,15 +115,21 @@ var InterruptError = class extends Error {
115
115
  };
116
116
 
117
117
  // src/core/utils.ts
118
+ function matchesPattern(pattern, label) {
119
+ return pattern.includes("*") ? new RegExp(
120
+ "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
121
+ ).test(label) : pattern === label;
122
+ }
118
123
  function matchesFilter(filter, meta) {
119
124
  if (!Array.isArray(filter)) return filter(meta);
125
+ const negations = filter.filter((p) => p.startsWith("!"));
126
+ const positives = filter.filter((p) => !p.startsWith("!"));
120
127
  const label = meta.label;
121
- if (label === void 0) return false;
122
- return filter.some(
123
- (p) => p.includes("*") ? new RegExp(
124
- "^" + p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
125
- ).test(label) : p === label
126
- );
128
+ if (label !== void 0 && negations.some((p) => matchesPattern(p.slice(1), label)))
129
+ return false;
130
+ if (positives.length > 0)
131
+ return label !== void 0 && positives.some((p) => matchesPattern(p, label));
132
+ return true;
127
133
  }
128
134
  function resolveNumber(val, fallback, shared, params) {
129
135
  if (val === void 0) return fallback;
@@ -250,6 +256,27 @@ var CoreFlowBuilder = class _CoreFlowBuilder {
250
256
  _hooks() {
251
257
  return this._hooksCache ??= buildHookCache(this._hooksList);
252
258
  }
259
+ /**
260
+ * Register lifecycle hooks on this flow instance at runtime.
261
+ *
262
+ * Unlike plugin methods (which use `_setHooks` internally), `addHooks` is
263
+ * intended for consumers — app code, tests, or request-scoped instrumentation
264
+ * that needs to observe or modify a specific flow instance without defining a
265
+ * plugin.
266
+ *
267
+ * Returns a `dispose` function that removes the registered hooks when called.
268
+ *
269
+ * @example
270
+ * const dispose = flow.addHooks(
271
+ * { beforeStep: (meta) => console.log("->", meta.label) },
272
+ * ["llm:*"],
273
+ * );
274
+ * await flow.run(shared);
275
+ * dispose(); // removes the hooks
276
+ */
277
+ addHooks(hooks, filter) {
278
+ return this._setHooks(hooks, filter);
279
+ }
253
280
  /**
254
281
  * Register lifecycle hooks (called by plugin methods, not by consumers).
255
282
  * Returns a dispose function that removes these hooks when called.
@@ -1,4 +1,4 @@
1
- import { S as StepFilter, a as FlowneerPlugin, V as Validator } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { S as StepFilter, a as FlowneerPlugin, V as Validator } from '../../FlowBuilder-CwBQDOEN.js';
2
2
 
3
3
  declare module "../../Flowneer" {
4
4
  interface FlowBuilder<S, P> {
@@ -1,4 +1,4 @@
1
- import { a as FlowneerPlugin } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { a as FlowneerPlugin } from '../../FlowBuilder-CwBQDOEN.js';
2
2
 
3
3
  /** A single message in conversational memory. */
4
4
  interface MemoryMessage {
@@ -1,4 +1,4 @@
1
- import { a as FlowneerPlugin } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { a as FlowneerPlugin } from '../../FlowBuilder-CwBQDOEN.js';
2
2
 
3
3
  /** Send a message to a named channel on `shared.__channels`. */
4
4
  declare function sendTo<S extends Record<string, any>>(shared: S, channel: string, message: unknown): void;
@@ -1,4 +1,4 @@
1
- import { S as StepFilter, a as FlowneerPlugin, d as StepMeta } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { S as StepFilter, a as FlowneerPlugin, d as StepMeta } from '../../FlowBuilder-CwBQDOEN.js';
2
2
 
3
3
  declare module "../../Flowneer" {
4
4
  interface FlowBuilder<S, P> {
@@ -1,4 +1,4 @@
1
- import { V as Validator } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { V as Validator } from '../../FlowBuilder-CwBQDOEN.js';
2
2
 
3
3
  /**
4
4
  * Extract and parse JSON from a (possibly noisy) LLM response.
@@ -1,4 +1,4 @@
1
- import { d as StepMeta, S as StepFilter, a as FlowneerPlugin } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { d as StepMeta, S as StepFilter, a as FlowneerPlugin } from '../../FlowBuilder-CwBQDOEN.js';
2
2
 
3
3
  /**
4
4
  * Events that can trigger a checkpoint save.
@@ -1,4 +1,4 @@
1
- import { N as NodeFn, S as StepFilter, a as FlowneerPlugin } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { N as NodeFn, S as StepFilter, a as FlowneerPlugin } from '../../FlowBuilder-CwBQDOEN.js';
2
2
 
3
3
  declare module "../../Flowneer" {
4
4
  interface FlowBuilder<S, P> {
@@ -1,4 +1,4 @@
1
- import { c as FlowHooks, a as FlowneerPlugin } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { c as FlowHooks, a as FlowneerPlugin } from '../../FlowBuilder-CwBQDOEN.js';
2
2
 
3
3
  interface Span {
4
4
  traceId: string;
@@ -1,4 +1,4 @@
1
- import { a as FlowneerPlugin } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { a as FlowneerPlugin } from '../../FlowBuilder-CwBQDOEN.js';
2
2
 
3
3
  /** Describes a single parameter for a tool. */
4
4
  interface ToolParam {
@@ -1,4 +1,4 @@
1
- import { a as FlowneerPlugin, N as NodeFn, F as FlowBuilder } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { a as FlowneerPlugin, N as NodeFn, F as FlowBuilder } from '../../FlowBuilder-CwBQDOEN.js';
2
2
  import { ToolCall, ToolResult, ToolParam, Tool, ToolRegistry } from '../../plugins/tools/index.js';
3
3
 
4
4
  /**
@@ -290,6 +290,172 @@ declare function selfConsistency<S = any, P extends Record<string, unknown> = Re
290
290
  */
291
291
  declare function critiqueAndRevise<S = any, P extends Record<string, unknown> = Record<string, unknown>>(generate: NodeFn<S, P>, critique: NodeFn<S, P>, revise: NodeFn<S, P>, rounds?: number): FlowBuilder<S, P>;
292
292
 
293
+ /**
294
+ * A single message in the swarm conversation history.
295
+ * Agents write to `shared.messages` using this shape.
296
+ */
297
+ interface SwarmMessage {
298
+ role: "user" | "assistant";
299
+ content: string;
300
+ /** Name of the agent that produced this message */
301
+ agent?: string;
302
+ }
303
+ /**
304
+ * Fields that `swarm()` reads and writes on shared state.
305
+ * Extend this with your own application fields via intersection:
306
+ *
307
+ * ```typescript
308
+ * type MyState = SwarmState & { topic: string; result?: string };
309
+ * ```
310
+ */
311
+ interface SwarmState {
312
+ /** Name of the agent currently handling the request.
313
+ * Defaults to `options.defaultAgent` on the first `.run()` call. */
314
+ currentAgent?: string;
315
+ /** Conversation history — manage this inside your agent fns. */
316
+ messages?: SwarmMessage[];
317
+ /** Number of handoffs that have occurred in the current `.run()` call. */
318
+ turnCount?: number;
319
+ /** @internal — loop exit sentinel; removed after each `.run()` */
320
+ __swarmDone?: boolean;
321
+ /** @internal — set by `handoffTo()`; consumed by the handoff checker */
322
+ __swarmHandoff?: {
323
+ target: string;
324
+ reason?: string;
325
+ };
326
+ }
327
+ /**
328
+ * A single agent in the swarm.
329
+ * Each agent is a `NodeFn` paired with a name and description.
330
+ */
331
+ interface SwarmAgent<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
332
+ /** Unique name used in `handoffTo()` calls and `SwarmState.currentAgent`. */
333
+ name: string;
334
+ /** Human-readable description (can be provided to an LLM for routing). */
335
+ description: string;
336
+ /** The agent's step function — same signature as any Flowneer `NodeFn`. */
337
+ fn: NodeFn<S, P>;
338
+ }
339
+ /**
340
+ * Options for `swarm()`.
341
+ */
342
+ interface SwarmOptions<S = any> {
343
+ /**
344
+ * Name of the agent that handles the first turn.
345
+ * Must appear in the `agents` array.
346
+ */
347
+ defaultAgent: string;
348
+ /**
349
+ * Maximum number of handoffs per `.run()` call before the flow stops.
350
+ * Counts hops (not total agents); the original agent's first run is free.
351
+ * Defaults to `5`.
352
+ */
353
+ maxHandoffs?: number;
354
+ /**
355
+ * Called each time a handoff is accepted.
356
+ * `from` is the agent that handed off, `to` is the new agent.
357
+ */
358
+ onHandoff?: (from: string, to: string, reason: string | undefined, shared: S) => void | Promise<void>;
359
+ /**
360
+ * Called when `maxHandoffs` is exceeded instead of completing the handoff.
361
+ * The turn ends after this callback returns.
362
+ */
363
+ onMaxHandoffs?: (shared: S) => void | Promise<void>;
364
+ /**
365
+ * Optional LLM router that selects the starting agent on each `.run()` call.
366
+ * Runs once after state initialisation, before the handoff loop begins.
367
+ */
368
+ router?: SwarmRouter<S>;
369
+ }
370
+ /**
371
+ * Context object passed to a {@link SwarmRouter} prompt function.
372
+ */
373
+ interface RouterContext<S = any> {
374
+ /** Full conversation history at the time of routing. */
375
+ messages: SwarmMessage[];
376
+ /** All agents registered in the swarm. */
377
+ agents: SwarmAgent<S, any>[];
378
+ /** Name of the agent that will be used if the router returns an unknown name. */
379
+ currentAgent: string;
380
+ /** Live shared state — mutations here are visible to the dispatched agent. */
381
+ shared: S;
382
+ }
383
+ /**
384
+ * An optional LLM-based router that selects the starting agent for each `.run()` call.
385
+ *
386
+ * @example
387
+ * const flow = swarm(agents, {
388
+ * defaultAgent: "triage",
389
+ * router: {
390
+ * call: (prompt) => openai.chat.completions.create({ ... }).then(r => r.choices[0].message.content!),
391
+ * },
392
+ * });
393
+ */
394
+ interface SwarmRouter<S = any> {
395
+ /**
396
+ * Calls the LLM with the resolved prompt and returns the agent name to start with.
397
+ * The response is trimmed and matched case-insensitively against the agents array.
398
+ * An unrecognised response is silently ignored and `currentAgent` remains unchanged.
399
+ */
400
+ call: (prompt: string) => Promise<string>;
401
+ /**
402
+ * Static prompt string or async function that returns the prompt.
403
+ * When omitted, a default prompt listing all agents and the latest user message is used.
404
+ */
405
+ prompt?: string | ((context: RouterContext<S>) => string | Promise<string>);
406
+ }
407
+ /**
408
+ * Formats a `SwarmMessage[]` into a plain-text string suitable for use in LLM
409
+ * prompts. Each line is `[agentName] role: content`; the `[agentName]` prefix
410
+ * is omitted when `message.agent` is undefined.
411
+ *
412
+ * @example
413
+ * const prompt = `Conversation so far:\n${historyText(shared.messages ?? [])}`;
414
+ */
415
+ declare function historyText(messages: SwarmMessage[]): string;
416
+ /**
417
+ * Signal that control should pass to another agent in the swarm.
418
+ *
419
+ * Call this inside an agent's `fn` to hand off to `agentName`.
420
+ * If the target name is not found in the swarm the handoff is silently dropped
421
+ * and the current turn ends.
422
+ *
423
+ * @example
424
+ * const billingAgent: SwarmAgent<MyState> = {
425
+ * name: "billing",
426
+ * description: "Handles billing and payment queries",
427
+ * fn: async (shared) => {
428
+ * if (!isBillingQuery(shared.messages)) {
429
+ * handoffTo(shared, "support", "not a billing question");
430
+ * return;
431
+ * }
432
+ * shared.messages!.push({ role: "assistant", content: await billingLlm(shared) });
433
+ * },
434
+ * };
435
+ */
436
+ declare function handoffTo(shared: SwarmState, agentName: string, reason?: string): void;
437
+ /**
438
+ * Creates a decentralized swarm of agents that hand off to each other
439
+ * dynamically at runtime.
440
+ *
441
+ * Each agent can call `handoffTo(shared, targetName, reason?)` inside its `fn`
442
+ * to yield control to another agent. The flow loops until either:
443
+ * - An agent finishes without calling `handoffTo`, or
444
+ * - `options.maxHandoffs` is exceeded (default 5) — `onMaxHandoffs` is called.
445
+ *
446
+ * `currentAgent` persists between `.run()` calls so the swarm remembers which
447
+ * agent was active. It is set to `defaultAgent` only on the first call.
448
+ *
449
+ * @example
450
+ * const flow = swarm(
451
+ * [triageAgent, billingAgent, supportAgent],
452
+ * { defaultAgent: "triage" },
453
+ * );
454
+ *
455
+ * await flow.run({ messages: [{ role: "user", content: "I need a refund" }] });
456
+ */
457
+ declare function swarm<S extends SwarmState = SwarmState, P extends Record<string, unknown> = Record<string, unknown>>(agents: SwarmAgent<S, P>[], options: SwarmOptions<S>, FlowClass?: new () => FlowBuilder<S, P>): FlowBuilder<S, P>;
458
+
293
459
  /**
294
460
  * Minimal structural interface that matches a Zod ZodObject.
295
461
  * We duck-type against `.shape` so this plugin has zero Zod dependency —
@@ -438,4 +604,4 @@ interface CreateAgentOptions {
438
604
  */
439
605
  declare function createAgent(options: CreateAgentOptions): FlowBuilder<AgentState>;
440
606
 
441
- export { type AgentState, type ChatMessage, type CreateAgentOptions, type EvaluatorOptimizerOptions, type EvaluatorOptimizerResult, type LlmAdapter, type LlmResponse, type LlmToolDef, type PlanAndExecuteOptions, type ReActLoopOptions, type ReflexionOptions, type ThinkResult, type ToolConfig, type ToolConfigParams, type ToolConfigSchema, type ZodLikeObject, createAgent, critiqueAndRevise, evaluatorOptimizer, hierarchicalCrew, planAndExecute, reflexionAgent, roundRobinDebate, selfConsistency, sequentialCrew, supervisorCrew, tool, withReActLoop };
607
+ export { type AgentState, type ChatMessage, type CreateAgentOptions, type EvaluatorOptimizerOptions, type EvaluatorOptimizerResult, type LlmAdapter, type LlmResponse, type LlmToolDef, type PlanAndExecuteOptions, type ReActLoopOptions, type ReflexionOptions, type RouterContext, type SwarmAgent, type SwarmMessage, type SwarmOptions, type SwarmRouter, type SwarmState, type ThinkResult, type ToolConfig, type ToolConfigParams, type ToolConfigSchema, type ZodLikeObject, createAgent, critiqueAndRevise, evaluatorOptimizer, handoffTo, hierarchicalCrew, historyText, planAndExecute, reflexionAgent, roundRobinDebate, selfConsistency, sequentialCrew, supervisorCrew, swarm, tool, withReActLoop };
@@ -71,15 +71,21 @@ var InterruptError = class extends Error {
71
71
  };
72
72
 
73
73
  // src/core/utils.ts
74
+ function matchesPattern(pattern, label) {
75
+ return pattern.includes("*") ? new RegExp(
76
+ "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
77
+ ).test(label) : pattern === label;
78
+ }
74
79
  function matchesFilter(filter, meta) {
75
80
  if (!Array.isArray(filter)) return filter(meta);
81
+ const negations = filter.filter((p) => p.startsWith("!"));
82
+ const positives = filter.filter((p) => !p.startsWith("!"));
76
83
  const label = meta.label;
77
- if (label === void 0) return false;
78
- return filter.some(
79
- (p) => p.includes("*") ? new RegExp(
80
- "^" + p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
81
- ).test(label) : p === label
82
- );
84
+ if (label !== void 0 && negations.some((p) => matchesPattern(p.slice(1), label)))
85
+ return false;
86
+ if (positives.length > 0)
87
+ return label !== void 0 && positives.some((p) => matchesPattern(p, label));
88
+ return true;
83
89
  }
84
90
  function resolveNumber(val, fallback, shared, params) {
85
91
  if (val === void 0) return fallback;
@@ -206,6 +212,27 @@ var CoreFlowBuilder = class _CoreFlowBuilder {
206
212
  _hooks() {
207
213
  return this._hooksCache ??= buildHookCache(this._hooksList);
208
214
  }
215
+ /**
216
+ * Register lifecycle hooks on this flow instance at runtime.
217
+ *
218
+ * Unlike plugin methods (which use `_setHooks` internally), `addHooks` is
219
+ * intended for consumers — app code, tests, or request-scoped instrumentation
220
+ * that needs to observe or modify a specific flow instance without defining a
221
+ * plugin.
222
+ *
223
+ * Returns a `dispose` function that removes the registered hooks when called.
224
+ *
225
+ * @example
226
+ * const dispose = flow.addHooks(
227
+ * { beforeStep: (meta) => console.log("->", meta.label) },
228
+ * ["llm:*"],
229
+ * );
230
+ * await flow.run(shared);
231
+ * dispose(); // removes the hooks
232
+ */
233
+ addHooks(hooks, filter) {
234
+ return this._setHooks(hooks, filter);
235
+ }
209
236
  /**
210
237
  * Register lifecycle hooks (called by plugin methods, not by consumers).
211
238
  * Returns a dispose function that removes these hooks when called.
@@ -816,6 +843,120 @@ function critiqueAndRevise(generate, critique, revise, rounds = 1) {
816
843
  });
817
844
  }
818
845
 
846
+ // presets/agent/swarm.ts
847
+ function historyText(messages) {
848
+ return messages.map((m) => `${m.agent ? `[${m.agent}] ` : ""}${m.role}: ${m.content}`).join("\n");
849
+ }
850
+ function buildDefaultRouterPrompt(ctx) {
851
+ const agentList = ctx.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
852
+ const history = historyText(ctx.messages);
853
+ const latest = [...ctx.messages].reverse().find((m) => m.role === "user");
854
+ return [
855
+ "You are a routing assistant. Choose the best agent to handle the user's request.",
856
+ "",
857
+ "Available agents:",
858
+ agentList,
859
+ "",
860
+ ...history ? ["Conversation history:", history, ""] : [],
861
+ ...latest ? [`Latest user message: ${latest.content}`, ""] : [],
862
+ "Respond with only the exact agent name, nothing else."
863
+ ].join("\n");
864
+ }
865
+ function handoffTo(shared, agentName, reason) {
866
+ shared.__swarmHandoff = { target: agentName, reason };
867
+ }
868
+ function swarm(agents, options, FlowClass = FlowBuilder) {
869
+ const agentMap = new Map(
870
+ agents.map((a) => [a.name, a])
871
+ );
872
+ if (!agentMap.has(options.defaultAgent)) {
873
+ throw new Error(
874
+ `swarm: defaultAgent "${options.defaultAgent}" not found in agents list. Available agents: ${agents.map((a) => a.name).join(", ")}`
875
+ );
876
+ }
877
+ const maxHandoffs = options.maxHandoffs ?? 5;
878
+ return new FlowClass().startWith(
879
+ (shared) => {
880
+ if (shared.currentAgent === void 0) {
881
+ shared.currentAgent = options.defaultAgent;
882
+ }
883
+ shared.turnCount = 0;
884
+ shared.__swarmDone = false;
885
+ delete shared.__swarmHandoff;
886
+ },
887
+ { label: "swarm:init" }
888
+ ).then(
889
+ async (shared) => {
890
+ if (!options.router) return;
891
+ const ctx = {
892
+ messages: shared.messages ?? [],
893
+ agents,
894
+ currentAgent: shared.currentAgent,
895
+ shared
896
+ };
897
+ const rawPrompt = typeof options.router.prompt === "function" ? await options.router.prompt(ctx) : options.router.prompt ?? buildDefaultRouterPrompt(ctx);
898
+ const response = await options.router.call(rawPrompt);
899
+ const raw = response.trim();
900
+ const match = agents.find(
901
+ (a) => a.name.toLowerCase() === raw.toLowerCase()
902
+ );
903
+ if (match) {
904
+ shared.currentAgent = match.name;
905
+ }
906
+ },
907
+ { label: "swarm:router" }
908
+ ).loop(
909
+ (shared) => !shared.__swarmDone,
910
+ (b) => {
911
+ b.startWith(
912
+ async (shared, params) => {
913
+ const agent = agentMap.get(shared.currentAgent);
914
+ if (!agent) {
915
+ shared.currentAgent = options.defaultAgent;
916
+ shared.__swarmDone = true;
917
+ return;
918
+ }
919
+ delete shared.__swarmHandoff;
920
+ await agent.fn(shared, params);
921
+ },
922
+ { label: "swarm:dispatch" }
923
+ ).then(
924
+ async (shared) => {
925
+ const handoff = shared.__swarmHandoff;
926
+ if (!handoff) {
927
+ shared.__swarmDone = true;
928
+ return;
929
+ }
930
+ if (!agentMap.has(handoff.target)) {
931
+ shared.__swarmDone = true;
932
+ return;
933
+ }
934
+ if ((shared.turnCount ?? 0) >= maxHandoffs) {
935
+ await options.onMaxHandoffs?.(shared);
936
+ shared.__swarmDone = true;
937
+ return;
938
+ }
939
+ await options.onHandoff?.(
940
+ shared.currentAgent,
941
+ handoff.target,
942
+ handoff.reason,
943
+ shared
944
+ );
945
+ shared.turnCount = (shared.turnCount ?? 0) + 1;
946
+ shared.currentAgent = handoff.target;
947
+ },
948
+ { label: "swarm:handoff" }
949
+ );
950
+ },
951
+ { label: "swarm:loop" }
952
+ ).then(
953
+ (shared) => {
954
+ delete shared.__swarmDone;
955
+ },
956
+ { label: "swarm:cleanup" }
957
+ );
958
+ }
959
+
819
960
  // presets/agent/tool.ts
820
961
  function zodTypeToParamType(typeName) {
821
962
  switch (typeName) {
@@ -987,13 +1128,16 @@ export {
987
1128
  createAgent,
988
1129
  critiqueAndRevise,
989
1130
  evaluatorOptimizer,
1131
+ handoffTo,
990
1132
  hierarchicalCrew,
1133
+ historyText,
991
1134
  planAndExecute,
992
1135
  reflexionAgent,
993
1136
  roundRobinDebate,
994
1137
  selfConsistency,
995
1138
  sequentialCrew,
996
1139
  supervisorCrew,
1140
+ swarm,
997
1141
  tool,
998
1142
  withReActLoop
999
1143
  };
@@ -1,5 +1,5 @@
1
1
  import { S as StepConfig, a as FnRegistry, b as ValidationResult, F as FlowConfig } from '../../schema-CIqQAXqY.js';
2
- import { F as FlowBuilder } from '../../FlowBuilder-8kwREeyD.js';
2
+ import { F as FlowBuilder } from '../../FlowBuilder-CwBQDOEN.js';
3
3
 
4
4
  /** Recursive applicator passed to nested step builders (loop body, batch processor). */
5
5
  type ApplyFn = (steps: StepConfig[], flow: FlowBuilder<any, any>, registry: FnRegistry) => void;
@@ -21,15 +21,21 @@ var InterruptError = class extends Error {
21
21
  };
22
22
 
23
23
  // src/core/utils.ts
24
+ function matchesPattern(pattern, label) {
25
+ return pattern.includes("*") ? new RegExp(
26
+ "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
27
+ ).test(label) : pattern === label;
28
+ }
24
29
  function matchesFilter(filter, meta) {
25
30
  if (!Array.isArray(filter)) return filter(meta);
31
+ const negations = filter.filter((p) => p.startsWith("!"));
32
+ const positives = filter.filter((p) => !p.startsWith("!"));
26
33
  const label = meta.label;
27
- if (label === void 0) return false;
28
- return filter.some(
29
- (p) => p.includes("*") ? new RegExp(
30
- "^" + p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
31
- ).test(label) : p === label
32
- );
34
+ if (label !== void 0 && negations.some((p) => matchesPattern(p.slice(1), label)))
35
+ return false;
36
+ if (positives.length > 0)
37
+ return label !== void 0 && positives.some((p) => matchesPattern(p, label));
38
+ return true;
33
39
  }
34
40
  function resolveNumber(val, fallback, shared, params) {
35
41
  if (val === void 0) return fallback;
@@ -156,6 +162,27 @@ var CoreFlowBuilder = class _CoreFlowBuilder {
156
162
  _hooks() {
157
163
  return this._hooksCache ??= buildHookCache(this._hooksList);
158
164
  }
165
+ /**
166
+ * Register lifecycle hooks on this flow instance at runtime.
167
+ *
168
+ * Unlike plugin methods (which use `_setHooks` internally), `addHooks` is
169
+ * intended for consumers — app code, tests, or request-scoped instrumentation
170
+ * that needs to observe or modify a specific flow instance without defining a
171
+ * plugin.
172
+ *
173
+ * Returns a `dispose` function that removes the registered hooks when called.
174
+ *
175
+ * @example
176
+ * const dispose = flow.addHooks(
177
+ * { beforeStep: (meta) => console.log("->", meta.label) },
178
+ * ["llm:*"],
179
+ * );
180
+ * await flow.run(shared);
181
+ * dispose(); // removes the hooks
182
+ */
183
+ addHooks(hooks, filter) {
184
+ return this._setHooks(hooks, filter);
185
+ }
159
186
  /**
160
187
  * Register lifecycle hooks (called by plugin methods, not by consumers).
161
188
  * Returns a dispose function that removes these hooks when called.
@@ -1,7 +1,7 @@
1
- export { AgentState, ChatMessage, CreateAgentOptions, EvaluatorOptimizerOptions, EvaluatorOptimizerResult, LlmAdapter, LlmResponse, LlmToolDef, PlanAndExecuteOptions, ReActLoopOptions, ReflexionOptions, ThinkResult, ToolConfig, ToolConfigParams, ToolConfigSchema, ZodLikeObject, createAgent, critiqueAndRevise, evaluatorOptimizer, hierarchicalCrew, planAndExecute, reflexionAgent, roundRobinDebate, selfConsistency, sequentialCrew, supervisorCrew, tool, withReActLoop } from './agent/index.js';
1
+ export { AgentState, ChatMessage, CreateAgentOptions, EvaluatorOptimizerOptions, EvaluatorOptimizerResult, LlmAdapter, LlmResponse, LlmToolDef, PlanAndExecuteOptions, ReActLoopOptions, ReflexionOptions, RouterContext, SwarmAgent, SwarmMessage, SwarmOptions, SwarmRouter, SwarmState, ThinkResult, ToolConfig, ToolConfigParams, ToolConfigSchema, ZodLikeObject, createAgent, critiqueAndRevise, evaluatorOptimizer, handoffTo, hierarchicalCrew, historyText, planAndExecute, reflexionAgent, roundRobinDebate, selfConsistency, sequentialCrew, supervisorCrew, swarm, tool, withReActLoop } from './agent/index.js';
2
2
  export { ApplyFn, ConfigValidationError, CustomStepBuilder, JsonFlowBuilder, StepConfigBuilder } from './config/index.js';
3
3
  export { IterativeRagOptions, RagOptions, iterativeRag, ragPipeline } from './rag/index.js';
4
4
  export { ApprovalGateOptions, ClarifyLoopOptions, GenerateUntilValidOptions, MapReduceOptions, approvalGate, clarifyLoop, generateUntilValid, mapReduceLlm } from './pipeline/index.js';
5
- import '../FlowBuilder-8kwREeyD.js';
5
+ import '../FlowBuilder-CwBQDOEN.js';
6
6
  import '../plugins/tools/index.js';
7
7
  import '../schema-CIqQAXqY.js';
@@ -71,15 +71,21 @@ var InterruptError = class extends Error {
71
71
  };
72
72
 
73
73
  // src/core/utils.ts
74
+ function matchesPattern(pattern, label) {
75
+ return pattern.includes("*") ? new RegExp(
76
+ "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
77
+ ).test(label) : pattern === label;
78
+ }
74
79
  function matchesFilter(filter, meta) {
75
80
  if (!Array.isArray(filter)) return filter(meta);
81
+ const negations = filter.filter((p) => p.startsWith("!"));
82
+ const positives = filter.filter((p) => !p.startsWith("!"));
76
83
  const label = meta.label;
77
- if (label === void 0) return false;
78
- return filter.some(
79
- (p) => p.includes("*") ? new RegExp(
80
- "^" + p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
81
- ).test(label) : p === label
82
- );
84
+ if (label !== void 0 && negations.some((p) => matchesPattern(p.slice(1), label)))
85
+ return false;
86
+ if (positives.length > 0)
87
+ return label !== void 0 && positives.some((p) => matchesPattern(p, label));
88
+ return true;
83
89
  }
84
90
  function resolveNumber(val, fallback, shared, params) {
85
91
  if (val === void 0) return fallback;
@@ -206,6 +212,27 @@ var CoreFlowBuilder = class _CoreFlowBuilder {
206
212
  _hooks() {
207
213
  return this._hooksCache ??= buildHookCache(this._hooksList);
208
214
  }
215
+ /**
216
+ * Register lifecycle hooks on this flow instance at runtime.
217
+ *
218
+ * Unlike plugin methods (which use `_setHooks` internally), `addHooks` is
219
+ * intended for consumers — app code, tests, or request-scoped instrumentation
220
+ * that needs to observe or modify a specific flow instance without defining a
221
+ * plugin.
222
+ *
223
+ * Returns a `dispose` function that removes the registered hooks when called.
224
+ *
225
+ * @example
226
+ * const dispose = flow.addHooks(
227
+ * { beforeStep: (meta) => console.log("->", meta.label) },
228
+ * ["llm:*"],
229
+ * );
230
+ * await flow.run(shared);
231
+ * dispose(); // removes the hooks
232
+ */
233
+ addHooks(hooks, filter) {
234
+ return this._setHooks(hooks, filter);
235
+ }
209
236
  /**
210
237
  * Register lifecycle hooks (called by plugin methods, not by consumers).
211
238
  * Returns a dispose function that removes these hooks when called.
@@ -816,6 +843,120 @@ function critiqueAndRevise(generate, critique, revise, rounds = 1) {
816
843
  });
817
844
  }
818
845
 
846
+ // presets/agent/swarm.ts
847
+ function historyText(messages) {
848
+ return messages.map((m) => `${m.agent ? `[${m.agent}] ` : ""}${m.role}: ${m.content}`).join("\n");
849
+ }
850
+ function buildDefaultRouterPrompt(ctx) {
851
+ const agentList = ctx.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
852
+ const history = historyText(ctx.messages);
853
+ const latest = [...ctx.messages].reverse().find((m) => m.role === "user");
854
+ return [
855
+ "You are a routing assistant. Choose the best agent to handle the user's request.",
856
+ "",
857
+ "Available agents:",
858
+ agentList,
859
+ "",
860
+ ...history ? ["Conversation history:", history, ""] : [],
861
+ ...latest ? [`Latest user message: ${latest.content}`, ""] : [],
862
+ "Respond with only the exact agent name, nothing else."
863
+ ].join("\n");
864
+ }
865
+ function handoffTo(shared, agentName, reason) {
866
+ shared.__swarmHandoff = { target: agentName, reason };
867
+ }
868
+ function swarm(agents, options, FlowClass = FlowBuilder) {
869
+ const agentMap = new Map(
870
+ agents.map((a) => [a.name, a])
871
+ );
872
+ if (!agentMap.has(options.defaultAgent)) {
873
+ throw new Error(
874
+ `swarm: defaultAgent "${options.defaultAgent}" not found in agents list. Available agents: ${agents.map((a) => a.name).join(", ")}`
875
+ );
876
+ }
877
+ const maxHandoffs = options.maxHandoffs ?? 5;
878
+ return new FlowClass().startWith(
879
+ (shared) => {
880
+ if (shared.currentAgent === void 0) {
881
+ shared.currentAgent = options.defaultAgent;
882
+ }
883
+ shared.turnCount = 0;
884
+ shared.__swarmDone = false;
885
+ delete shared.__swarmHandoff;
886
+ },
887
+ { label: "swarm:init" }
888
+ ).then(
889
+ async (shared) => {
890
+ if (!options.router) return;
891
+ const ctx = {
892
+ messages: shared.messages ?? [],
893
+ agents,
894
+ currentAgent: shared.currentAgent,
895
+ shared
896
+ };
897
+ const rawPrompt = typeof options.router.prompt === "function" ? await options.router.prompt(ctx) : options.router.prompt ?? buildDefaultRouterPrompt(ctx);
898
+ const response = await options.router.call(rawPrompt);
899
+ const raw = response.trim();
900
+ const match = agents.find(
901
+ (a) => a.name.toLowerCase() === raw.toLowerCase()
902
+ );
903
+ if (match) {
904
+ shared.currentAgent = match.name;
905
+ }
906
+ },
907
+ { label: "swarm:router" }
908
+ ).loop(
909
+ (shared) => !shared.__swarmDone,
910
+ (b) => {
911
+ b.startWith(
912
+ async (shared, params) => {
913
+ const agent = agentMap.get(shared.currentAgent);
914
+ if (!agent) {
915
+ shared.currentAgent = options.defaultAgent;
916
+ shared.__swarmDone = true;
917
+ return;
918
+ }
919
+ delete shared.__swarmHandoff;
920
+ await agent.fn(shared, params);
921
+ },
922
+ { label: "swarm:dispatch" }
923
+ ).then(
924
+ async (shared) => {
925
+ const handoff = shared.__swarmHandoff;
926
+ if (!handoff) {
927
+ shared.__swarmDone = true;
928
+ return;
929
+ }
930
+ if (!agentMap.has(handoff.target)) {
931
+ shared.__swarmDone = true;
932
+ return;
933
+ }
934
+ if ((shared.turnCount ?? 0) >= maxHandoffs) {
935
+ await options.onMaxHandoffs?.(shared);
936
+ shared.__swarmDone = true;
937
+ return;
938
+ }
939
+ await options.onHandoff?.(
940
+ shared.currentAgent,
941
+ handoff.target,
942
+ handoff.reason,
943
+ shared
944
+ );
945
+ shared.turnCount = (shared.turnCount ?? 0) + 1;
946
+ shared.currentAgent = handoff.target;
947
+ },
948
+ { label: "swarm:handoff" }
949
+ );
950
+ },
951
+ { label: "swarm:loop" }
952
+ ).then(
953
+ (shared) => {
954
+ delete shared.__swarmDone;
955
+ },
956
+ { label: "swarm:cleanup" }
957
+ );
958
+ }
959
+
819
960
  // presets/agent/tool.ts
820
961
  function zodTypeToParamType(typeName) {
821
962
  switch (typeName) {
@@ -1391,7 +1532,9 @@ export {
1391
1532
  critiqueAndRevise,
1392
1533
  evaluatorOptimizer,
1393
1534
  generateUntilValid,
1535
+ handoffTo,
1394
1536
  hierarchicalCrew,
1537
+ historyText,
1395
1538
  iterativeRag,
1396
1539
  mapReduceLlm,
1397
1540
  planAndExecute,
@@ -1401,6 +1544,7 @@ export {
1401
1544
  selfConsistency,
1402
1545
  sequentialCrew,
1403
1546
  supervisorCrew,
1547
+ swarm,
1404
1548
  tool,
1405
1549
  withReActLoop
1406
1550
  };
@@ -1,4 +1,4 @@
1
- import { N as NodeFn, F as FlowBuilder } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { N as NodeFn, F as FlowBuilder } from '../../FlowBuilder-CwBQDOEN.js';
2
2
 
3
3
  interface GenerateUntilValidOptions<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
4
4
  /**
@@ -21,15 +21,21 @@ var InterruptError = class extends Error {
21
21
  };
22
22
 
23
23
  // src/core/utils.ts
24
+ function matchesPattern(pattern, label) {
25
+ return pattern.includes("*") ? new RegExp(
26
+ "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
27
+ ).test(label) : pattern === label;
28
+ }
24
29
  function matchesFilter(filter, meta) {
25
30
  if (!Array.isArray(filter)) return filter(meta);
31
+ const negations = filter.filter((p) => p.startsWith("!"));
32
+ const positives = filter.filter((p) => !p.startsWith("!"));
26
33
  const label = meta.label;
27
- if (label === void 0) return false;
28
- return filter.some(
29
- (p) => p.includes("*") ? new RegExp(
30
- "^" + p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
31
- ).test(label) : p === label
32
- );
34
+ if (label !== void 0 && negations.some((p) => matchesPattern(p.slice(1), label)))
35
+ return false;
36
+ if (positives.length > 0)
37
+ return label !== void 0 && positives.some((p) => matchesPattern(p, label));
38
+ return true;
33
39
  }
34
40
  function resolveNumber(val, fallback, shared, params) {
35
41
  if (val === void 0) return fallback;
@@ -156,6 +162,27 @@ var CoreFlowBuilder = class _CoreFlowBuilder {
156
162
  _hooks() {
157
163
  return this._hooksCache ??= buildHookCache(this._hooksList);
158
164
  }
165
+ /**
166
+ * Register lifecycle hooks on this flow instance at runtime.
167
+ *
168
+ * Unlike plugin methods (which use `_setHooks` internally), `addHooks` is
169
+ * intended for consumers — app code, tests, or request-scoped instrumentation
170
+ * that needs to observe or modify a specific flow instance without defining a
171
+ * plugin.
172
+ *
173
+ * Returns a `dispose` function that removes the registered hooks when called.
174
+ *
175
+ * @example
176
+ * const dispose = flow.addHooks(
177
+ * { beforeStep: (meta) => console.log("->", meta.label) },
178
+ * ["llm:*"],
179
+ * );
180
+ * await flow.run(shared);
181
+ * dispose(); // removes the hooks
182
+ */
183
+ addHooks(hooks, filter) {
184
+ return this._setHooks(hooks, filter);
185
+ }
159
186
  /**
160
187
  * Register lifecycle hooks (called by plugin methods, not by consumers).
161
188
  * Returns a dispose function that removes these hooks when called.
@@ -1,4 +1,4 @@
1
- import { N as NodeFn, F as FlowBuilder } from '../../FlowBuilder-8kwREeyD.js';
1
+ import { N as NodeFn, F as FlowBuilder } from '../../FlowBuilder-CwBQDOEN.js';
2
2
 
3
3
  interface RagOptions<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
4
4
  /** Retrieves relevant documents/chunks and writes them to shared state. */
@@ -21,15 +21,21 @@ var InterruptError = class extends Error {
21
21
  };
22
22
 
23
23
  // src/core/utils.ts
24
+ function matchesPattern(pattern, label) {
25
+ return pattern.includes("*") ? new RegExp(
26
+ "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
27
+ ).test(label) : pattern === label;
28
+ }
24
29
  function matchesFilter(filter, meta) {
25
30
  if (!Array.isArray(filter)) return filter(meta);
31
+ const negations = filter.filter((p) => p.startsWith("!"));
32
+ const positives = filter.filter((p) => !p.startsWith("!"));
26
33
  const label = meta.label;
27
- if (label === void 0) return false;
28
- return filter.some(
29
- (p) => p.includes("*") ? new RegExp(
30
- "^" + p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
31
- ).test(label) : p === label
32
- );
34
+ if (label !== void 0 && negations.some((p) => matchesPattern(p.slice(1), label)))
35
+ return false;
36
+ if (positives.length > 0)
37
+ return label !== void 0 && positives.some((p) => matchesPattern(p, label));
38
+ return true;
33
39
  }
34
40
  function resolveNumber(val, fallback, shared, params) {
35
41
  if (val === void 0) return fallback;
@@ -156,6 +162,27 @@ var CoreFlowBuilder = class _CoreFlowBuilder {
156
162
  _hooks() {
157
163
  return this._hooksCache ??= buildHookCache(this._hooksList);
158
164
  }
165
+ /**
166
+ * Register lifecycle hooks on this flow instance at runtime.
167
+ *
168
+ * Unlike plugin methods (which use `_setHooks` internally), `addHooks` is
169
+ * intended for consumers — app code, tests, or request-scoped instrumentation
170
+ * that needs to observe or modify a specific flow instance without defining a
171
+ * plugin.
172
+ *
173
+ * Returns a `dispose` function that removes the registered hooks when called.
174
+ *
175
+ * @example
176
+ * const dispose = flow.addHooks(
177
+ * { beforeStep: (meta) => console.log("->", meta.label) },
178
+ * ["llm:*"],
179
+ * );
180
+ * await flow.run(shared);
181
+ * dispose(); // removes the hooks
182
+ */
183
+ addHooks(hooks, filter) {
184
+ return this._setHooks(hooks, filter);
185
+ }
159
186
  /**
160
187
  * Register lifecycle hooks (called by plugin methods, not by consumers).
161
188
  * Returns a dispose function that removes these hooks when called.
@@ -1,5 +1,5 @@
1
- import { F as FlowBuilder } from '../FlowBuilder-8kwREeyD.js';
2
- export { A as AnchorStep, e as AugmentedState, B as BatchStep, f as BranchStep, C as CoreFlowBuilder, D as DagStep, c as FlowHooks, a as FlowneerPlugin, g as FnStep, L as LoopStep, N as NodeFn, b as NodeOptions, h as NumberOrFn, P as ParallelStep, R as ResolvedHooks, i as RunOptions, j as Step, k as StepContext, S as StepFilter, l as StepHandler, d as StepMeta, m as StreamEvent, V as Validator } from '../FlowBuilder-8kwREeyD.js';
1
+ import { F as FlowBuilder } from '../FlowBuilder-CwBQDOEN.js';
2
+ export { A as AnchorStep, e as AugmentedState, B as BatchStep, f as BranchStep, C as CoreFlowBuilder, D as DagStep, c as FlowHooks, a as FlowneerPlugin, g as FnStep, L as LoopStep, N as NodeFn, b as NodeOptions, h as NumberOrFn, P as ParallelStep, i as PluginContext, R as ResolvedHooks, j as RunOptions, k as Step, l as StepContext, S as StepFilter, m as StepHandler, d as StepMeta, n as StreamEvent, V as Validator } from '../FlowBuilder-CwBQDOEN.js';
3
3
  export { F as FlowError, I as InterruptError } from '../errors-u-hq7p5N.js';
4
4
 
5
5
  /**
package/dist/src/index.js CHANGED
@@ -21,15 +21,21 @@ var InterruptError = class extends Error {
21
21
  };
22
22
 
23
23
  // src/core/utils.ts
24
+ function matchesPattern(pattern, label) {
25
+ return pattern.includes("*") ? new RegExp(
26
+ "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
27
+ ).test(label) : pattern === label;
28
+ }
24
29
  function matchesFilter(filter, meta) {
25
30
  if (!Array.isArray(filter)) return filter(meta);
31
+ const negations = filter.filter((p) => p.startsWith("!"));
32
+ const positives = filter.filter((p) => !p.startsWith("!"));
26
33
  const label = meta.label;
27
- if (label === void 0) return false;
28
- return filter.some(
29
- (p) => p.includes("*") ? new RegExp(
30
- "^" + p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
31
- ).test(label) : p === label
32
- );
34
+ if (label !== void 0 && negations.some((p) => matchesPattern(p.slice(1), label)))
35
+ return false;
36
+ if (positives.length > 0)
37
+ return label !== void 0 && positives.some((p) => matchesPattern(p, label));
38
+ return true;
33
39
  }
34
40
  function resolveNumber(val, fallback, shared, params) {
35
41
  if (val === void 0) return fallback;
@@ -156,6 +162,27 @@ var CoreFlowBuilder = class _CoreFlowBuilder {
156
162
  _hooks() {
157
163
  return this._hooksCache ??= buildHookCache(this._hooksList);
158
164
  }
165
+ /**
166
+ * Register lifecycle hooks on this flow instance at runtime.
167
+ *
168
+ * Unlike plugin methods (which use `_setHooks` internally), `addHooks` is
169
+ * intended for consumers — app code, tests, or request-scoped instrumentation
170
+ * that needs to observe or modify a specific flow instance without defining a
171
+ * plugin.
172
+ *
173
+ * Returns a `dispose` function that removes the registered hooks when called.
174
+ *
175
+ * @example
176
+ * const dispose = flow.addHooks(
177
+ * { beforeStep: (meta) => console.log("->", meta.label) },
178
+ * ["llm:*"],
179
+ * );
180
+ * await flow.run(shared);
181
+ * dispose(); // removes the hooks
182
+ */
183
+ addHooks(hooks, filter) {
184
+ return this._setHooks(hooks, filter);
185
+ }
159
186
  /**
160
187
  * Register lifecycle hooks (called by plugin methods, not by consumers).
161
188
  * Returns a dispose function that removes these hooks when called.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowneer",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "Zero-dependency fluent flow builder for AI agents",
5
5
  "license": "MIT",
6
6
  "type": "module",