mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-02 04:43:58 +00:00
createSwarm() composes named agents into one SwarmRunner: a router picks the first agent; any agent hands control to a named peer via `HANDOFF: <name>`, bounded by maxHandoffs. createLLMRouter() is a cheap-model router. Composes with the rubric loop and HTTP handler since a swarm is itself a SwarmRunner. 6 new tests (routing, handoff, maxHandoffs cap, unknown-peer/unknown-route fallbacks, empty-agents guard); 19/19 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
86 lines
2.9 KiB
TypeScript
86 lines
2.9 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { createSwarm } from "./index.js";
|
|
import type { SwarmAgent, SwarmRouter, SwarmRunner } from "./index.js";
|
|
|
|
/** A runner that always emits the same output (optionally containing a HANDOFF). */
|
|
function fixedRunner(output: string): SwarmRunner {
|
|
return {
|
|
run: vi.fn(async (input) => ({
|
|
threadId: input.threadId ?? "t",
|
|
messages: [...input.messages, { role: "assistant" as const, content: output }],
|
|
output
|
|
}))
|
|
};
|
|
}
|
|
|
|
function agent(name: string, output: string): SwarmAgent {
|
|
return { name, description: `${name} agent`, runner: fixedRunner(output) };
|
|
}
|
|
|
|
function fixedRouter(name: string): SwarmRouter {
|
|
return { route: vi.fn(async () => name) };
|
|
}
|
|
|
|
const ask = { messages: [{ role: "user" as const, content: "do it" }] };
|
|
|
|
describe("createSwarm", () => {
|
|
it("routes to the chosen agent and returns its answer", async () => {
|
|
const swarm = createSwarm({
|
|
agents: [agent("alpha", "alpha done"), agent("beta", "beta done")],
|
|
router: fixedRouter("beta")
|
|
});
|
|
const res = await swarm.run(ask);
|
|
expect(res.output).toBe("beta done");
|
|
});
|
|
|
|
it("hands off to a named peer and returns the peer's answer", async () => {
|
|
const handoffs: string[] = [];
|
|
const swarm = createSwarm({
|
|
agents: [agent("router", "HANDOFF: worker"), agent("worker", "worker finished")],
|
|
router: fixedRouter("router"),
|
|
onHandoff: (h) => handoffs.push(`${h.from}->${h.to}`)
|
|
});
|
|
const res = await swarm.run(ask);
|
|
expect(handoffs).toEqual(["router->worker"]);
|
|
expect(res.output).toBe("worker finished");
|
|
});
|
|
|
|
it("stops at maxHandoffs when agents keep handing off", async () => {
|
|
const onHandoff = vi.fn();
|
|
const swarm = createSwarm({
|
|
agents: [agent("ping", "HANDOFF: pong"), agent("pong", "HANDOFF: ping")],
|
|
router: fixedRouter("ping"),
|
|
maxHandoffs: 2,
|
|
onHandoff
|
|
});
|
|
const res = await swarm.run(ask);
|
|
// hop 0 ping->pong, hop 1 pong->ping, hop 2 ping reached maxHandoffs -> return
|
|
expect(onHandoff).toHaveBeenCalledTimes(2);
|
|
expect(res.output).toContain("HANDOFF");
|
|
});
|
|
|
|
it("ignores a handoff to an unknown agent", async () => {
|
|
const onHandoff = vi.fn();
|
|
const swarm = createSwarm({
|
|
agents: [agent("solo", "HANDOFF: ghost")],
|
|
router: fixedRouter("solo"),
|
|
onHandoff
|
|
});
|
|
const res = await swarm.run(ask);
|
|
expect(onHandoff).not.toHaveBeenCalled();
|
|
expect(res.output).toBe("HANDOFF: ghost");
|
|
});
|
|
|
|
it("falls back to the first agent when the router names an unknown one", async () => {
|
|
const swarm = createSwarm({
|
|
agents: [agent("first", "first done"), agent("second", "second done")],
|
|
router: fixedRouter("does-not-exist")
|
|
});
|
|
const res = await swarm.run(ask);
|
|
expect(res.output).toBe("first done");
|
|
});
|
|
|
|
it("throws when constructed with no agents", () => {
|
|
expect(() => createSwarm({ agents: [], router: fixedRouter("x") })).toThrow();
|
|
});
|
|
});
|