mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 22:37:29 +00:00
@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>
118 lines
4 KiB
TypeScript
118 lines
4 KiB
TypeScript
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");
|
|
});
|
|
});
|