flowneer 0.9.4 → 0.9.5

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
  */
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, i as RunOptions, d as StepMeta, m as StreamEvent, V as Validator } from './FlowBuilder-Bvf_nDeb.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;
@@ -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-Bvf_nDeb.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-Bvf_nDeb.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-Bvf_nDeb.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-Bvf_nDeb.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;
@@ -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-Bvf_nDeb.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;
@@ -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-Bvf_nDeb.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-Bvf_nDeb.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-Bvf_nDeb.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-Bvf_nDeb.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-Bvf_nDeb.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-Bvf_nDeb.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-Bvf_nDeb.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-Bvf_nDeb.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-Bvf_nDeb.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-Bvf_nDeb.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;
@@ -816,6 +822,120 @@ function critiqueAndRevise(generate, critique, revise, rounds = 1) {
816
822
  });
817
823
  }
818
824
 
825
+ // presets/agent/swarm.ts
826
+ function historyText(messages) {
827
+ return messages.map((m) => `${m.agent ? `[${m.agent}] ` : ""}${m.role}: ${m.content}`).join("\n");
828
+ }
829
+ function buildDefaultRouterPrompt(ctx) {
830
+ const agentList = ctx.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
831
+ const history = historyText(ctx.messages);
832
+ const latest = [...ctx.messages].reverse().find((m) => m.role === "user");
833
+ return [
834
+ "You are a routing assistant. Choose the best agent to handle the user's request.",
835
+ "",
836
+ "Available agents:",
837
+ agentList,
838
+ "",
839
+ ...history ? ["Conversation history:", history, ""] : [],
840
+ ...latest ? [`Latest user message: ${latest.content}`, ""] : [],
841
+ "Respond with only the exact agent name, nothing else."
842
+ ].join("\n");
843
+ }
844
+ function handoffTo(shared, agentName, reason) {
845
+ shared.__swarmHandoff = { target: agentName, reason };
846
+ }
847
+ function swarm(agents, options, FlowClass = FlowBuilder) {
848
+ const agentMap = new Map(
849
+ agents.map((a) => [a.name, a])
850
+ );
851
+ if (!agentMap.has(options.defaultAgent)) {
852
+ throw new Error(
853
+ `swarm: defaultAgent "${options.defaultAgent}" not found in agents list. Available agents: ${agents.map((a) => a.name).join(", ")}`
854
+ );
855
+ }
856
+ const maxHandoffs = options.maxHandoffs ?? 5;
857
+ return new FlowClass().startWith(
858
+ (shared) => {
859
+ if (shared.currentAgent === void 0) {
860
+ shared.currentAgent = options.defaultAgent;
861
+ }
862
+ shared.turnCount = 0;
863
+ shared.__swarmDone = false;
864
+ delete shared.__swarmHandoff;
865
+ },
866
+ { label: "swarm:init" }
867
+ ).then(
868
+ async (shared) => {
869
+ if (!options.router) return;
870
+ const ctx = {
871
+ messages: shared.messages ?? [],
872
+ agents,
873
+ currentAgent: shared.currentAgent,
874
+ shared
875
+ };
876
+ const rawPrompt = typeof options.router.prompt === "function" ? await options.router.prompt(ctx) : options.router.prompt ?? buildDefaultRouterPrompt(ctx);
877
+ const response = await options.router.call(rawPrompt);
878
+ const raw = response.trim();
879
+ const match = agents.find(
880
+ (a) => a.name.toLowerCase() === raw.toLowerCase()
881
+ );
882
+ if (match) {
883
+ shared.currentAgent = match.name;
884
+ }
885
+ },
886
+ { label: "swarm:router" }
887
+ ).loop(
888
+ (shared) => !shared.__swarmDone,
889
+ (b) => {
890
+ b.startWith(
891
+ async (shared, params) => {
892
+ const agent = agentMap.get(shared.currentAgent);
893
+ if (!agent) {
894
+ shared.currentAgent = options.defaultAgent;
895
+ shared.__swarmDone = true;
896
+ return;
897
+ }
898
+ delete shared.__swarmHandoff;
899
+ await agent.fn(shared, params);
900
+ },
901
+ { label: "swarm:dispatch" }
902
+ ).then(
903
+ async (shared) => {
904
+ const handoff = shared.__swarmHandoff;
905
+ if (!handoff) {
906
+ shared.__swarmDone = true;
907
+ return;
908
+ }
909
+ if (!agentMap.has(handoff.target)) {
910
+ shared.__swarmDone = true;
911
+ return;
912
+ }
913
+ if ((shared.turnCount ?? 0) >= maxHandoffs) {
914
+ await options.onMaxHandoffs?.(shared);
915
+ shared.__swarmDone = true;
916
+ return;
917
+ }
918
+ await options.onHandoff?.(
919
+ shared.currentAgent,
920
+ handoff.target,
921
+ handoff.reason,
922
+ shared
923
+ );
924
+ shared.turnCount = (shared.turnCount ?? 0) + 1;
925
+ shared.currentAgent = handoff.target;
926
+ },
927
+ { label: "swarm:handoff" }
928
+ );
929
+ },
930
+ { label: "swarm:loop" }
931
+ ).then(
932
+ (shared) => {
933
+ delete shared.__swarmDone;
934
+ },
935
+ { label: "swarm:cleanup" }
936
+ );
937
+ }
938
+
819
939
  // presets/agent/tool.ts
