logicsrc/packages/agentswarm/src/handler.ts
Anthony Ettinger 018a583a00 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>
2026-06-25 17:41:49 +00:00

81 lines
2.8 KiB
TypeScript

import type { SwarmRunInput, SwarmRunner } from "./types.js";
const CORS_HEADERS: Record<string, string> = {
"access-control-allow-origin": "*",
"access-control-allow-methods": "POST, OPTIONS",
"access-control-allow-headers": "content-type, authorization"
};
export interface SwarmHandlerOptions {
/** Engine that runs an agent turn (e.g. the deepagents-backed runner). */
runner: SwarmRunner;
/**
* Optional per-request gate, run before the agent. Use it to authenticate,
* authorize, or meter a call (e.g. enforce an x402 payment). Throw to reject;
* attach a numeric `status` to the error (see {@link SwarmError}) to control
* the HTTP status returned.
*/
onRequest?: (input: SwarmRunInput, request: Request) => void | Promise<void>;
}
function json(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json", ...CORS_HEADERS }
});
}
function statusOf(error: unknown, fallback: number): number {
const status = (error as { status?: unknown })?.status;
return typeof status === "number" ? status : fallback;
}
function messageOf(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
/**
* Build a framework-agnostic Web handler `(Request) => Response` that runs a
* swarm turn. Mount it on any route in the host app — for example a Next route
* handler (`export const POST = createSwarmHandler({ runner })`), a Hono route,
* or `tronbrowser.dev/swarm`. The host owns hosting, model keys, storage, and
* billing; this just turns an HTTP request into a {@link SwarmRunner} call.
*/
export function createSwarmHandler(options: SwarmHandlerOptions): (request: Request) => Promise<Response> {
const { runner, onRequest } = options;
return async function handle(request: Request): Promise<Response> {
if (request.method === "OPTIONS") {
return new Response(null, { status: 204, headers: CORS_HEADERS });
}
if (request.method !== "POST") {
return json({ error: "method_not_allowed" }, 405);
}
let input: SwarmRunInput;
try {
const body = (await request.json()) as Partial<SwarmRunInput>;
if (!Array.isArray(body.messages) || body.messages.length === 0) {
return json({ error: "messages must be a non-empty array" }, 400);
}
input = { messages: body.messages, rubric: body.rubric, threadId: body.threadId };
} catch {
return json({ error: "invalid_json" }, 400);
}
if (onRequest) {
try {
await onRequest(input, request);
} catch (error) {
return json({ error: messageOf(error) }, statusOf(error, 403));
}
}
try {
const result = await runner.run(input);
return json(result, 200);
} catch (error) {
return json({ error: messageOf(error) }, statusOf(error, 500));
}
};
}