feat(agentswarm): M1+M3 — embeddable swarm runtime wrapping deepagents

@logicsrc/agentswarm: a self-hosted, framework-agnostic multi-agent runtime
that each app mounts on its own route (e.g. tronbrowser.dev/swarm).

M1 — core:
- createSwarmHandler(): Web (Request)=>Response handler, CORS + validation
- SwarmRunner: injectable engine interface
- createDeepAgentRunner(): deepagents (createDeepAgent) adapter; deepagents +
  @langchain/langgraph are optional peers loaded via dynamic import so the core
  builds/tests with zero heavy deps
- SwarmError + onRequest gate: seam for x402 metering / auth (402/403)

M3 — rubric self-check (deepagents RubricMiddleware is Python-only, so ported
at the runner layer):
- createRubricRunner(): grades output via an injectable judge and revises until
  it passes or maxIterations; passes through untouched when no rubric
- createLLMJudge(): cheap-model grader (lazy langchain initChatModel)

13/13 vitest pass; tsc clean; examples/server.mjs demo verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-06-25 17:41:49 +00:00
parent ffe37a01ce
commit 018a583a00
13 changed files with 805 additions and 1 deletions

View file

@ -0,0 +1,118 @@
import { describe, expect, it, vi } from "vitest";
import { SwarmError, createSwarmHandler } from "./index.js";
import type { SwarmRunInput, SwarmRunResult, SwarmRunner } from "./index.js";
function mockRunner(impl?: (input: SwarmRunInput) => SwarmRunResult): SwarmRunner {
return {
run: vi.fn(async (input: SwarmRunInput): Promise<SwarmRunResult> =>
impl
? impl(input)
: {
threadId: input.threadId ?? "thread_test",
messages: [...input.messages, { role: "assistant", content: "ok" }],
output: "ok"
}
)
};
}
function post(body: unknown): Request {
return new Request("http://localhost/swarm", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body)
});
}
describe("createSwarmHandler", () => {
it("runs a turn and returns the result", async () => {
const handle = createSwarmHandler({ runner: mockRunner() });
const res = await handle(post({ messages: [{ role: "user", content: "hi" }] }));
expect(res.status).toBe(200);
const body = await res.json();
expect(body.output).toBe("ok");
expect(body.threadId).toBe("thread_test");
});
it("passes rubric and threadId through to the runner", async () => {
const runner = mockRunner((input) => ({
threadId: input.threadId!,
messages: input.messages,
output: input.rubric ?? ""
}));
const handle = createSwarmHandler({ runner });
const res = await handle(
post({ messages: [{ role: "user", content: "hi" }], rubric: "be terse", threadId: "t1" })
);
const body = await res.json();
expect(body.threadId).toBe("t1");
expect(body.output).toBe("be terse");
});
it("rejects non-POST with 405", async () => {
const handle = createSwarmHandler({ runner: mockRunner() });
const res = await handle(new Request("http://localhost/swarm", { method: "GET" }));
expect(res.status).toBe(405);
});
it("answers a CORS preflight with 204", async () => {
const handle = createSwarmHandler({ runner: mockRunner() });
const res = await handle(new Request("http://localhost/swarm", { method: "OPTIONS" }));
expect(res.status).toBe(204);
expect(res.headers.get("access-control-allow-origin")).toBe("*");
});
it("400s on empty messages", async () => {
const handle = createSwarmHandler({ runner: mockRunner() });
const res = await handle(post({ messages: [] }));
expect(res.status).toBe(400);
});
it("400s on invalid JSON", async () => {
const handle = createSwarmHandler({ runner: mockRunner() });
const res = await handle(
new Request("http://localhost/swarm", {
method: "POST",
headers: { "content-type": "application/json" },
body: "{not json"
})
);
expect(res.status).toBe(400);
});
it("maps an onRequest SwarmError to its status (e.g. 402 payment required)", async () => {
const handle = createSwarmHandler({
runner: mockRunner(),
onRequest: () => {
throw new SwarmError("payment required", 402);
}
});
const res = await handle(post({ messages: [{ role: "user", content: "hi" }] }));
expect(res.status).toBe(402);
expect((await res.json()).error).toBe("payment required");
});
it("does not call the runner when onRequest rejects", async () => {
const runner = mockRunner();
const handle = createSwarmHandler({
runner,
onRequest: () => {
throw new SwarmError("nope", 403);
}
});
await handle(post({ messages: [{ role: "user", content: "hi" }] }));
expect(runner.run).not.toHaveBeenCalled();
});
it("maps a runner failure to 500", async () => {
const runner: SwarmRunner = {
run: vi.fn(async () => {
throw new Error("boom");
})
};
const handle = createSwarmHandler({ runner });
const res = await handle(post({ messages: [{ role: "user", content: "hi" }] }));
expect(res.status).toBe(500);
expect((await res.json()).error).toBe("boom");
});
});