820
940
  function zodTypeToParamType(typeName) {
821
941
  switch (typeName) {
@@ -987,13 +1107,16 @@ export {
987
1107
  createAgent,
988
1108
  critiqueAndRevise,
989
1109
  evaluatorOptimizer,
1110
+ handoffTo,
990
1111
  hierarchicalCrew,
1112
+ historyText,
991
1113
  planAndExecute,
992
1114
  reflexionAgent,
993
1115
  roundRobinDebate,
994
1116
  selfConsistency,
995
1117
  sequentialCrew,
996
1118
  supervisorCrew,
1119
+ swarm,
997
1120
  tool,
998
1121
  withReActLoop
999
1122
  };
@@ -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-Bvf_nDeb.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;
@@ -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-Bvf_nDeb.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;
@@ -816,6 +822,120 @@ function critiqueAndRevise(generate, critique, revise, rounds = 1) {
816
822
  });
817
823
  }
818
824
 
825
+ // presets/agent/swarm.ts
826
+ function historyText(messages) {
827
+ return messages.map((m) => `${m.agent ? `[${m.agent}] ` : ""}${m.role}: ${m.content}`).join("\n");
828
+ }
829
+ function buildDefaultRouterPrompt(ctx) {
830
+ const agentList = ctx.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
831
+ const history = historyText(ctx.messages);
832
+ const latest = [...ctx.messages].reverse().find((m) => m.role === "user");
833
+ return [
834
+ "You are a routing assistant. Choose the best agent to handle the user's request.",
835
+ "",
836
+ "Available agents:",
837
+ agentList,
838
+ "",
839
+ ...history ? ["Conversation history:", history, ""] : [],
840
+ ...latest ? [`Latest user message: ${latest.content}`, ""] : [],
841
+ "Respond with only the exact agent name, nothing else."
842
+ ].join("\n");
843
+ }
844
+ function handoffTo(shared, agentName, reason) {
845
+ shared.__swarmHandoff = { target: agentName, reason };
846
+ }
847
+ function swarm(agents, options, FlowClass = FlowBuilder) {
848
+ const agentMap = new Map(
849
+ agents.map((a) => [a.name, a])
850
+ );
851
+ if (!agentMap.has(options.defaultAgent)) {
852
+ throw new Error(
853
+ `swarm: defaultAgent "${options.defaultAgent}" not found in agents list. Available agents: ${agents.map((a) => a.name).join(", ")}`
854
+ );
855
+ }
856
+ const maxHandoffs = options.maxHandoffs ?? 5;
857
+ return new FlowClass().startWith(
858
+ (shared) => {
859
+ if (shared.currentAgent === void 0) {
860
+ shared.currentAgent = options.defaultAgent;
861
+ }
862
+ shared.turnCount = 0;
863
+ shared.__swarmDone = false;
864
+ delete shared.__swarmHandoff;
865
+ },
866
+ { label: "swarm:init" }
867
+ ).then(
868
+ async (shared) => {
869
+ if (!options.router) return;
870
+ const ctx = {
871
+ messages: shared.messages ?? [],
872
+ agents,
873
+ currentAgent: shared.currentAgent,
874
+ shared
875
+ };
876
+ const rawPrompt = typeof options.router.prompt === "function" ? await options.router.prompt(ctx) : options.router.prompt ?? buildDefaultRouterPrompt(ctx);
877
+ const response = await options.router.call(rawPrompt);
878
+ const raw = response.trim();
879
+ const match = agents.find(
880
+ (a) => a.name.toLowerCase() === raw.toLowerCase()
881
+ );
882
+ if (match) {
883
+ shared.currentAgent = match.name;
884
+ }
885
+ },
886
+ { label: "swarm:router" }
887
+ ).loop(
888
+ (shared) => !shared.__swarmDone,
889
+ (b) => {
890
+ b.startWith(
891
+ async (shared, params) => {
892
+ const agent = agentMap.get(shared.currentAgent);
893
+ if (!agent) {
894
+ shared.currentAgent = options.defaultAgent;
895
+ shared.__swarmDone = true;
896
+ return;
897
+ }
898
+ delete shared.__swarmHandoff;
899
+ await agent.fn(shared, params);
900
+ },
901
+ { label: "swarm:dispatch" }
902
+ ).then(
903
+ async (shared) => {
904
+ const handoff = shared.__swarmHandoff;
905
+ if (!handoff) {
906
+ shared.__swarmDone = true;
907
+ return;
908
+ }
909
+ if (!agentMap.has(handoff.target)) {
910
+ shared.__swarmDone = true;
911
+ return;
912
+ }
913
+ if ((shared.turnCount ?? 0) >= maxHandoffs) {
914
+ await options.onMaxHandoffs?.(shared);
915
+ shared.__swarmDone = true;
916
+ return;
917
+ }
918
+ await options.onHandoff?.(
919
+ shared.currentAgent,
920
+ handoff.target,
921
+ handoff.reason,
922
+ shared
923
+ );
924
+ shared.turnCount = (shared.turnCount ?? 0) + 1;
925
+ shared.currentAgent = handoff.target;
926
+ },
927
+ { label: "swarm:handoff" }
928
+ );
929
+ },
930
+ { label: "swarm:loop" }
931
+ ).then(
932
+ (shared) => {
933
+ delete shared.__swarmDone;
934
+ },
935
+ { label: "swarm:cleanup" }
936
+ );
937
+ }
938
+
819
939
  // presets/agent/tool.ts
820
940
  function zodTypeToParamType(typeName) {
821
941
  switch (typeName) {
@@ -1391,7 +1511,9 @@ export {
1391
1511
  critiqueAndRevise,
1392
1512
  evaluatorOptimizer,
1393
1513
  generateUntilValid,
1514
+ handoffTo,
1394
1515
  hierarchicalCrew,
1516
+ historyText,
1395
1517
  iterativeRag,
1396
1518
  mapReduceLlm,
1397
1519
  planAndExecute,
@@ -1401,6 +1523,7 @@ export {
1401
1523
  selfConsistency,
1402
1524
  sequentialCrew,
1403
1525
  supervisorCrew,
1526
+ swarm,
1404
1527
  tool,
1405
1528
  withReActLoop
1406
1529
  };
@@ -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-Bvf_nDeb.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;
@@ -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-Bvf_nDeb.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;
@@ -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-Bvf_nDeb.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-Bvf_nDeb.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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowneer",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "Zero-dependency fluent flow builder for AI agents",
5
5
  "license": "MIT",
6
6
  "type": "module",