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,87 @@
import type { SwarmMessage, SwarmRunInput, SwarmRunResult, SwarmRunner } from "./types.js";
export interface DeepAgentRunnerOptions {
/** Provider-prefixed model id, e.g. "anthropic:claude-sonnet-4-6". */
model: string;
/** System instructions for the lead agent. */
instructions?: string;
/**
* LangGraph checkpointer for thread persistence. Defaults to an in-memory
* `MemorySaver`. Swap for a Turso/SQLite-backed saver to persist threads
* across restarts (the tronbrowser BYO-SQLite path).
*/
checkpointer?: unknown;
/** Extra params forwarded to deepagents `createDeepAgent` (subagents, middleware, …). */
agentParams?: Record<string, unknown>;
}
/**
* Indirection so TypeScript does not statically resolve the optional peers
* (`deepagents`, `@langchain/langgraph`). They are only required by hosts that
* use the real runner; the core package and its tests build without them.
*/
async function loadModule(specifier: string): Promise<any> {
return import(specifier);
}
function newThreadId(): string {
return `thread_${crypto.randomUUID()}`;
}
/** Map a LangChain message object to a plain {@link SwarmMessage}. */
function toSwarmMessage(message: any): SwarmMessage {
const type =
typeof message?.getType === "function" ? message.getType() : (message?.role ?? message?.type);
const role: SwarmMessage["role"] =
type === "human" || type === "user" ? "user" : type === "system" ? "system" : "assistant";
const content =
typeof message?.content === "string" ? message.content : JSON.stringify(message?.content ?? "");
return { role, content };
}
/**
* Create a {@link SwarmRunner} backed by deepagents (`createDeepAgent`).
*
* Requires the optional peers `deepagents` and `@langchain/langgraph`, plus a
* model provider (e.g. `@langchain/anthropic`), to be installed by the host app:
* `npm i deepagents @langchain/langgraph @langchain/anthropic`.
*/
export async function createDeepAgentRunner(options: DeepAgentRunnerOptions): Promise<SwarmRunner> {
let deepagents: any;
try {
deepagents = await loadModule("deepagents");
} catch {
throw new Error(
"createDeepAgentRunner requires the 'deepagents' package. Install it in the host app: " +
"npm i deepagents @langchain/langgraph @langchain/anthropic"
);
}
let checkpointer = options.checkpointer;
if (!checkpointer) {
const langgraph = await loadModule("@langchain/langgraph");
checkpointer = new langgraph.MemorySaver();
}
const agent = deepagents.createDeepAgent({
model: options.model,
...(options.instructions ? { instructions: options.instructions } : {}),
checkpointer,
...options.agentParams
});
return {
async run(input: SwarmRunInput): Promise<SwarmRunResult> {
const threadId = input.threadId ?? newThreadId();
const state: Record<string, unknown> = { messages: input.messages };
if (input.rubric) state.rubric = input.rubric;
const result = await agent.invoke(state, { configurable: { thread_id: threadId } });
const messages: SwarmMessage[] = Array.isArray(result?.messages)
? result.messages.map(toSwarmMessage)
: [];
const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
return { threadId, messages, output: lastAssistant?.content ?? "" };
}
};
}