agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
88 lines • 3.81 kB
JavaScript
/**
* Swarm — multi-agent handoff. At each step, an LLM-driven routing
* decision picks which agent handles the next turn.
*
* Origin: OpenAI Swarm experiment (2024). Useful for specialist
* routing — each agent has a narrow role + its own tools.
*
* Pattern: Factory → produces a `Runner` built from
* `Loop(Conditional(route-to-agent))`.
* Role: patterns/ layer. Pure composition over existing primitives.
* Agent roster is FIXED at build time; the routing decision
* is made at runtime by a consumer-supplied `route(input)`
* function (sync — pure over `{ message }`).
*
* For LLM-driven routing (the classic Swarm style), the consumer
* composes a "router" LLMCall as the first step of each iteration and
* parses its response in their `route()` function.
*/
import { flowChart } from 'footprintjs';
import { RunnerBase } from '../core/RunnerBase.js';
import { Conditional } from '../core-flow/Conditional.js';
import { Loop } from '../core-flow/Loop.js';
/**
* Build a Swarm Runner. Each iteration:
* 1. Router evaluates `route(input)` to pick an agent id.
* 2. Conditional dispatches to that agent's runner.
* 3. Agent's output becomes the next iteration's input.
* Loop halts when `route` returns a halt-sentinel id (or unknown id
* falling to the `done` branch) OR when `maxHandoffs` is reached.
*/
export function swarm(opts) {
if (opts.agents.length < 2) {
throw new Error('Swarm: must have >= 2 agents (use Agent for 1)');
}
const routeFn = opts.route;
const ids = new Set(opts.agents.map((a) => a.id));
if (ids.has('done')) {
throw new Error('Swarm: agent id "done" is reserved for the halt branch — rename to avoid collision');
}
const maxHandoffs = opts.maxHandoffs ?? 10;
// Build the routing conditional. One `.when(id, ...)` per agent; the
// routing function's return-value drives branch selection.
let conditional = Conditional.create({ id: 'route' });
for (const agent of opts.agents) {
conditional = conditional.when(agent.id, (input) => routeFn(input) === agent.id, agent.runner, agent.name ?? agent.id);
}
const routing = conditional.otherwise('done', new IdentityRunner()).build();
// Wrap in Loop so handoffs iterate. Exit guard: when `route` returns
// a halt sentinel, the next `Conditional.when` evaluation fires the
// `done` fallback; the body returns the identity output. To stop the
// loop at that point we use the `until` guard inspecting whether the
// current message was produced by the identity path — heuristically,
// we halt when `route(latestOutput)` returns undefined or 'done'.
return Loop.create({
id: opts.id ?? 'swarm',
name: opts.name ?? 'Swarm',
})
.repeat(routing)
.times(maxHandoffs)
.until(({ latestOutput }) => {
const next = routeFn({ message: latestOutput });
return next === undefined || next === 'done' || !ids.has(next);
})
.build();
}
/**
* Identity runner — echoes its input message unchanged. Used as the
* Swarm's `otherwise` fallback so the loop can exit cleanly when the
* router returns a halt sentinel. Not exported (internal-only).
*/
class IdentityRunner extends RunnerBase {
id = 'done';
name = 'Done';
getSpec() {
return flowChart('Done', (scope) => {
scope.echoedMessage = scope.$getArgs().message;
}, 'done-seed')
.addFunction('Return', (scope) => scope.echoedMessage, 'done-return')
.build();
}
async run(input) {
return input.message;
}
async resume() {
throw new Error('Swarm IdentityRunner does not support pause/resume');
}
}
//# sourceMappingURL=Swarm.js.map