Skip to content

Commit 9d79646

Browse files
ralyodioclaude
andcommitted
feat(api): /api/swarm — deepagents agent swarm via @logicsrc/agentswarm
Signed-in, bring-your-own-key endpoint that runs a deepagents-backed swarm. Caller supplies provider + apiKey per request (transient, not stored — same pattern as /api/models); anthropic uses ChatAnthropic, other providers use the OpenAI-compatible adapter (incl. optional c0mpute.com GPUs). An optional `rubric` enables the self-check loop. Mounted at /api/swarm, auth via currentUser. Adds @logicsrc/agentswarm + deepagents + @langchain/{langgraph,anthropic,openai}. Typecheck clean (Docker tsconfig), boots, route auth-gated (401 without session). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0a13f3c commit 9d79646

3 files changed

Lines changed: 91 additions & 0 deletions

File tree

‎services/api/package.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
},
1616
"dependencies": {
1717
"@hono/node-server": "^1.13.7",
18+
"@langchain/anthropic": "^1.5.1",
19+
"@langchain/langgraph": "^1.4.6",
20+
"@langchain/openai": "^1.5.3",
1821
"@libsql/client": "^0.14.0",
22+
"@logicsrc/agentswarm": "^0.1.0",
23+
"deepagents": "^1.10.5",
1924
"hono": "^4.6.14"
2025
},
2126
"devDependencies": {

‎services/api/src/index.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { token, uuid, hashPassword, verifyPassword, SESSION_TTL, EMAIL_TOKEN_TTL } from './auth.js';
1111
import { sendEmail } from './email.js';
1212
import { store } from './store/routes.js';
13+
import { swarmRoutes } from './swarm.js';
1314

1415
const CP = {
1516
clientId: process.env.COINPAY_CLIENT_ID || '',
@@ -49,6 +50,9 @@ app.get('/api/healthz', (c) => c.json({ ok: true }));
4950
/* ---------- Extension store (tronbrowser.dev/store) ---------- */
5051
app.route('/api/store', store);
5152

53+
/* ---------- Agent swarm (@logicsrc/agentswarm, BYO key) ---------- */
54+
app.route('/api/swarm', swarmRoutes({ currentUser }));
55+
5256
/* ---------- CoinPay OAuth (preferred) ---------- */
5357
app.get('/api/auth/coinpay/login', (c) => {
5458
if (!CP.clientId) return c.text('CoinPay not configured', 500);

‎services/api/src/swarm.ts‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* /api/swarm — run a deepagents-backed agent swarm, powered by
3+
* @logicsrc/agentswarm. Signed-in, bring-your-own-key: the caller supplies a
4+
* provider + API key per request (used transiently, NOT stored — same model as
5+
* /api/models). An optional `rubric` turns on the self-check loop.
6+
*/
7+
import { Hono } from 'hono';
8+
import {
9+
createDeepAgentRunner,
10+
createRubricRunner,
11+
createLLMJudge,
12+
type SwarmRunner,
13+
} from '@logicsrc/agentswarm';
14+
15+
/** OpenAI-compatible provider bases (mirrors /api/models). anthropic is special-cased. */
16+
const OPENAI_COMPAT_BASE: Record<string, string> = {
17+
openai: 'https://api.openai.com/v1',
18+
google: 'https://generativelanguage.googleapis.com/v1beta/openai',
19+
deepseek: 'https://api.deepseek.com/v1',
20+
perplexity: 'https://api.perplexity.ai',
21+
huggingface: 'https://router.huggingface.co/v1',
22+
kimi: 'https://api.moonshot.ai/v1',
23+
qwen: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
24+
c0mpute: 'https://api.c0mpute.com/v1',
25+
};
26+
27+
/** Build a LangChain chat model for the requested provider with the caller's key. */
28+
async function buildModel(
29+
provider: string,
30+
apiKey: string,
31+
model?: string,
32+
baseUrl?: string,
33+
): Promise<object> {
34+
if (provider === 'anthropic') {
35+
const { ChatAnthropic } = await import('@langchain/anthropic');
36+
return new ChatAnthropic({ apiKey, model: model || 'claude-sonnet-4-6' });
37+
}
38+
const base = (baseUrl || OPENAI_COMPAT_BASE[provider] || '').replace(/\/$/, '');
39+
if (!base || /localhost|127\.0\.0\.1/.test(base)) {
40+
throw new Error(`unsupported or disallowed provider/baseUrl: ${provider}`);
41+
}
42+
const { ChatOpenAI } = await import('@langchain/openai');
43+
return new ChatOpenAI({ apiKey, model: model || 'gpt-4o-mini', configuration: { baseURL: base } });
44+
}
45+
46+
export interface SwarmDeps {
47+
/** Resolves the signed-in user, or null. */
48+
currentUser: (c: any) => Promise<{ id: string } | null>;
49+
}
50+
51+
/** Hono sub-router mounted at /api/swarm. */
52+
export function swarmRoutes(deps: SwarmDeps): Hono {
53+
const r = new Hono();
54+
55+
r.post('/', async (c) => {
56+
const user = await deps.currentUser(c);
57+
if (!user) return c.json({ error: 'unauthorized' }, 401);
58+
59+
const body = await c.req.json().catch(() => ({} as any));
60+
const { messages, rubric, provider = 'anthropic', apiKey, model, baseUrl } = body;
61+
if (!Array.isArray(messages) || messages.length === 0) {
62+
return c.json({ error: 'messages must be a non-empty array' }, 400);
63+
}
64+
if (!apiKey || typeof apiKey !== 'string') {
65+
return c.json({ error: 'apiKey required (bring your own key)' }, 400);
66+
}
67+
68+
try {
69+
const llm = await buildModel(String(provider), apiKey, model, baseUrl);
70+
let runner: SwarmRunner = await createDeepAgentRunner({ model: llm });
71+
if (typeof rubric === 'string' && rubric.trim()) {
72+
runner = createRubricRunner({ runner, judge: await createLLMJudge({ chatModel: llm }) });
73+
}
74+
const result = await runner.run({ messages, rubric });
75+
return c.json(result);
76+
} catch (e: any) {
77+
return c.json({ error: e?.message || String(e) }, 500);
78+
}
79+
});
80+
81+
return r;
82+
}

0 commit comments

Comments
 (0)