mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-14 06:47:28 +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>
107 lines
3.6 KiB
TypeScript
107 lines
3.6 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { createRubricRunner } from "./index.js";
|
|
import type { RubricEvaluation, RubricJudge, SwarmRunInput, SwarmRunner } from "./index.js";
|
|
|
|
/** A runner that labels each answer with its attempt number, recording its calls. */
|
|
function scriptedRunner(): { runner: SwarmRunner; calls: SwarmRunInput[] } {
|
|
const calls: SwarmRunInput[] = [];
|
|
let attempt = 0;
|
|
const runner: SwarmRunner = {
|
|
run: vi.fn(async (input: SwarmRunInput) => {
|
|
calls.push(input);
|
|
attempt += 1;
|
|
const output = `attempt ${attempt}`;
|
|
return {
|
|
threadId: input.threadId ?? "t",
|
|
messages: [...input.messages, { role: "assistant" as const, content: output }],
|
|
output
|
|
};
|
|
})
|
|
};
|
|
return { runner, calls };
|
|
}
|
|
|
|
/** A judge that fails until the Nth evaluation, then passes. */
|
|
function judgePassingOn(n: number): RubricJudge {
|
|
let count = 0;
|
|
return {
|
|
evaluate: async () => {
|
|
count += 1;
|
|
return count >= n
|
|
? { passed: true, explanation: "ok" }
|
|
: { passed: false, explanation: "missing criterion X" };
|
|
}
|
|
};
|
|
}
|
|
|
|
describe("createRubricRunner", () => {
|
|
it("passes through untouched when there is no rubric", async () => {
|
|
const { runner, calls } = scriptedRunner();
|
|
const judge = { evaluate: vi.fn() };
|
|
const rr = createRubricRunner({ runner, judge });
|
|
|
|
const res = await rr.run({ messages: [{ role: "user", content: "hi" }] });
|
|
|
|
expect(judge.evaluate).not.toHaveBeenCalled();
|
|
expect(calls).toHaveLength(1);
|
|
expect(res.output).toBe("attempt 1");
|
|
});
|
|
|
|
it("returns the first answer when it passes immediately", async () => {
|
|
const { runner, calls } = scriptedRunner();
|
|
const evals: RubricEvaluation[] = [];
|
|
const rr = createRubricRunner({
|
|
runner,
|
|
judge: judgePassingOn(1),
|
|
onEvaluation: (e) => evals.push(e)
|
|
});
|
|
|
|
const res = await rr.run({ messages: [{ role: "user", content: "hi" }], rubric: "r" });
|
|
|
|
expect(calls).toHaveLength(1);
|
|
expect(evals).toEqual([{ iteration: 1, passed: true, explanation: "ok" }]);
|
|
expect(res.output).toBe("attempt 1");
|
|
});
|
|
|
|
it("revises until the answer passes", async () => {
|
|
const { runner, calls } = scriptedRunner();
|
|
const evals: RubricEvaluation[] = [];
|
|
const rr = createRubricRunner({
|
|
runner,
|
|
judge: judgePassingOn(2),
|
|
onEvaluation: (e) => evals.push(e)
|
|
});
|
|
|
|
const res = await rr.run({ messages: [{ role: "user", content: "hi" }], rubric: "meets X" });
|
|
|
|
expect(calls).toHaveLength(2);
|
|
expect(evals.map((e) => e.passed)).toEqual([false, true]);
|
|
expect(res.output).toBe("attempt 2");
|
|
|
|
// The retry replays history plus a revision message citing the rubric.
|
|
const lastUserMsg = calls[1].messages.filter((m) => m.role === "user").at(-1);
|
|
expect(lastUserMsg?.content).toContain("rubric");
|
|
expect(lastUserMsg?.content).toContain("missing criterion X");
|
|
expect(calls[1].rubric).toBe("meets X");
|
|
expect(calls[1].threadId).toBe("t");
|
|
});
|
|
|
|
it("returns best effort after maxIterations without passing", async () => {
|
|
const { runner, calls } = scriptedRunner();
|
|
const evals: RubricEvaluation[] = [];
|
|
const rr = createRubricRunner({
|
|
runner,
|
|
judge: judgePassingOn(99),
|
|
maxIterations: 2,
|
|
onEvaluation: (e) => evals.push(e)
|
|
});
|
|
|
|
const res = await rr.run({ messages: [{ role: "user", content: "hi" }], rubric: "r" });
|
|
|
|
expect(calls).toHaveLength(2);
|
|
expect(evals).toHaveLength(2);
|
|
expect(evals.every((e) => !e.passed)).toBe(true);
|
|
expect(evals.map((e) => e.iteration)).toEqual([1, 2]);
|
|
expect(res.output).toBe("attempt 2");
|
|
});
|
|
});
|