mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-14 06:47:28 +00:00
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:
parent
ffe37a01ce
commit
018a583a00
13 changed files with 805 additions and 1 deletions
81
packages/agentswarm/src/handler.ts
Normal file
81
packages/agentswarm/src/handler.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
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));
|
||||
}
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue