@radaros/transport 0.3.20 → 0.3.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2032 -0
- package/dist/index.d.cts +333 -0
- package/package.json +9 -6
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2032 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
MCPManager: () => MCPManager,
|
|
24
|
+
buildMultiModalInput: () => buildMultiModalInput,
|
|
25
|
+
createA2AServer: () => createA2AServer,
|
|
26
|
+
createAdminRouter: () => createAdminRouter,
|
|
27
|
+
createAgentGateway: () => createAgentGateway,
|
|
28
|
+
createAgentRouter: () => createAgentRouter,
|
|
29
|
+
createBrowserGateway: () => createBrowserGateway,
|
|
30
|
+
createFileUploadMiddleware: () => createFileUploadMiddleware,
|
|
31
|
+
createVoiceGateway: () => createVoiceGateway,
|
|
32
|
+
errorHandler: () => errorHandler,
|
|
33
|
+
generateAgentCard: () => generateAgentCard,
|
|
34
|
+
generateMultiAgentCard: () => generateMultiAgentCard,
|
|
35
|
+
generateOpenAPISpec: () => generateOpenAPISpec,
|
|
36
|
+
requestLogger: () => requestLogger
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/a2a/a2a-server.ts
|
|
41
|
+
var import_node_crypto = require("crypto");
|
|
42
|
+
var import_node_module = require("module");
|
|
43
|
+
|
|
44
|
+
// src/a2a/agent-card.ts
|
|
45
|
+
function generateAgentCard(agent, serverUrl, provider, version) {
|
|
46
|
+
const skills = agent.tools.map((tool) => ({
|
|
47
|
+
id: tool.name,
|
|
48
|
+
name: tool.name,
|
|
49
|
+
description: tool.description
|
|
50
|
+
}));
|
|
51
|
+
if (skills.length === 0) {
|
|
52
|
+
skills.push({
|
|
53
|
+
id: "general",
|
|
54
|
+
name: "General",
|
|
55
|
+
description: typeof agent.instructions === "string" ? agent.instructions.slice(0, 200) : "General-purpose agent"
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const description = typeof agent.instructions === "string" ? agent.instructions : `RadarOS agent: ${agent.name}`;
|
|
59
|
+
return {
|
|
60
|
+
name: agent.name,
|
|
61
|
+
description,
|
|
62
|
+
url: serverUrl,
|
|
63
|
+
version: version ?? "1.0.0",
|
|
64
|
+
provider,
|
|
65
|
+
capabilities: {
|
|
66
|
+
streaming: true,
|
|
67
|
+
pushNotifications: false,
|
|
68
|
+
stateTransitionHistory: true
|
|
69
|
+
},
|
|
70
|
+
skills,
|
|
71
|
+
defaultInputModes: ["text/plain"],
|
|
72
|
+
defaultOutputModes: ["text/plain"],
|
|
73
|
+
supportedInputModes: ["text/plain", "application/json"],
|
|
74
|
+
supportedOutputModes: ["text/plain", "application/json"]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function generateMultiAgentCard(agents, serverUrl, provider, version) {
|
|
78
|
+
const skills = Object.entries(agents).map(([name, agent]) => ({
|
|
79
|
+
id: name,
|
|
80
|
+
name: agent.name,
|
|
81
|
+
description: typeof agent.instructions === "string" ? agent.instructions.slice(0, 200) : `Agent: ${name}`
|
|
82
|
+
}));
|
|
83
|
+
return {
|
|
84
|
+
name: "RadarOS Agent Server",
|
|
85
|
+
description: `Multi-agent server with ${Object.keys(agents).length} agents: ${Object.keys(agents).join(", ")}`,
|
|
86
|
+
url: serverUrl,
|
|
87
|
+
version: version ?? "1.0.0",
|
|
88
|
+
provider,
|
|
89
|
+
capabilities: {
|
|
90
|
+
streaming: true,
|
|
91
|
+
pushNotifications: false,
|
|
92
|
+
stateTransitionHistory: true
|
|
93
|
+
},
|
|
94
|
+
skills,
|
|
95
|
+
defaultInputModes: ["text/plain"],
|
|
96
|
+
defaultOutputModes: ["text/plain"],
|
|
97
|
+
supportedInputModes: ["text/plain", "application/json"],
|
|
98
|
+
supportedOutputModes: ["text/plain", "application/json"]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/a2a/a2a-server.ts
|
|
103
|
+
var import_meta = {};
|
|
104
|
+
var _require = (0, import_node_module.createRequire)(import_meta.url);
|
|
105
|
+
var TaskStore = class {
|
|
106
|
+
tasks = /* @__PURE__ */ new Map();
|
|
107
|
+
maxTasks = 1e4;
|
|
108
|
+
get(id) {
|
|
109
|
+
return this.tasks.get(id);
|
|
110
|
+
}
|
|
111
|
+
set(task) {
|
|
112
|
+
if (this.tasks.size >= this.maxTasks) {
|
|
113
|
+
for (const [id, t] of this.tasks) {
|
|
114
|
+
if (t.status?.state === "completed" || t.status?.state === "canceled") {
|
|
115
|
+
this.tasks.delete(id);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this.tasks.set(task.id, task);
|
|
121
|
+
}
|
|
122
|
+
updateState(id, state, message) {
|
|
123
|
+
const task = this.tasks.get(id);
|
|
124
|
+
if (!task) return;
|
|
125
|
+
task.status = {
|
|
126
|
+
state,
|
|
127
|
+
message,
|
|
128
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
function a2aPartsToText(parts) {
|
|
133
|
+
return parts.filter((p) => p.kind === "text").map((p) => p.text).join("\n");
|
|
134
|
+
}
|
|
135
|
+
function textToA2AParts(text) {
|
|
136
|
+
return [{ kind: "text", text }];
|
|
137
|
+
}
|
|
138
|
+
function jsonRpcError(id, code, message) {
|
|
139
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
140
|
+
}
|
|
141
|
+
function resolveAgent(agents, message) {
|
|
142
|
+
const meta = message.metadata;
|
|
143
|
+
const agentName = meta?.agentName;
|
|
144
|
+
if (agentName && agents[agentName]) {
|
|
145
|
+
return agents[agentName];
|
|
146
|
+
}
|
|
147
|
+
const names = Object.keys(agents);
|
|
148
|
+
if (names.length === 1) {
|
|
149
|
+
return agents[names[0]];
|
|
150
|
+
}
|
|
151
|
+
const textContent = a2aPartsToText(message.parts).toLowerCase();
|
|
152
|
+
for (const [name, agent] of Object.entries(agents)) {
|
|
153
|
+
if (textContent.includes(name.toLowerCase())) {
|
|
154
|
+
return agent;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return agents[names[0]] ?? null;
|
|
158
|
+
}
|
|
159
|
+
function createA2AServer(app, opts) {
|
|
160
|
+
const express = _require("express");
|
|
161
|
+
const basePath = opts.basePath ?? "/";
|
|
162
|
+
const taskStore = new TaskStore();
|
|
163
|
+
const serverUrl = basePath === "/" ? "" : basePath;
|
|
164
|
+
const agentCard = generateMultiAgentCard(opts.agents, serverUrl || "/", opts.provider, opts.version);
|
|
165
|
+
app.get("/.well-known/agent.json", (_req, res) => {
|
|
166
|
+
res.json(agentCard);
|
|
167
|
+
});
|
|
168
|
+
app.use(basePath, express.json());
|
|
169
|
+
app.post(basePath, async (req, res) => {
|
|
170
|
+
const body = req.body;
|
|
171
|
+
if (!body || body.jsonrpc !== "2.0" || !body.method) {
|
|
172
|
+
return res.status(400).json(jsonRpcError(body?.id ?? 0, -32600, "Invalid JSON-RPC request"));
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
switch (body.method) {
|
|
176
|
+
case "message/send":
|
|
177
|
+
return await handleMessageSend(req, res, body, opts.agents, taskStore);
|
|
178
|
+
case "message/stream":
|
|
179
|
+
return await handleMessageStream(req, res, body, opts.agents, taskStore);
|
|
180
|
+
case "tasks/get":
|
|
181
|
+
return handleTasksGet(res, body, taskStore);
|
|
182
|
+
case "tasks/cancel":
|
|
183
|
+
return handleTasksCancel(res, body, taskStore);
|
|
184
|
+
default:
|
|
185
|
+
return res.json(jsonRpcError(body.id, -32601, `Method '${body.method}' not found`));
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
return res.json(jsonRpcError(body.id, -32e3, err.message ?? "Internal error"));
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async function handleMessageSend(_req, res, body, agents, store) {
|
|
193
|
+
const params = body.params;
|
|
194
|
+
const message = params?.message;
|
|
195
|
+
if (!message?.parts?.length) {
|
|
196
|
+
return res.json(jsonRpcError(body.id, -32602, "Missing message.parts"));
|
|
197
|
+
}
|
|
198
|
+
const agent = resolveAgent(agents, message);
|
|
199
|
+
if (!agent) {
|
|
200
|
+
return res.json(jsonRpcError(body.id, -32602, "No matching agent found"));
|
|
201
|
+
}
|
|
202
|
+
const taskId = (0, import_node_crypto.randomUUID)();
|
|
203
|
+
const input = a2aPartsToText(message.parts);
|
|
204
|
+
const task = {
|
|
205
|
+
id: taskId,
|
|
206
|
+
sessionId: params?.sessionId ?? void 0,
|
|
207
|
+
status: { state: "submitted", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
208
|
+
history: [message],
|
|
209
|
+
metadata: { agentName: agent.name }
|
|
210
|
+
};
|
|
211
|
+
store.set(task);
|
|
212
|
+
store.updateState(taskId, "working");
|
|
213
|
+
try {
|
|
214
|
+
const result = await agent.run(input, {
|
|
215
|
+
sessionId: task.sessionId
|
|
216
|
+
});
|
|
217
|
+
const responseParts = textToA2AParts(result.text);
|
|
218
|
+
if (result.structured) {
|
|
219
|
+
responseParts.push({
|
|
220
|
+
kind: "data",
|
|
221
|
+
data: result.structured
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const agentMessage = {
|
|
225
|
+
role: "agent",
|
|
226
|
+
parts: responseParts,
|
|
227
|
+
messageId: (0, import_node_crypto.randomUUID)(),
|
|
228
|
+
taskId
|
|
229
|
+
};
|
|
230
|
+
task.history.push(agentMessage);
|
|
231
|
+
if (result.toolCalls?.length) {
|
|
232
|
+
task.artifacts = result.toolCalls.map((tc) => ({
|
|
233
|
+
artifactId: tc.toolCallId,
|
|
234
|
+
name: tc.toolName,
|
|
235
|
+
parts: [
|
|
236
|
+
{
|
|
237
|
+
kind: "text",
|
|
238
|
+
text: typeof tc.result === "string" ? tc.result : tc.result.content
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
store.updateState(taskId, "completed", agentMessage);
|
|
244
|
+
const response = {
|
|
245
|
+
jsonrpc: "2.0",
|
|
246
|
+
id: body.id,
|
|
247
|
+
result: store.get(taskId)
|
|
248
|
+
};
|
|
249
|
+
res.json(response);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
const errorMessage = {
|
|
252
|
+
role: "agent",
|
|
253
|
+
parts: [{ kind: "text", text: `Error: ${err.message}` }],
|
|
254
|
+
taskId
|
|
255
|
+
};
|
|
256
|
+
store.updateState(taskId, "failed", errorMessage);
|
|
257
|
+
res.json(jsonRpcError(body.id, -32e3, err.message));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function handleMessageStream(_req, res, body, agents, store) {
|
|
261
|
+
const params = body.params;
|
|
262
|
+
const message = params?.message;
|
|
263
|
+
if (!message?.parts?.length) {
|
|
264
|
+
return res.json(jsonRpcError(body.id, -32602, "Missing message.parts"));
|
|
265
|
+
}
|
|
266
|
+
const agent = resolveAgent(agents, message);
|
|
267
|
+
if (!agent) {
|
|
268
|
+
return res.json(jsonRpcError(body.id, -32602, "No matching agent found"));
|
|
269
|
+
}
|
|
270
|
+
const taskId = (0, import_node_crypto.randomUUID)();
|
|
271
|
+
const input = a2aPartsToText(message.parts);
|
|
272
|
+
const task = {
|
|
273
|
+
id: taskId,
|
|
274
|
+
sessionId: params?.sessionId ?? void 0,
|
|
275
|
+
status: { state: "submitted", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
276
|
+
history: [message],
|
|
277
|
+
metadata: { agentName: agent.name }
|
|
278
|
+
};
|
|
279
|
+
store.set(task);
|
|
280
|
+
res.writeHead(200, {
|
|
281
|
+
"Content-Type": "text/event-stream",
|
|
282
|
+
"Cache-Control": "no-cache",
|
|
283
|
+
Connection: "keep-alive"
|
|
284
|
+
});
|
|
285
|
+
const sendEvent = (data) => {
|
|
286
|
+
res.write(`data: ${JSON.stringify(data)}
|
|
287
|
+
|
|
288
|
+
`);
|
|
289
|
+
};
|
|
290
|
+
store.updateState(taskId, "working");
|
|
291
|
+
sendEvent({
|
|
292
|
+
jsonrpc: "2.0",
|
|
293
|
+
id: body.id,
|
|
294
|
+
result: store.get(taskId)
|
|
295
|
+
});
|
|
296
|
+
try {
|
|
297
|
+
let fullText = "";
|
|
298
|
+
for await (const chunk of agent.stream(input, {
|
|
299
|
+
sessionId: task.sessionId
|
|
300
|
+
})) {
|
|
301
|
+
if (chunk.type === "text") {
|
|
302
|
+
fullText += chunk.text;
|
|
303
|
+
sendEvent({
|
|
304
|
+
jsonrpc: "2.0",
|
|
305
|
+
id: body.id,
|
|
306
|
+
result: {
|
|
307
|
+
id: taskId,
|
|
308
|
+
status: {
|
|
309
|
+
state: "working",
|
|
310
|
+
message: {
|
|
311
|
+
role: "agent",
|
|
312
|
+
parts: [{ kind: "text", text: chunk.text }]
|
|
313
|
+
},
|
|
314
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const agentMessage = {
|
|
321
|
+
role: "agent",
|
|
322
|
+
parts: textToA2AParts(fullText),
|
|
323
|
+
messageId: (0, import_node_crypto.randomUUID)(),
|
|
324
|
+
taskId
|
|
325
|
+
};
|
|
326
|
+
task.history.push(agentMessage);
|
|
327
|
+
store.updateState(taskId, "completed", agentMessage);
|
|
328
|
+
sendEvent({
|
|
329
|
+
jsonrpc: "2.0",
|
|
330
|
+
id: body.id,
|
|
331
|
+
result: store.get(taskId)
|
|
332
|
+
});
|
|
333
|
+
res.end();
|
|
334
|
+
} catch (err) {
|
|
335
|
+
const errorMessage = {
|
|
336
|
+
role: "agent",
|
|
337
|
+
parts: [{ kind: "text", text: `Error: ${err.message}` }],
|
|
338
|
+
taskId
|
|
339
|
+
};
|
|
340
|
+
store.updateState(taskId, "failed", errorMessage);
|
|
341
|
+
sendEvent({
|
|
342
|
+
jsonrpc: "2.0",
|
|
343
|
+
id: body.id,
|
|
344
|
+
result: store.get(taskId)
|
|
345
|
+
});
|
|
346
|
+
res.end();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function handleTasksGet(res, body, store) {
|
|
350
|
+
const params = body.params;
|
|
351
|
+
const taskId = params?.id;
|
|
352
|
+
if (!taskId) {
|
|
353
|
+
return res.json(jsonRpcError(body.id, -32602, "Missing task id"));
|
|
354
|
+
}
|
|
355
|
+
const task = store.get(taskId);
|
|
356
|
+
if (!task) {
|
|
357
|
+
return res.json(jsonRpcError(body.id, -32602, `Task '${taskId}' not found`));
|
|
358
|
+
}
|
|
359
|
+
const historyLength = params?.historyLength;
|
|
360
|
+
const result = { ...task };
|
|
361
|
+
if (historyLength && result.history) {
|
|
362
|
+
result.history = result.history.slice(-historyLength);
|
|
363
|
+
}
|
|
364
|
+
res.json({ jsonrpc: "2.0", id: body.id, result });
|
|
365
|
+
}
|
|
366
|
+
function handleTasksCancel(res, body, store) {
|
|
367
|
+
const params = body.params;
|
|
368
|
+
const taskId = params?.id;
|
|
369
|
+
if (!taskId) {
|
|
370
|
+
return res.json(jsonRpcError(body.id, -32602, "Missing task id"));
|
|
371
|
+
}
|
|
372
|
+
const task = store.get(taskId);
|
|
373
|
+
if (!task) {
|
|
374
|
+
return res.json(jsonRpcError(body.id, -32602, `Task '${taskId}' not found`));
|
|
375
|
+
}
|
|
376
|
+
store.updateState(taskId, "canceled");
|
|
377
|
+
res.json({
|
|
378
|
+
jsonrpc: "2.0",
|
|
379
|
+
id: body.id,
|
|
380
|
+
result: store.get(taskId)
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/express/admin-router.ts
|
|
385
|
+
var import_node_module2 = require("module");
|
|
386
|
+
var import_core2 = require("@radaros/core");
|
|
387
|
+
|
|
388
|
+
// src/express/mcp-manager.ts
|
|
389
|
+
var import_core = require("@radaros/core");
|
|
390
|
+
var MCPManager = class {
|
|
391
|
+
servers = /* @__PURE__ */ new Map();
|
|
392
|
+
/** Add a server config. Auto-generates an id from the name if not provided. */
|
|
393
|
+
add(config, id) {
|
|
394
|
+
const serverId = id ?? config.name;
|
|
395
|
+
if (this.servers.has(serverId)) {
|
|
396
|
+
throw new Error(`MCP server "${serverId}" already exists`);
|
|
397
|
+
}
|
|
398
|
+
const provider = new import_core.MCPToolProvider(config);
|
|
399
|
+
const entry = {
|
|
400
|
+
id: serverId,
|
|
401
|
+
config,
|
|
402
|
+
provider,
|
|
403
|
+
status: "disconnected",
|
|
404
|
+
toolCount: 0
|
|
405
|
+
};
|
|
406
|
+
this.servers.set(serverId, entry);
|
|
407
|
+
return this.summarize(entry);
|
|
408
|
+
}
|
|
409
|
+
/** Connect a server by id. Discovers tools on success. */
|
|
410
|
+
async connect(id) {
|
|
411
|
+
const entry = this.getEntry(id);
|
|
412
|
+
entry.status = "connecting";
|
|
413
|
+
entry.error = void 0;
|
|
414
|
+
try {
|
|
415
|
+
await entry.provider.connect();
|
|
416
|
+
const tools = await entry.provider.getTools();
|
|
417
|
+
entry.status = "connected";
|
|
418
|
+
entry.toolCount = tools.length;
|
|
419
|
+
entry.connectedAt = /* @__PURE__ */ new Date();
|
|
420
|
+
} catch (err) {
|
|
421
|
+
entry.status = "error";
|
|
422
|
+
entry.error = err.message;
|
|
423
|
+
}
|
|
424
|
+
return this.summarize(entry);
|
|
425
|
+
}
|
|
426
|
+
/** Disconnect a server by id. */
|
|
427
|
+
async disconnect(id) {
|
|
428
|
+
const entry = this.getEntry(id);
|
|
429
|
+
try {
|
|
430
|
+
await entry.provider.close();
|
|
431
|
+
} catch {
|
|
432
|
+
}
|
|
433
|
+
entry.status = "disconnected";
|
|
434
|
+
entry.toolCount = 0;
|
|
435
|
+
entry.connectedAt = void 0;
|
|
436
|
+
return this.summarize(entry);
|
|
437
|
+
}
|
|
438
|
+
/** Remove a server entirely. Disconnects first if connected. */
|
|
439
|
+
async remove(id) {
|
|
440
|
+
const entry = this.getEntry(id);
|
|
441
|
+
if (entry.status === "connected" || entry.status === "connecting") {
|
|
442
|
+
try {
|
|
443
|
+
await entry.provider.close();
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
this.servers.delete(id);
|
|
448
|
+
}
|
|
449
|
+
/** Get all tools from all connected servers, merged into one array. */
|
|
450
|
+
async getAllTools() {
|
|
451
|
+
const all = [];
|
|
452
|
+
for (const entry of this.servers.values()) {
|
|
453
|
+
if (entry.status === "connected") {
|
|
454
|
+
try {
|
|
455
|
+
const tools = await entry.provider.getTools();
|
|
456
|
+
all.push(...tools);
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return all;
|
|
462
|
+
}
|
|
463
|
+
/** Get tools from a specific server. */
|
|
464
|
+
async getTools(id) {
|
|
465
|
+
const entry = this.getEntry(id);
|
|
466
|
+
if (entry.status !== "connected") {
|
|
467
|
+
throw new Error(`MCP server "${id}" is not connected`);
|
|
468
|
+
}
|
|
469
|
+
return entry.provider.getTools();
|
|
470
|
+
}
|
|
471
|
+
/** List all registered servers. */
|
|
472
|
+
list() {
|
|
473
|
+
return Array.from(this.servers.values()).map((e) => this.summarize(e));
|
|
474
|
+
}
|
|
475
|
+
/** Get a single server summary. */
|
|
476
|
+
get(id) {
|
|
477
|
+
return this.summarize(this.getEntry(id));
|
|
478
|
+
}
|
|
479
|
+
has(id) {
|
|
480
|
+
return this.servers.has(id);
|
|
481
|
+
}
|
|
482
|
+
/** Disconnect all servers. */
|
|
483
|
+
async closeAll() {
|
|
484
|
+
const promises = Array.from(this.servers.values()).map(async (entry) => {
|
|
485
|
+
if (entry.status === "connected") {
|
|
486
|
+
try {
|
|
487
|
+
await entry.provider.close();
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
entry.status = "disconnected";
|
|
491
|
+
entry.toolCount = 0;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
await Promise.all(promises);
|
|
495
|
+
}
|
|
496
|
+
getEntry(id) {
|
|
497
|
+
const entry = this.servers.get(id);
|
|
498
|
+
if (!entry) throw new Error(`MCP server "${id}" not found`);
|
|
499
|
+
return entry;
|
|
500
|
+
}
|
|
501
|
+
summarize(entry) {
|
|
502
|
+
return {
|
|
503
|
+
id: entry.id,
|
|
504
|
+
name: entry.config.name,
|
|
505
|
+
transport: entry.config.transport,
|
|
506
|
+
url: entry.config.url,
|
|
507
|
+
command: entry.config.command,
|
|
508
|
+
status: entry.status,
|
|
509
|
+
toolCount: entry.toolCount,
|
|
510
|
+
error: entry.error,
|
|
511
|
+
connectedAt: entry.connectedAt?.toISOString()
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/express/admin-router.ts
|
|
517
|
+
var import_meta2 = {};
|
|
518
|
+
var _require2 = (0, import_node_module2.createRequire)(import_meta2.url);
|
|
519
|
+
function createAdminRouter(opts) {
|
|
520
|
+
let express;
|
|
521
|
+
try {
|
|
522
|
+
express = _require2("express");
|
|
523
|
+
} catch {
|
|
524
|
+
throw new Error("express is required for createAdminRouter. Install it: npm install express");
|
|
525
|
+
}
|
|
526
|
+
const router = express.Router();
|
|
527
|
+
const mcpManager = opts?.mcpManager ?? new MCPManager();
|
|
528
|
+
if (opts?.middleware) {
|
|
529
|
+
for (const mw of opts.middleware) router.use(mw);
|
|
530
|
+
}
|
|
531
|
+
if (!opts?.middleware?.length && process.env.NODE_ENV === "production") {
|
|
532
|
+
console.warn(
|
|
533
|
+
"[admin-router] WARNING: Admin routes mounted without authentication middleware. These endpoints can add MCP servers, execute tools, and modify configurations. Pass middleware in AdminRouterOptions to secure them."
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
router.get("/mcp", (_req, res) => {
|
|
537
|
+
res.json(mcpManager.list());
|
|
538
|
+
});
|
|
539
|
+
router.get("/mcp/tools", async (_req, res) => {
|
|
540
|
+
try {
|
|
541
|
+
const tools = await mcpManager.getAllTools();
|
|
542
|
+
res.json(
|
|
543
|
+
tools.map((t) => ({
|
|
544
|
+
name: t.name,
|
|
545
|
+
description: t.description,
|
|
546
|
+
parameters: Object.keys(t.parameters?.shape ?? {})
|
|
547
|
+
}))
|
|
548
|
+
);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
res.status(500).json({ error: err.message });
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
router.post("/mcp", async (req, res) => {
|
|
554
|
+
try {
|
|
555
|
+
const { name, transport, url, command, args, env, headers, id, autoConnect } = req.body;
|
|
556
|
+
if (!name || !transport) {
|
|
557
|
+
return res.status(400).json({ error: "name and transport are required" });
|
|
558
|
+
}
|
|
559
|
+
const config = { name, transport };
|
|
560
|
+
if (url) config.url = url;
|
|
561
|
+
if (command) config.command = command;
|
|
562
|
+
if (args) config.args = args;
|
|
563
|
+
if (env) config.env = env;
|
|
564
|
+
if (headers) config.headers = headers;
|
|
565
|
+
const summary = mcpManager.add(config, id);
|
|
566
|
+
if (autoConnect !== false) {
|
|
567
|
+
const connected = await mcpManager.connect(summary.id);
|
|
568
|
+
return res.status(201).json(connected);
|
|
569
|
+
}
|
|
570
|
+
res.status(201).json(summary);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
res.status(400).json({ error: err.message });
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
router.get("/mcp/:id", (req, res) => {
|
|
576
|
+
try {
|
|
577
|
+
res.json(mcpManager.get(req.params.id));
|
|
578
|
+
} catch (err) {
|
|
579
|
+
res.status(404).json({ error: err.message });
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
router.post("/mcp/:id/connect", async (req, res) => {
|
|
583
|
+
try {
|
|
584
|
+
const summary = await mcpManager.connect(req.params.id);
|
|
585
|
+
res.json(summary);
|
|
586
|
+
} catch (err) {
|
|
587
|
+
res.status(400).json({ error: err.message });
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
router.post("/mcp/:id/disconnect", async (req, res) => {
|
|
591
|
+
try {
|
|
592
|
+
const summary = await mcpManager.disconnect(req.params.id);
|
|
593
|
+
res.json(summary);
|
|
594
|
+
} catch (err) {
|
|
595
|
+
res.status(400).json({ error: err.message });
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
router.delete("/mcp/:id", async (req, res) => {
|
|
599
|
+
try {
|
|
600
|
+
await mcpManager.remove(req.params.id);
|
|
601
|
+
res.json({ ok: true });
|
|
602
|
+
} catch (err) {
|
|
603
|
+
res.status(404).json({ error: err.message });
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
router.get("/mcp/:id/tools", async (req, res) => {
|
|
607
|
+
try {
|
|
608
|
+
const tools = await mcpManager.getTools(req.params.id);
|
|
609
|
+
res.json(
|
|
610
|
+
tools.map((t) => ({
|
|
611
|
+
name: t.name,
|
|
612
|
+
description: t.description,
|
|
613
|
+
parameters: Object.keys(t.parameters?.shape ?? {})
|
|
614
|
+
}))
|
|
615
|
+
);
|
|
616
|
+
} catch (err) {
|
|
617
|
+
res.status(400).json({ error: err.message });
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
router.get("/toolkits", (_req, res) => {
|
|
621
|
+
res.json(import_core2.toolkitCatalog.list());
|
|
622
|
+
});
|
|
623
|
+
router.get("/toolkits/:id", (req, res) => {
|
|
624
|
+
const meta = import_core2.toolkitCatalog.get(req.params.id);
|
|
625
|
+
if (!meta) return res.status(404).json({ error: `Toolkit "${req.params.id}" not found` });
|
|
626
|
+
res.json(meta);
|
|
627
|
+
});
|
|
628
|
+
router.post("/toolkits/:id", (req, res) => {
|
|
629
|
+
try {
|
|
630
|
+
const toolkit = import_core2.toolkitCatalog.create(req.params.id, req.body ?? {});
|
|
631
|
+
const tools = toolkit.getTools().map((t) => ({
|
|
632
|
+
name: t.name,
|
|
633
|
+
description: t.description
|
|
634
|
+
}));
|
|
635
|
+
res.status(201).json({
|
|
636
|
+
id: req.params.id,
|
|
637
|
+
name: toolkit.name,
|
|
638
|
+
tools
|
|
639
|
+
});
|
|
640
|
+
} catch (err) {
|
|
641
|
+
res.status(400).json({ error: err.message });
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
return { router, mcpManager };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/express/file-upload.ts
|
|
648
|
+
var import_node_module3 = require("module");
|
|
649
|
+
var import_meta3 = {};
|
|
650
|
+
var _require3 = (0, import_node_module3.createRequire)(import_meta3.url);
|
|
651
|
+
var MIME_TO_PART_TYPE = {
|
|
652
|
+
"image/png": "image",
|
|
653
|
+
"image/jpeg": "image",
|
|
654
|
+
"image/jpg": "image",
|
|
655
|
+
"image/gif": "image",
|
|
656
|
+
"image/webp": "image",
|
|
657
|
+
"audio/mpeg": "audio",
|
|
658
|
+
"audio/mp3": "audio",
|
|
659
|
+
"audio/wav": "audio",
|
|
660
|
+
"audio/ogg": "audio",
|
|
661
|
+
"audio/webm": "audio",
|
|
662
|
+
"audio/flac": "audio",
|
|
663
|
+
"audio/aac": "audio",
|
|
664
|
+
"audio/mp4": "audio"
|
|
665
|
+
};
|
|
666
|
+
function getPartType(mimeType) {
|
|
667
|
+
return MIME_TO_PART_TYPE[mimeType] ?? "file";
|
|
668
|
+
}
|
|
669
|
+
function createFileUploadMiddleware(opts = {}) {
|
|
670
|
+
let multer;
|
|
671
|
+
try {
|
|
672
|
+
multer = _require3("multer");
|
|
673
|
+
} catch {
|
|
674
|
+
throw new Error("multer is required for file uploads. Install it: npm install multer");
|
|
675
|
+
}
|
|
676
|
+
const storage = multer.memoryStorage();
|
|
677
|
+
const upload = multer({
|
|
678
|
+
storage,
|
|
679
|
+
limits: {
|
|
680
|
+
fileSize: opts.maxFileSize ?? 50 * 1024 * 1024,
|
|
681
|
+
files: opts.maxFiles ?? 10
|
|
682
|
+
},
|
|
683
|
+
fileFilter: opts.allowedMimeTypes ? (_req, file, cb) => {
|
|
684
|
+
if (opts.allowedMimeTypes.includes(file.mimetype)) {
|
|
685
|
+
cb(null, true);
|
|
686
|
+
} else {
|
|
687
|
+
cb(new Error(`File type ${file.mimetype} is not allowed`));
|
|
688
|
+
}
|
|
689
|
+
} : void 0
|
|
690
|
+
});
|
|
691
|
+
return upload.array("files", opts.maxFiles ?? 10);
|
|
692
|
+
}
|
|
693
|
+
function filesToContentParts(files) {
|
|
694
|
+
return files.map((file) => {
|
|
695
|
+
const base64 = file.buffer.toString("base64");
|
|
696
|
+
const partType = getPartType(file.mimetype);
|
|
697
|
+
return {
|
|
698
|
+
type: partType,
|
|
699
|
+
data: base64,
|
|
700
|
+
mimeType: file.mimetype,
|
|
701
|
+
...partType === "file" ? { fileName: file.originalname } : {}
|
|
702
|
+
};
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
function buildMultiModalInput(body, files) {
|
|
706
|
+
const textInput = body?.input;
|
|
707
|
+
if (!files || files.length === 0) {
|
|
708
|
+
return textInput;
|
|
709
|
+
}
|
|
710
|
+
const parts = [];
|
|
711
|
+
if (textInput) {
|
|
712
|
+
parts.push({ type: "text", text: textInput });
|
|
713
|
+
}
|
|
714
|
+
parts.push(...filesToContentParts(files));
|
|
715
|
+
return parts;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/express/middleware.ts
|
|
719
|
+
function errorHandler(options) {
|
|
720
|
+
const log = options?.logger ?? console;
|
|
721
|
+
return (err, _req, res, _next) => {
|
|
722
|
+
log.error("[radaros:transport] Error:", err.message);
|
|
723
|
+
res.status(err.statusCode ?? 500).json({
|
|
724
|
+
error: err.message ?? "Internal server error"
|
|
725
|
+
});
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
function requestLogger(options) {
|
|
729
|
+
const log = options?.logger ?? console;
|
|
730
|
+
return (req, _res, next) => {
|
|
731
|
+
log.log(`[radaros:transport] ${req.method} ${req.path}`);
|
|
732
|
+
next();
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/express/router-factory.ts
|
|
737
|
+
var import_node_module5 = require("module");
|
|
738
|
+
var import_core3 = require("@radaros/core");
|
|
739
|
+
|
|
740
|
+
// src/express/swagger.ts
|
|
741
|
+
var import_node_module4 = require("module");
|
|
742
|
+
var import_meta4 = {};
|
|
743
|
+
var _require4 = (0, import_node_module4.createRequire)(import_meta4.url);
|
|
744
|
+
function zodSchemaToJsonSchema(schema) {
|
|
745
|
+
try {
|
|
746
|
+
const zodToJsonSchema = _require4("zod-to-json-schema").default ?? _require4("zod-to-json-schema");
|
|
747
|
+
const result = zodToJsonSchema(schema, { target: "openApi3" });
|
|
748
|
+
const { $schema, ...rest } = result;
|
|
749
|
+
return rest;
|
|
750
|
+
} catch {
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
var SCHEMAS = {
|
|
755
|
+
RunRequest: {
|
|
756
|
+
type: "object",
|
|
757
|
+
required: ["input"],
|
|
758
|
+
properties: {
|
|
759
|
+
input: {
|
|
760
|
+
oneOf: [
|
|
761
|
+
{ type: "string", description: "Text input" },
|
|
762
|
+
{
|
|
763
|
+
type: "array",
|
|
764
|
+
description: "Multi-modal content parts",
|
|
765
|
+
items: {
|
|
766
|
+
oneOf: [
|
|
767
|
+
{
|
|
768
|
+
type: "object",
|
|
769
|
+
properties: {
|
|
770
|
+
type: { type: "string", enum: ["text"] },
|
|
771
|
+
text: { type: "string" }
|
|
772
|
+
},
|
|
773
|
+
required: ["type", "text"]
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
type: "object",
|
|
777
|
+
properties: {
|
|
778
|
+
type: { type: "string", enum: ["image"] },
|
|
779
|
+
data: { type: "string", description: "Base64 data or URL" },
|
|
780
|
+
mimeType: { type: "string", enum: ["image/png", "image/jpeg", "image/gif", "image/webp"] }
|
|
781
|
+
},
|
|
782
|
+
required: ["type", "data"]
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
type: "object",
|
|
786
|
+
properties: {
|
|
787
|
+
type: { type: "string", enum: ["audio"] },
|
|
788
|
+
data: { type: "string", description: "Base64 data or URL" },
|
|
789
|
+
mimeType: { type: "string" }
|
|
790
|
+
},
|
|
791
|
+
required: ["type", "data"]
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
type: "object",
|
|
795
|
+
properties: {
|
|
796
|
+
type: { type: "string", enum: ["file"] },
|
|
797
|
+
data: { type: "string", description: "Base64 data or URL" },
|
|
798
|
+
mimeType: { type: "string" },
|
|
799
|
+
fileName: { type: "string" }
|
|
800
|
+
},
|
|
801
|
+
required: ["type", "data"]
|
|
802
|
+
}
|
|
803
|
+
]
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
]
|
|
807
|
+
},
|
|
808
|
+
sessionId: { type: "string", description: "Session ID for conversation continuity" },
|
|
809
|
+
userId: { type: "string", description: "User identifier" }
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
MultipartRunRequest: {
|
|
813
|
+
type: "object",
|
|
814
|
+
properties: {
|
|
815
|
+
input: { type: "string", description: "Text input" },
|
|
816
|
+
sessionId: { type: "string" },
|
|
817
|
+
userId: { type: "string" },
|
|
818
|
+
files: {
|
|
819
|
+
type: "array",
|
|
820
|
+
items: { type: "string", format: "binary" },
|
|
821
|
+
description: "Files to include as multi-modal input (images, audio, documents)"
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
required: ["input"]
|
|
825
|
+
},
|
|
826
|
+
RunOutput: {
|
|
827
|
+
type: "object",
|
|
828
|
+
properties: {
|
|
829
|
+
text: { type: "string", description: "Agent response text" },
|
|
830
|
+
toolCalls: {
|
|
831
|
+
type: "array",
|
|
832
|
+
items: {
|
|
833
|
+
type: "object",
|
|
834
|
+
properties: {
|
|
835
|
+
toolCallId: { type: "string" },
|
|
836
|
+
toolName: { type: "string" },
|
|
837
|
+
result: {}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
usage: {
|
|
842
|
+
type: "object",
|
|
843
|
+
properties: {
|
|
844
|
+
promptTokens: { type: "number" },
|
|
845
|
+
completionTokens: { type: "number" },
|
|
846
|
+
totalTokens: { type: "number" }
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
structured: { description: "Parsed structured output (if schema is configured)" },
|
|
850
|
+
durationMs: { type: "number" }
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
StreamChunk: {
|
|
854
|
+
type: "object",
|
|
855
|
+
description: "Server-Sent Event data",
|
|
856
|
+
properties: {
|
|
857
|
+
type: { type: "string", enum: ["text", "tool_call_start", "tool_call_delta", "tool_call_end", "finish"] },
|
|
858
|
+
text: { type: "string" }
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
Error: {
|
|
862
|
+
type: "object",
|
|
863
|
+
properties: {
|
|
864
|
+
error: { type: "string" }
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
WorkflowRunRequest: {
|
|
868
|
+
type: "object",
|
|
869
|
+
properties: {
|
|
870
|
+
sessionId: { type: "string" },
|
|
871
|
+
userId: { type: "string" }
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
function buildAgentDescription(agent) {
|
|
876
|
+
const parts = [];
|
|
877
|
+
parts.push(`**Model:** \`${agent.providerId}/${agent.modelId}\``);
|
|
878
|
+
if (typeof agent.instructions === "string") {
|
|
879
|
+
const instr = agent.instructions.length > 200 ? `${agent.instructions.slice(0, 200)}\u2026` : agent.instructions;
|
|
880
|
+
parts.push(`**Instructions:** ${instr}`);
|
|
881
|
+
}
|
|
882
|
+
if (agent.tools?.length > 0) {
|
|
883
|
+
const toolNames = agent.tools.map((t) => `\`${t.name}\``).join(", ");
|
|
884
|
+
parts.push(`**Tools:** ${toolNames}`);
|
|
885
|
+
}
|
|
886
|
+
if (agent.hasStructuredOutput) {
|
|
887
|
+
parts.push("**Structured Output:** Enabled");
|
|
888
|
+
}
|
|
889
|
+
return parts.join("\n\n");
|
|
890
|
+
}
|
|
891
|
+
function generateOpenAPISpec(routerOpts, swaggerOpts = {}) {
|
|
892
|
+
const prefix = swaggerOpts.routePrefix ?? "";
|
|
893
|
+
const providers = /* @__PURE__ */ new Set();
|
|
894
|
+
if (routerOpts.agents) {
|
|
895
|
+
for (const agent of Object.values(routerOpts.agents)) {
|
|
896
|
+
providers.add(agent.providerId ?? "unknown");
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const securitySchemes = {};
|
|
900
|
+
const securityRequirements = [];
|
|
901
|
+
if (providers.has("openai")) {
|
|
902
|
+
securitySchemes.OpenAIKey = {
|
|
903
|
+
type: "apiKey",
|
|
904
|
+
in: "header",
|
|
905
|
+
name: "x-openai-api-key",
|
|
906
|
+
description: "OpenAI API key (sk-...)"
|
|
907
|
+
};
|
|
908
|
+
securityRequirements.push({ OpenAIKey: [] });
|
|
909
|
+
}
|
|
910
|
+
if (providers.has("google")) {
|
|
911
|
+
securitySchemes.GoogleKey = {
|
|
912
|
+
type: "apiKey",
|
|
913
|
+
in: "header",
|
|
914
|
+
name: "x-google-api-key",
|
|
915
|
+
description: "Google AI API key (AIza...)"
|
|
916
|
+
};
|
|
917
|
+
securityRequirements.push({ GoogleKey: [] });
|
|
918
|
+
}
|
|
919
|
+
if (providers.has("anthropic")) {
|
|
920
|
+
securitySchemes.AnthropicKey = {
|
|
921
|
+
type: "apiKey",
|
|
922
|
+
in: "header",
|
|
923
|
+
name: "x-anthropic-api-key",
|
|
924
|
+
description: "Anthropic API key (sk-ant-...)"
|
|
925
|
+
};
|
|
926
|
+
securityRequirements.push({ AnthropicKey: [] });
|
|
927
|
+
}
|
|
928
|
+
securitySchemes.GenericKey = {
|
|
929
|
+
type: "apiKey",
|
|
930
|
+
in: "header",
|
|
931
|
+
name: "x-api-key",
|
|
932
|
+
description: "Generic API key (used if provider-specific key is not set)"
|
|
933
|
+
};
|
|
934
|
+
const spec = {
|
|
935
|
+
openapi: "3.0.3",
|
|
936
|
+
info: {
|
|
937
|
+
title: swaggerOpts.title ?? "RadarOS API",
|
|
938
|
+
description: swaggerOpts.description ?? "Auto-generated API documentation for RadarOS agents, teams, and workflows.",
|
|
939
|
+
version: swaggerOpts.version ?? "1.0.0"
|
|
940
|
+
},
|
|
941
|
+
paths: {},
|
|
942
|
+
components: {
|
|
943
|
+
schemas: SCHEMAS,
|
|
944
|
+
securitySchemes
|
|
945
|
+
},
|
|
946
|
+
security: securityRequirements,
|
|
947
|
+
tags: []
|
|
948
|
+
};
|
|
949
|
+
if (swaggerOpts.servers) {
|
|
950
|
+
spec.servers = swaggerOpts.servers;
|
|
951
|
+
}
|
|
952
|
+
if (routerOpts.agents && Object.keys(routerOpts.agents).length > 0) {
|
|
953
|
+
spec.tags.push({ name: "Agents", description: "Agent endpoints for running and streaming AI agents" });
|
|
954
|
+
for (const [name, agent] of Object.entries(routerOpts.agents)) {
|
|
955
|
+
const agentDesc = buildAgentDescription(agent);
|
|
956
|
+
let responseSchemaRef = "#/components/schemas/RunOutput";
|
|
957
|
+
const zodSchema = agent.structuredOutputSchema;
|
|
958
|
+
if (zodSchema) {
|
|
959
|
+
const structuredJsonSchema = zodSchemaToJsonSchema(zodSchema);
|
|
960
|
+
if (structuredJsonSchema) {
|
|
961
|
+
const schemaName = `RunOutput_${name}`;
|
|
962
|
+
spec.components.schemas[schemaName] = {
|
|
963
|
+
type: "object",
|
|
964
|
+
properties: {
|
|
965
|
+
text: { type: "string", description: "Raw agent response text" },
|
|
966
|
+
toolCalls: {
|
|
967
|
+
type: "array",
|
|
968
|
+
items: {
|
|
969
|
+
type: "object",
|
|
970
|
+
properties: {
|
|
971
|
+
toolCallId: { type: "string" },
|
|
972
|
+
toolName: { type: "string" },
|
|
973
|
+
result: {}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
usage: {
|
|
978
|
+
type: "object",
|
|
979
|
+
properties: {
|
|
980
|
+
promptTokens: { type: "number" },
|
|
981
|
+
completionTokens: { type: "number" },
|
|
982
|
+
totalTokens: { type: "number" }
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
structured: {
|
|
986
|
+
...structuredJsonSchema,
|
|
987
|
+
description: "Parsed structured output"
|
|
988
|
+
},
|
|
989
|
+
durationMs: { type: "number" }
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
responseSchemaRef = `#/components/schemas/${schemaName}`;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
spec.paths[`${prefix}/agents/${name}/run`] = {
|
|
996
|
+
post: {
|
|
997
|
+
tags: ["Agents"],
|
|
998
|
+
summary: `Run agent: ${name}`,
|
|
999
|
+
description: agentDesc,
|
|
1000
|
+
operationId: `runAgent_${name}`,
|
|
1001
|
+
requestBody: {
|
|
1002
|
+
required: true,
|
|
1003
|
+
content: {
|
|
1004
|
+
"application/json": {
|
|
1005
|
+
schema: { $ref: "#/components/schemas/RunRequest" }
|
|
1006
|
+
},
|
|
1007
|
+
"multipart/form-data": {
|
|
1008
|
+
schema: { $ref: "#/components/schemas/MultipartRunRequest" }
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
responses: {
|
|
1013
|
+
"200": {
|
|
1014
|
+
description: "Agent run result",
|
|
1015
|
+
content: {
|
|
1016
|
+
"application/json": {
|
|
1017
|
+
schema: { $ref: responseSchemaRef }
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
"400": {
|
|
1022
|
+
description: "Bad request",
|
|
1023
|
+
content: {
|
|
1024
|
+
"application/json": {
|
|
1025
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
"500": {
|
|
1030
|
+
description: "Internal server error",
|
|
1031
|
+
content: {
|
|
1032
|
+
"application/json": {
|
|
1033
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
spec.paths[`${prefix}/agents/${name}/stream`] = {
|
|
1041
|
+
post: {
|
|
1042
|
+
tags: ["Agents"],
|
|
1043
|
+
summary: `Stream agent: ${name}`,
|
|
1044
|
+
description: `Stream responses from agent **${name}** via Server-Sent Events.
|
|
1045
|
+
|
|
1046
|
+
${agentDesc}`,
|
|
1047
|
+
operationId: `streamAgent_${name}`,
|
|
1048
|
+
requestBody: {
|
|
1049
|
+
required: true,
|
|
1050
|
+
content: {
|
|
1051
|
+
"application/json": {
|
|
1052
|
+
schema: { $ref: "#/components/schemas/RunRequest" }
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
},
|
|
1056
|
+
responses: {
|
|
1057
|
+
"200": {
|
|
1058
|
+
description: "SSE stream of agent chunks",
|
|
1059
|
+
content: {
|
|
1060
|
+
"text/event-stream": {
|
|
1061
|
+
schema: { $ref: "#/components/schemas/StreamChunk" }
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
"400": {
|
|
1066
|
+
description: "Bad request",
|
|
1067
|
+
content: {
|
|
1068
|
+
"application/json": {
|
|
1069
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (routerOpts.teams && Object.keys(routerOpts.teams).length > 0) {
|
|
1079
|
+
spec.tags.push({ name: "Teams", description: "Team endpoints for multi-agent coordination" });
|
|
1080
|
+
for (const name of Object.keys(routerOpts.teams)) {
|
|
1081
|
+
spec.paths[`${prefix}/teams/${name}/run`] = {
|
|
1082
|
+
post: {
|
|
1083
|
+
tags: ["Teams"],
|
|
1084
|
+
summary: `Run team: ${name}`,
|
|
1085
|
+
operationId: `runTeam_${name}`,
|
|
1086
|
+
requestBody: {
|
|
1087
|
+
required: true,
|
|
1088
|
+
content: {
|
|
1089
|
+
"application/json": {
|
|
1090
|
+
schema: { $ref: "#/components/schemas/RunRequest" }
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
1094
|
+
responses: {
|
|
1095
|
+
"200": {
|
|
1096
|
+
description: "Team run result",
|
|
1097
|
+
content: {
|
|
1098
|
+
"application/json": {
|
|
1099
|
+
schema: { $ref: "#/components/schemas/RunOutput" }
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
},
|
|
1103
|
+
"400": {
|
|
1104
|
+
description: "Bad request",
|
|
1105
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
|
|
1106
|
+
},
|
|
1107
|
+
"500": {
|
|
1108
|
+
description: "Internal server error",
|
|
1109
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
spec.paths[`${prefix}/teams/${name}/stream`] = {
|
|
1115
|
+
post: {
|
|
1116
|
+
tags: ["Teams"],
|
|
1117
|
+
summary: `Stream team: ${name}`,
|
|
1118
|
+
operationId: `streamTeam_${name}`,
|
|
1119
|
+
requestBody: {
|
|
1120
|
+
required: true,
|
|
1121
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/RunRequest" } } }
|
|
1122
|
+
},
|
|
1123
|
+
responses: {
|
|
1124
|
+
"200": {
|
|
1125
|
+
description: "SSE stream",
|
|
1126
|
+
content: { "text/event-stream": { schema: { $ref: "#/components/schemas/StreamChunk" } } }
|
|
1127
|
+
},
|
|
1128
|
+
"400": {
|
|
1129
|
+
description: "Bad request",
|
|
1130
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (routerOpts.workflows && Object.keys(routerOpts.workflows).length > 0) {
|
|
1138
|
+
spec.tags.push({ name: "Workflows", description: "Workflow endpoints for step-based pipelines" });
|
|
1139
|
+
for (const name of Object.keys(routerOpts.workflows)) {
|
|
1140
|
+
spec.paths[`${prefix}/workflows/${name}/run`] = {
|
|
1141
|
+
post: {
|
|
1142
|
+
tags: ["Workflows"],
|
|
1143
|
+
summary: `Run workflow: ${name}`,
|
|
1144
|
+
operationId: `runWorkflow_${name}`,
|
|
1145
|
+
requestBody: {
|
|
1146
|
+
required: true,
|
|
1147
|
+
content: {
|
|
1148
|
+
"application/json": {
|
|
1149
|
+
schema: { $ref: "#/components/schemas/WorkflowRunRequest" }
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
},
|
|
1153
|
+
responses: {
|
|
1154
|
+
"200": { description: "Workflow result", content: { "application/json": { schema: { type: "object" } } } },
|
|
1155
|
+
"500": {
|
|
1156
|
+
description: "Internal server error",
|
|
1157
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return spec;
|
|
1165
|
+
}
|
|
1166
|
+
function serveSwaggerUI(spec) {
|
|
1167
|
+
let swaggerUiExpress;
|
|
1168
|
+
try {
|
|
1169
|
+
swaggerUiExpress = _require4("swagger-ui-express");
|
|
1170
|
+
} catch {
|
|
1171
|
+
throw new Error("swagger-ui-express is required for Swagger UI. Install it: npm install swagger-ui-express");
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
setup: swaggerUiExpress.setup(spec, {
|
|
1175
|
+
customCss: ".swagger-ui .topbar { display: none }",
|
|
1176
|
+
customSiteTitle: spec.info.title
|
|
1177
|
+
}),
|
|
1178
|
+
serve: swaggerUiExpress.serve
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/express/router-factory.ts
|
|
1183
|
+
var import_meta5 = {};
|
|
1184
|
+
var _require5 = (0, import_node_module5.createRequire)(import_meta5.url);
|
|
1185
|
+
function corsMiddleware(origins) {
|
|
1186
|
+
return (req, res, next) => {
|
|
1187
|
+
const origin = req.headers.origin;
|
|
1188
|
+
let allowed = false;
|
|
1189
|
+
if (origins === true || origins === "*") {
|
|
1190
|
+
allowed = true;
|
|
1191
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1192
|
+
} else if (typeof origins === "string") {
|
|
1193
|
+
allowed = origin === origins;
|
|
1194
|
+
if (allowed) res.setHeader("Access-Control-Allow-Origin", origin);
|
|
1195
|
+
} else if (Array.isArray(origins)) {
|
|
1196
|
+
allowed = origins.includes(origin);
|
|
1197
|
+
if (allowed) res.setHeader("Access-Control-Allow-Origin", origin);
|
|
1198
|
+
}
|
|
1199
|
+
if (allowed) {
|
|
1200
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
1201
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key");
|
|
1202
|
+
res.setHeader("Access-Control-Max-Age", "86400");
|
|
1203
|
+
}
|
|
1204
|
+
if (req.method === "OPTIONS") {
|
|
1205
|
+
res.status(204).end();
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
next();
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
function rateLimitMiddleware(config = {}) {
|
|
1212
|
+
const windowMs = config.windowMs ?? 6e4;
|
|
1213
|
+
const max = config.max ?? 100;
|
|
1214
|
+
const hits = /* @__PURE__ */ new Map();
|
|
1215
|
+
return (req, res, next) => {
|
|
1216
|
+
const key = req.ip ?? req.socket?.remoteAddress ?? "unknown";
|
|
1217
|
+
const now = Date.now();
|
|
1218
|
+
const record = hits.get(key);
|
|
1219
|
+
if (!record || now > record.resetTime) {
|
|
1220
|
+
hits.set(key, { count: 1, resetTime: now + windowMs });
|
|
1221
|
+
next();
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
record.count++;
|
|
1225
|
+
if (record.count > max) {
|
|
1226
|
+
res.status(429).json({ error: "Too many requests, please try again later" });
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
next();
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
var API_KEY_HEADERS = {
|
|
1233
|
+
"x-openai-api-key": "openai",
|
|
1234
|
+
"x-google-api-key": "google",
|
|
1235
|
+
"x-anthropic-api-key": "anthropic",
|
|
1236
|
+
"x-api-key": "_generic"
|
|
1237
|
+
};
|
|
1238
|
+
function validateBody(body, fields) {
|
|
1239
|
+
if (!body || typeof body !== "object") throw new Error("Invalid request body");
|
|
1240
|
+
const result = {};
|
|
1241
|
+
for (const [key, type] of Object.entries(fields)) {
|
|
1242
|
+
const val = body[key];
|
|
1243
|
+
const isOptional = type.endsWith("?");
|
|
1244
|
+
const baseType = type.replace("?", "");
|
|
1245
|
+
if (val === void 0 || val === null) {
|
|
1246
|
+
if (!isOptional) throw new Error(`Missing required field: ${key}`);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (baseType === "string" && typeof val !== "string") throw new Error(`Field ${key} must be a string`);
|
|
1250
|
+
if (baseType === "object" && typeof val !== "object") throw new Error(`Field ${key} must be an object`);
|
|
1251
|
+
result[key] = val;
|
|
1252
|
+
}
|
|
1253
|
+
return result;
|
|
1254
|
+
}
|
|
1255
|
+
function extractApiKey(req, agent) {
|
|
1256
|
+
for (const [header, provider] of Object.entries(API_KEY_HEADERS)) {
|
|
1257
|
+
const value = req.headers[header];
|
|
1258
|
+
if (value && (provider === "_generic" || provider === agent.providerId)) {
|
|
1259
|
+
return value;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return req.body?.apiKey ?? void 0;
|
|
1263
|
+
}
|
|
1264
|
+
function createAgentRouter(opts) {
|
|
1265
|
+
if (opts.serve?.length) {
|
|
1266
|
+
const discovered = (0, import_core3.classifyServables)(opts.serve);
|
|
1267
|
+
opts = {
|
|
1268
|
+
...opts,
|
|
1269
|
+
agents: { ...discovered.agents, ...opts.agents },
|
|
1270
|
+
teams: { ...discovered.teams, ...opts.teams },
|
|
1271
|
+
workflows: { ...discovered.workflows, ...opts.workflows }
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
const reg = opts.registry === false ? null : opts.registry ?? import_core3.registry;
|
|
1275
|
+
let express;
|
|
1276
|
+
try {
|
|
1277
|
+
express = _require5("express");
|
|
1278
|
+
} catch {
|
|
1279
|
+
throw new Error("express is required for createAgentRouter. Install it: npm install express");
|
|
1280
|
+
}
|
|
1281
|
+
const router = express.Router();
|
|
1282
|
+
if (opts.cors) {
|
|
1283
|
+
router.use(corsMiddleware(opts.cors));
|
|
1284
|
+
}
|
|
1285
|
+
if (opts.rateLimit) {
|
|
1286
|
+
const config = opts.rateLimit === true ? {} : opts.rateLimit;
|
|
1287
|
+
router.use(rateLimitMiddleware(config));
|
|
1288
|
+
}
|
|
1289
|
+
if (opts.middleware) {
|
|
1290
|
+
for (const mw of opts.middleware) {
|
|
1291
|
+
router.use(mw);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
let uploadMiddleware = null;
|
|
1295
|
+
if (opts.fileUpload) {
|
|
1296
|
+
const uploadOpts = typeof opts.fileUpload === "object" ? opts.fileUpload : {};
|
|
1297
|
+
uploadMiddleware = createFileUploadMiddleware(uploadOpts);
|
|
1298
|
+
}
|
|
1299
|
+
function withUpload(handler) {
|
|
1300
|
+
if (!uploadMiddleware) return handler;
|
|
1301
|
+
return (req, res, next) => {
|
|
1302
|
+
uploadMiddleware(req, res, (err) => {
|
|
1303
|
+
if (err) {
|
|
1304
|
+
return res.status(400).json({ error: err.message });
|
|
1305
|
+
}
|
|
1306
|
+
handler(req, res).catch(next);
|
|
1307
|
+
});
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
if (opts.swagger?.enabled) {
|
|
1311
|
+
const spec = generateOpenAPISpec(opts, opts.swagger);
|
|
1312
|
+
const docsPath = opts.swagger.docsPath ?? "/docs";
|
|
1313
|
+
const specPath = opts.swagger.specPath ?? "/docs/spec.json";
|
|
1314
|
+
router.get(specPath, (_req, res) => {
|
|
1315
|
+
res.json(spec);
|
|
1316
|
+
});
|
|
1317
|
+
try {
|
|
1318
|
+
const { serve, setup } = serveSwaggerUI(spec);
|
|
1319
|
+
router.use(docsPath, serve, setup);
|
|
1320
|
+
} catch (e) {
|
|
1321
|
+
console.warn(`[radaros:transport] Swagger UI disabled: ${e.message}`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
if (opts.agents) {
|
|
1325
|
+
for (const [name, agent] of Object.entries(opts.agents)) {
|
|
1326
|
+
router.post(
|
|
1327
|
+
`/agents/${name}/run`,
|
|
1328
|
+
withUpload(async (req, res) => {
|
|
1329
|
+
try {
|
|
1330
|
+
const validated = validateBody(req.body, {
|
|
1331
|
+
input: "string",
|
|
1332
|
+
sessionId: "string?",
|
|
1333
|
+
userId: "string?"
|
|
1334
|
+
});
|
|
1335
|
+
const input = buildMultiModalInput(req.body, req.files) ?? validated.input;
|
|
1336
|
+
if (!input) {
|
|
1337
|
+
return res.status(400).json({ error: "input is required" });
|
|
1338
|
+
}
|
|
1339
|
+
const sessionId = validated.sessionId;
|
|
1340
|
+
const userId = validated.userId;
|
|
1341
|
+
const apiKey = extractApiKey(req, agent);
|
|
1342
|
+
const result = await agent.run(input, { sessionId, userId, apiKey });
|
|
1343
|
+
res.json(result);
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
res.status(400).json({ error: error.message });
|
|
1346
|
+
}
|
|
1347
|
+
})
|
|
1348
|
+
);
|
|
1349
|
+
router.post(`/agents/${name}/stream`, async (req, res) => {
|
|
1350
|
+
try {
|
|
1351
|
+
const validated = validateBody(req.body, {
|
|
1352
|
+
input: "string",
|
|
1353
|
+
sessionId: "string?",
|
|
1354
|
+
userId: "string?"
|
|
1355
|
+
});
|
|
1356
|
+
const input = validated.input;
|
|
1357
|
+
if (!input) {
|
|
1358
|
+
return res.status(400).json({ error: "input is required" });
|
|
1359
|
+
}
|
|
1360
|
+
const sessionId = validated.sessionId;
|
|
1361
|
+
const userId = validated.userId;
|
|
1362
|
+
const apiKey = extractApiKey(req, agent);
|
|
1363
|
+
res.writeHead(200, {
|
|
1364
|
+
"Content-Type": "text/event-stream",
|
|
1365
|
+
"Cache-Control": "no-cache",
|
|
1366
|
+
Connection: "keep-alive"
|
|
1367
|
+
});
|
|
1368
|
+
const stream = agent.stream(input, { sessionId, userId, apiKey });
|
|
1369
|
+
for await (const chunk of stream) {
|
|
1370
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
1371
|
+
|
|
1372
|
+
`);
|
|
1373
|
+
}
|
|
1374
|
+
res.write("data: [DONE]\n\n");
|
|
1375
|
+
res.end();
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
if (!res.headersSent) {
|
|
1378
|
+
res.status(500).json({ error: error.message });
|
|
1379
|
+
} else {
|
|
1380
|
+
res.write(`data: ${JSON.stringify({ type: "error", error: error.message })}
|
|
1381
|
+
|
|
1382
|
+
`);
|
|
1383
|
+
res.end();
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (opts.teams) {
|
|
1390
|
+
for (const [name, team] of Object.entries(opts.teams)) {
|
|
1391
|
+
router.post(`/teams/${name}/run`, async (req, res) => {
|
|
1392
|
+
try {
|
|
1393
|
+
const validated = validateBody(req.body, {
|
|
1394
|
+
input: "string",
|
|
1395
|
+
sessionId: "string?",
|
|
1396
|
+
userId: "string?"
|
|
1397
|
+
});
|
|
1398
|
+
const input = validated.input;
|
|
1399
|
+
if (!input) {
|
|
1400
|
+
return res.status(400).json({ error: "input is required" });
|
|
1401
|
+
}
|
|
1402
|
+
const sessionId = validated.sessionId;
|
|
1403
|
+
const userId = validated.userId;
|
|
1404
|
+
const apiKey = req.headers["x-api-key"] ?? req.body?.apiKey;
|
|
1405
|
+
const result = await team.run(input, { sessionId, userId, apiKey });
|
|
1406
|
+
res.json(result);
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
res.status(500).json({ error: error.message });
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
router.post(`/teams/${name}/stream`, async (req, res) => {
|
|
1412
|
+
try {
|
|
1413
|
+
const validated = validateBody(req.body, {
|
|
1414
|
+
input: "string",
|
|
1415
|
+
sessionId: "string?",
|
|
1416
|
+
userId: "string?"
|
|
1417
|
+
});
|
|
1418
|
+
const input = validated.input;
|
|
1419
|
+
if (!input) {
|
|
1420
|
+
return res.status(400).json({ error: "input is required" });
|
|
1421
|
+
}
|
|
1422
|
+
const sessionId = validated.sessionId;
|
|
1423
|
+
const userId = validated.userId;
|
|
1424
|
+
const apiKey = req.headers["x-api-key"] ?? req.body?.apiKey;
|
|
1425
|
+
res.writeHead(200, {
|
|
1426
|
+
"Content-Type": "text/event-stream",
|
|
1427
|
+
"Cache-Control": "no-cache",
|
|
1428
|
+
Connection: "keep-alive"
|
|
1429
|
+
});
|
|
1430
|
+
const stream = team.stream(input, { sessionId, userId, apiKey });
|
|
1431
|
+
for await (const chunk of stream) {
|
|
1432
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
1433
|
+
|
|
1434
|
+
`);
|
|
1435
|
+
}
|
|
1436
|
+
res.write("data: [DONE]\n\n");
|
|
1437
|
+
res.end();
|
|
1438
|
+
} catch (error) {
|
|
1439
|
+
if (!res.headersSent) {
|
|
1440
|
+
res.status(500).json({ error: error.message });
|
|
1441
|
+
} else {
|
|
1442
|
+
res.write(`data: ${JSON.stringify({ type: "error", error: error.message })}
|
|
1443
|
+
|
|
1444
|
+
`);
|
|
1445
|
+
res.end();
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
if (opts.workflows) {
|
|
1452
|
+
for (const [name, workflow] of Object.entries(opts.workflows)) {
|
|
1453
|
+
router.post(`/workflows/${name}/run`, async (req, res) => {
|
|
1454
|
+
try {
|
|
1455
|
+
const { sessionId, userId } = req.body ?? {};
|
|
1456
|
+
const result = await workflow.run({ sessionId, userId });
|
|
1457
|
+
res.json(result);
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
res.status(500).json({ error: error.message });
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
if (reg) {
|
|
1465
|
+
router.post(
|
|
1466
|
+
"/agents/:name/run",
|
|
1467
|
+
withUpload(async (req, res) => {
|
|
1468
|
+
const agent = reg.getAgent(req.params.name);
|
|
1469
|
+
if (!agent) return res.status(404).json({ error: `Agent "${req.params.name}" not found` });
|
|
1470
|
+
try {
|
|
1471
|
+
const validated = validateBody(req.body, { input: "string", sessionId: "string?", userId: "string?" });
|
|
1472
|
+
const input = buildMultiModalInput(req.body, req.files) ?? validated.input;
|
|
1473
|
+
if (!input) return res.status(400).json({ error: "input is required" });
|
|
1474
|
+
const apiKey = extractApiKey(req, agent);
|
|
1475
|
+
const result = await agent.run(input, {
|
|
1476
|
+
sessionId: validated.sessionId,
|
|
1477
|
+
userId: validated.userId,
|
|
1478
|
+
apiKey
|
|
1479
|
+
});
|
|
1480
|
+
res.json(result);
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
res.status(400).json({ error: error.message });
|
|
1483
|
+
}
|
|
1484
|
+
})
|
|
1485
|
+
);
|
|
1486
|
+
router.post("/agents/:name/stream", async (req, res) => {
|
|
1487
|
+
const agent = reg.getAgent(req.params.name);
|
|
1488
|
+
if (!agent) return res.status(404).json({ error: `Agent "${req.params.name}" not found` });
|
|
1489
|
+
try {
|
|
1490
|
+
const validated = validateBody(req.body, { input: "string", sessionId: "string?", userId: "string?" });
|
|
1491
|
+
const input = validated.input;
|
|
1492
|
+
if (!input) return res.status(400).json({ error: "input is required" });
|
|
1493
|
+
const apiKey = extractApiKey(req, agent);
|
|
1494
|
+
res.writeHead(200, {
|
|
1495
|
+
"Content-Type": "text/event-stream",
|
|
1496
|
+
"Cache-Control": "no-cache",
|
|
1497
|
+
Connection: "keep-alive"
|
|
1498
|
+
});
|
|
1499
|
+
for await (const chunk of agent.stream(input, {
|
|
1500
|
+
sessionId: validated.sessionId,
|
|
1501
|
+
userId: validated.userId,
|
|
1502
|
+
apiKey
|
|
1503
|
+
})) {
|
|
1504
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
1505
|
+
|
|
1506
|
+
`);
|
|
1507
|
+
}
|
|
1508
|
+
res.write("data: [DONE]\n\n");
|
|
1509
|
+
res.end();
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
if (!res.headersSent) res.status(500).json({ error: error.message });
|
|
1512
|
+
else {
|
|
1513
|
+
res.write(`data: ${JSON.stringify({ type: "error", error: error.message })}
|
|
1514
|
+
|
|
1515
|
+
`);
|
|
1516
|
+
res.end();
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
router.post("/teams/:name/run", async (req, res) => {
|
|
1521
|
+
const team = reg.getTeam(req.params.name);
|
|
1522
|
+
if (!team) return res.status(404).json({ error: `Team "${req.params.name}" not found` });
|
|
1523
|
+
try {
|
|
1524
|
+
const validated = validateBody(req.body, { input: "string", sessionId: "string?", userId: "string?" });
|
|
1525
|
+
const input = validated.input;
|
|
1526
|
+
if (!input) return res.status(400).json({ error: "input is required" });
|
|
1527
|
+
const apiKey = req.headers["x-api-key"] ?? req.body?.apiKey;
|
|
1528
|
+
const result = await team.run(input, {
|
|
1529
|
+
sessionId: validated.sessionId,
|
|
1530
|
+
userId: validated.userId,
|
|
1531
|
+
apiKey
|
|
1532
|
+
});
|
|
1533
|
+
res.json(result);
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
res.status(500).json({ error: error.message });
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
router.post("/teams/:name/stream", async (req, res) => {
|
|
1539
|
+
const team = reg.getTeam(req.params.name);
|
|
1540
|
+
if (!team) return res.status(404).json({ error: `Team "${req.params.name}" not found` });
|
|
1541
|
+
try {
|
|
1542
|
+
const validated = validateBody(req.body, { input: "string", sessionId: "string?", userId: "string?" });
|
|
1543
|
+
const input = validated.input;
|
|
1544
|
+
if (!input) return res.status(400).json({ error: "input is required" });
|
|
1545
|
+
const apiKey = req.headers["x-api-key"] ?? req.body?.apiKey;
|
|
1546
|
+
res.writeHead(200, {
|
|
1547
|
+
"Content-Type": "text/event-stream",
|
|
1548
|
+
"Cache-Control": "no-cache",
|
|
1549
|
+
Connection: "keep-alive"
|
|
1550
|
+
});
|
|
1551
|
+
for await (const chunk of team.stream(input, {
|
|
1552
|
+
sessionId: validated.sessionId,
|
|
1553
|
+
userId: validated.userId,
|
|
1554
|
+
apiKey
|
|
1555
|
+
})) {
|
|
1556
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
1557
|
+
|
|
1558
|
+
`);
|
|
1559
|
+
}
|
|
1560
|
+
res.write("data: [DONE]\n\n");
|
|
1561
|
+
res.end();
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
if (!res.headersSent) res.status(500).json({ error: error.message });
|
|
1564
|
+
else {
|
|
1565
|
+
res.write(`data: ${JSON.stringify({ type: "error", error: error.message })}
|
|
1566
|
+
|
|
1567
|
+
`);
|
|
1568
|
+
res.end();
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
router.post("/workflows/:name/run", async (req, res) => {
|
|
1573
|
+
const workflow = reg.getWorkflow(req.params.name);
|
|
1574
|
+
if (!workflow) return res.status(404).json({ error: `Workflow "${req.params.name}" not found` });
|
|
1575
|
+
try {
|
|
1576
|
+
const { sessionId, userId } = req.body ?? {};
|
|
1577
|
+
const result = await workflow.run({ sessionId, userId });
|
|
1578
|
+
res.json(result);
|
|
1579
|
+
} catch (error) {
|
|
1580
|
+
res.status(500).json({ error: error.message });
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
router.get("/agents", (_req, res) => {
|
|
1584
|
+
res.json(reg.describeAgents());
|
|
1585
|
+
});
|
|
1586
|
+
router.get("/teams", (_req, res) => {
|
|
1587
|
+
res.json(reg.describeTeams());
|
|
1588
|
+
});
|
|
1589
|
+
router.get("/workflows", (_req, res) => {
|
|
1590
|
+
res.json(reg.describeWorkflows());
|
|
1591
|
+
});
|
|
1592
|
+
router.get("/registry", (_req, res) => {
|
|
1593
|
+
res.json(reg.list());
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
if (opts.admin) {
|
|
1597
|
+
const adminOpts = typeof opts.admin === "object" ? opts.admin : {};
|
|
1598
|
+
const { router: adminRouter } = createAdminRouter({
|
|
1599
|
+
mcpManager: adminOpts.mcpManager
|
|
1600
|
+
});
|
|
1601
|
+
router.use("/admin", adminRouter);
|
|
1602
|
+
}
|
|
1603
|
+
const fromToolkits = opts.toolkits ? (0, import_core3.collectToolkitTools)(opts.toolkits) : {};
|
|
1604
|
+
const mergedTools = { ...fromToolkits, ...opts.toolLibrary ?? {} };
|
|
1605
|
+
if (Object.keys(mergedTools).length > 0) {
|
|
1606
|
+
router.get("/tools", (_req, res) => {
|
|
1607
|
+
res.json((0, import_core3.describeToolLibrary)(mergedTools));
|
|
1608
|
+
});
|
|
1609
|
+
router.get("/tools/:name", (req, res) => {
|
|
1610
|
+
const tool = mergedTools[req.params.name];
|
|
1611
|
+
if (!tool) return res.status(404).json({ error: `Tool "${req.params.name}" not found` });
|
|
1612
|
+
res.json({
|
|
1613
|
+
name: tool.name,
|
|
1614
|
+
description: tool.description,
|
|
1615
|
+
parameters: Object.keys(tool.parameters.shape ?? {})
|
|
1616
|
+
});
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
return router;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// src/socketio/browser-gateway.ts
|
|
1623
|
+
function createBrowserGateway(opts) {
|
|
1624
|
+
const ns = opts.io.of(opts.namespace ?? "/radaros-browser");
|
|
1625
|
+
const streamScreenshots = opts.streamScreenshots ?? true;
|
|
1626
|
+
if (opts.authMiddleware) {
|
|
1627
|
+
ns.use(opts.authMiddleware);
|
|
1628
|
+
}
|
|
1629
|
+
const activeRuns = /* @__PURE__ */ new Map();
|
|
1630
|
+
ns.on("connection", (socket) => {
|
|
1631
|
+
socket.on(
|
|
1632
|
+
"browser.start",
|
|
1633
|
+
async (data) => {
|
|
1634
|
+
const agent = opts.agents[data.agentName];
|
|
1635
|
+
if (!agent) {
|
|
1636
|
+
socket.emit("browser.error", {
|
|
1637
|
+
error: `Browser agent "${data.agentName}" not found`
|
|
1638
|
+
});
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
if (activeRuns.has(socket.id)) {
|
|
1642
|
+
socket.emit("browser.error", {
|
|
1643
|
+
error: "A browser task is already running for this connection"
|
|
1644
|
+
});
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
const abort = new AbortController();
|
|
1648
|
+
activeRuns.set(socket.id, abort);
|
|
1649
|
+
const onScreenshot = (ev) => {
|
|
1650
|
+
if (streamScreenshots && !abort.signal.aborted) {
|
|
1651
|
+
socket.emit("browser.screenshot", {
|
|
1652
|
+
data: ev.data.toString("base64"),
|
|
1653
|
+
mimeType: "image/png"
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
const onAction = (ev) => {
|
|
1658
|
+
if (!abort.signal.aborted) {
|
|
1659
|
+
socket.emit("browser.action", { action: ev.action });
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
const onStep = (ev) => {
|
|
1663
|
+
if (!abort.signal.aborted) {
|
|
1664
|
+
socket.emit("browser.step", {
|
|
1665
|
+
index: ev.index,
|
|
1666
|
+
action: ev.action,
|
|
1667
|
+
pageUrl: ev.pageUrl,
|
|
1668
|
+
screenshot: streamScreenshots ? ev.screenshot.toString("base64") : void 0
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
const onError = (ev) => {
|
|
1673
|
+
if (!abort.signal.aborted) {
|
|
1674
|
+
socket.emit("browser.error", { error: ev.error.message });
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
agent.eventBus.on("browser.screenshot", onScreenshot);
|
|
1678
|
+
agent.eventBus.on("browser.action", onAction);
|
|
1679
|
+
agent.eventBus.on("browser.step", onStep);
|
|
1680
|
+
agent.eventBus.on("browser.error", onError);
|
|
1681
|
+
const cleanup = () => {
|
|
1682
|
+
agent.eventBus.off("browser.screenshot", onScreenshot);
|
|
1683
|
+
agent.eventBus.off("browser.action", onAction);
|
|
1684
|
+
agent.eventBus.off("browser.step", onStep);
|
|
1685
|
+
agent.eventBus.off("browser.error", onError);
|
|
1686
|
+
activeRuns.delete(socket.id);
|
|
1687
|
+
};
|
|
1688
|
+
socket.emit("browser.started", {
|
|
1689
|
+
agentName: data.agentName,
|
|
1690
|
+
task: data.task
|
|
1691
|
+
});
|
|
1692
|
+
try {
|
|
1693
|
+
const result = await agent.run(data.task, {
|
|
1694
|
+
startUrl: data.startUrl,
|
|
1695
|
+
apiKey: data.apiKey ?? socket.handshake?.auth?.apiKey,
|
|
1696
|
+
sessionId: data.sessionId
|
|
1697
|
+
});
|
|
1698
|
+
cleanup();
|
|
1699
|
+
if (!abort.signal.aborted) {
|
|
1700
|
+
socket.emit("browser.done", {
|
|
1701
|
+
result: result.result,
|
|
1702
|
+
success: result.success,
|
|
1703
|
+
finalUrl: result.finalUrl,
|
|
1704
|
+
durationMs: result.durationMs,
|
|
1705
|
+
totalSteps: result.steps.length,
|
|
1706
|
+
videoPath: result.videoPath
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
cleanup();
|
|
1711
|
+
if (!abort.signal.aborted) {
|
|
1712
|
+
socket.emit("browser.error", { error: error.message });
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
);
|
|
1717
|
+
socket.on("browser.stop", () => {
|
|
1718
|
+
const abort = activeRuns.get(socket.id);
|
|
1719
|
+
if (abort) {
|
|
1720
|
+
abort.abort();
|
|
1721
|
+
activeRuns.delete(socket.id);
|
|
1722
|
+
socket.emit("browser.stopped");
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
socket.on("disconnect", () => {
|
|
1726
|
+
const abort = activeRuns.get(socket.id);
|
|
1727
|
+
if (abort) {
|
|
1728
|
+
abort.abort();
|
|
1729
|
+
activeRuns.delete(socket.id);
|
|
1730
|
+
}
|
|
1731
|
+
});
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// src/socketio/gateway.ts
|
|
1736
|
+
var import_core4 = require("@radaros/core");
|
|
1737
|
+
function createSocketRateLimiter(maxPerMinute = 60) {
|
|
1738
|
+
return () => {
|
|
1739
|
+
let count = 0;
|
|
1740
|
+
let resetTime = Date.now() + 6e4;
|
|
1741
|
+
return () => {
|
|
1742
|
+
const now = Date.now();
|
|
1743
|
+
if (now > resetTime) {
|
|
1744
|
+
count = 0;
|
|
1745
|
+
resetTime = now + 6e4;
|
|
1746
|
+
}
|
|
1747
|
+
count++;
|
|
1748
|
+
return count <= maxPerMinute;
|
|
1749
|
+
};
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
function createAgentGateway(opts) {
|
|
1753
|
+
if (opts.serve?.length) {
|
|
1754
|
+
const discovered = (0, import_core4.classifyServables)(opts.serve);
|
|
1755
|
+
opts = {
|
|
1756
|
+
...opts,
|
|
1757
|
+
agents: { ...discovered.agents, ...opts.agents },
|
|
1758
|
+
teams: { ...discovered.teams, ...opts.teams }
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
const reg = opts.registry === false ? null : opts.registry ?? import_core4.registry;
|
|
1762
|
+
const ns = opts.io.of(opts.namespace ?? "/radaros");
|
|
1763
|
+
if (opts.authMiddleware) {
|
|
1764
|
+
ns.use(opts.authMiddleware);
|
|
1765
|
+
}
|
|
1766
|
+
const rateLimiterFactory = createSocketRateLimiter(opts.maxRequestsPerMinute ?? 60);
|
|
1767
|
+
ns.on("connection", (socket) => {
|
|
1768
|
+
const checkRate = rateLimiterFactory();
|
|
1769
|
+
socket.on("agent.run", async (data) => {
|
|
1770
|
+
if (!checkRate()) {
|
|
1771
|
+
socket.emit("agent.error", { error: "Rate limit exceeded" });
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (!data || typeof data.input !== "string" || !data.input.trim()) {
|
|
1775
|
+
socket.emit("agent.error", { error: "Invalid input: must be a non-empty string" });
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (data.sessionId !== void 0 && typeof data.sessionId !== "string") {
|
|
1779
|
+
socket.emit("agent.error", { error: "Invalid sessionId: must be a string" });
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
const agent = opts.agents?.[data.name] ?? reg?.getAgent(data.name);
|
|
1783
|
+
if (!agent) {
|
|
1784
|
+
socket.emit("agent.error", {
|
|
1785
|
+
error: `Agent "${data.name}" not found`
|
|
1786
|
+
});
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
try {
|
|
1790
|
+
const apiKey = data.apiKey ?? socket.handshake?.auth?.apiKey;
|
|
1791
|
+
const sessionId = data.sessionId ?? socket.id;
|
|
1792
|
+
let fullText = "";
|
|
1793
|
+
for await (const chunk of agent.stream(data.input, {
|
|
1794
|
+
sessionId,
|
|
1795
|
+
apiKey
|
|
1796
|
+
})) {
|
|
1797
|
+
if (chunk.type === "text") {
|
|
1798
|
+
fullText += chunk.text;
|
|
1799
|
+
socket.emit("agent.chunk", { chunk: chunk.text });
|
|
1800
|
+
} else if (chunk.type === "tool_call_start") {
|
|
1801
|
+
socket.emit("agent.tool.call", {
|
|
1802
|
+
toolName: chunk.toolCall.name,
|
|
1803
|
+
args: null
|
|
1804
|
+
});
|
|
1805
|
+
} else if (chunk.type === "tool_call_end") {
|
|
1806
|
+
socket.emit("agent.tool.done", { toolCallId: chunk.toolCallId });
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
socket.emit("agent.done", { output: { text: fullText } });
|
|
1810
|
+
} catch (error) {
|
|
1811
|
+
socket.emit("agent.error", { error: error.message });
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
socket.on("agents.list", (_data, ack) => {
|
|
1815
|
+
if (reg) {
|
|
1816
|
+
ack?.(reg.describeAgents());
|
|
1817
|
+
} else {
|
|
1818
|
+
const names = Object.keys(opts.agents ?? {});
|
|
1819
|
+
ack?.(names.map((n) => ({ name: n })));
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
socket.on("teams.list", (_data, ack) => {
|
|
1823
|
+
if (reg) {
|
|
1824
|
+
ack?.(reg.describeTeams());
|
|
1825
|
+
} else {
|
|
1826
|
+
const names = Object.keys(opts.teams ?? {});
|
|
1827
|
+
ack?.(names.map((n) => ({ name: n })));
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
socket.on("workflows.list", (_data, ack) => {
|
|
1831
|
+
if (reg) {
|
|
1832
|
+
ack?.(reg.describeWorkflows());
|
|
1833
|
+
} else {
|
|
1834
|
+
ack?.([]);
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
socket.on("registry.list", (_data, ack) => {
|
|
1838
|
+
if (reg) {
|
|
1839
|
+
ack?.(reg.list());
|
|
1840
|
+
} else {
|
|
1841
|
+
ack?.({
|
|
1842
|
+
agents: Object.keys(opts.agents ?? {}),
|
|
1843
|
+
teams: Object.keys(opts.teams ?? {}),
|
|
1844
|
+
workflows: []
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
const fromToolkits = opts.toolkits ? (0, import_core4.collectToolkitTools)(opts.toolkits) : {};
|
|
1849
|
+
const mergedTools = { ...fromToolkits, ...opts.toolLibrary ?? {} };
|
|
1850
|
+
socket.on("tools.list", (_data, ack) => {
|
|
1851
|
+
ack?.((0, import_core4.describeToolLibrary)(mergedTools));
|
|
1852
|
+
});
|
|
1853
|
+
socket.on("tools.get", (data, ack) => {
|
|
1854
|
+
const tool = mergedTools[data?.name];
|
|
1855
|
+
if (!tool) return ack?.({ error: `Tool "${data?.name}" not found` });
|
|
1856
|
+
ack?.({
|
|
1857
|
+
name: tool.name,
|
|
1858
|
+
description: tool.description,
|
|
1859
|
+
parameters: Object.keys(tool.parameters.shape ?? {})
|
|
1860
|
+
});
|
|
1861
|
+
});
|
|
1862
|
+
socket.on("disconnect", () => {
|
|
1863
|
+
});
|
|
1864
|
+
socket.on("team.run", async (data) => {
|
|
1865
|
+
if (!checkRate()) {
|
|
1866
|
+
socket.emit("agent.error", { error: "Rate limit exceeded" });
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
if (!data || typeof data.input !== "string" || !data.input.trim()) {
|
|
1870
|
+
socket.emit("agent.error", { error: "Invalid input: must be a non-empty string" });
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
if (data.sessionId !== void 0 && typeof data.sessionId !== "string") {
|
|
1874
|
+
socket.emit("agent.error", { error: "Invalid sessionId: must be a string" });
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
const team = opts.teams?.[data.name] ?? reg?.getTeam(data.name);
|
|
1878
|
+
if (!team) {
|
|
1879
|
+
socket.emit("agent.error", {
|
|
1880
|
+
error: `Team "${data.name}" not found`
|
|
1881
|
+
});
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
try {
|
|
1885
|
+
const apiKey = data.apiKey ?? socket.handshake?.auth?.apiKey;
|
|
1886
|
+
const result = await team.run(data.input, {
|
|
1887
|
+
sessionId: data.sessionId ?? socket.id,
|
|
1888
|
+
apiKey
|
|
1889
|
+
});
|
|
1890
|
+
socket.emit("agent.done", { output: result });
|
|
1891
|
+
} catch (error) {
|
|
1892
|
+
socket.emit("agent.error", { error: error.message });
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// src/socketio/voice-gateway.ts
|
|
1899
|
+
function createVoiceGateway(opts) {
|
|
1900
|
+
const ns = opts.io.of(opts.namespace ?? "/radaros-voice");
|
|
1901
|
+
if (opts.authMiddleware) {
|
|
1902
|
+
ns.use(opts.authMiddleware);
|
|
1903
|
+
}
|
|
1904
|
+
const activeSessions = /* @__PURE__ */ new Map();
|
|
1905
|
+
ns.on("connection", (socket) => {
|
|
1906
|
+
socket.on(
|
|
1907
|
+
"voice.start",
|
|
1908
|
+
async (data) => {
|
|
1909
|
+
const agent = opts.agents[data.agentName];
|
|
1910
|
+
if (!agent) {
|
|
1911
|
+
socket.emit("voice.error", {
|
|
1912
|
+
error: `Voice agent "${data.agentName}" not found`
|
|
1913
|
+
});
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
if (activeSessions.has(socket.id)) {
|
|
1917
|
+
socket.emit("voice.error", {
|
|
1918
|
+
error: "A voice session is already active for this connection"
|
|
1919
|
+
});
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
try {
|
|
1923
|
+
const apiKey = data.apiKey ?? socket.handshake?.auth?.apiKey;
|
|
1924
|
+
const userId = data.userId ?? socket.handshake?.auth?.userId;
|
|
1925
|
+
const sessionId = data.sessionId ?? socket.handshake?.auth?.sessionId;
|
|
1926
|
+
const session = await agent.connect({
|
|
1927
|
+
apiKey,
|
|
1928
|
+
userId,
|
|
1929
|
+
sessionId
|
|
1930
|
+
});
|
|
1931
|
+
activeSessions.set(socket.id, session);
|
|
1932
|
+
session.on("audio", (ev) => {
|
|
1933
|
+
socket.emit("voice.audio", {
|
|
1934
|
+
data: ev.data.toString("base64"),
|
|
1935
|
+
mimeType: ev.mimeType ?? "audio/pcm"
|
|
1936
|
+
});
|
|
1937
|
+
});
|
|
1938
|
+
session.on("transcript", (ev) => {
|
|
1939
|
+
socket.emit("voice.transcript", {
|
|
1940
|
+
text: ev.text,
|
|
1941
|
+
role: ev.role
|
|
1942
|
+
});
|
|
1943
|
+
});
|
|
1944
|
+
session.on("text", (ev) => {
|
|
1945
|
+
socket.emit("voice.text", { text: ev.text });
|
|
1946
|
+
});
|
|
1947
|
+
session.on("tool_call_start", (ev) => {
|
|
1948
|
+
socket.emit("voice.tool.call", {
|
|
1949
|
+
name: ev.name,
|
|
1950
|
+
args: ev.args
|
|
1951
|
+
});
|
|
1952
|
+
});
|
|
1953
|
+
session.on("tool_result", (ev) => {
|
|
1954
|
+
socket.emit("voice.tool.result", {
|
|
1955
|
+
name: ev.name,
|
|
1956
|
+
result: ev.result
|
|
1957
|
+
});
|
|
1958
|
+
});
|
|
1959
|
+
session.on("usage", (ev) => {
|
|
1960
|
+
socket.emit("voice.usage", ev);
|
|
1961
|
+
});
|
|
1962
|
+
session.on("interrupted", () => {
|
|
1963
|
+
socket.emit("voice.interrupted");
|
|
1964
|
+
});
|
|
1965
|
+
session.on("error", (ev) => {
|
|
1966
|
+
socket.emit("voice.error", { error: ev.error.message });
|
|
1967
|
+
});
|
|
1968
|
+
session.on("disconnected", () => {
|
|
1969
|
+
activeSessions.delete(socket.id);
|
|
1970
|
+
socket.emit("voice.stopped");
|
|
1971
|
+
});
|
|
1972
|
+
socket.emit("voice.started", { userId });
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
socket.emit("voice.error", { error: error.message });
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
);
|
|
1978
|
+
socket.on("voice.audio", (data) => {
|
|
1979
|
+
const session = activeSessions.get(socket.id);
|
|
1980
|
+
if (!session) return;
|
|
1981
|
+
session.sendAudio(Buffer.from(data.data, "base64"));
|
|
1982
|
+
});
|
|
1983
|
+
socket.on("voice.text", (data) => {
|
|
1984
|
+
const session = activeSessions.get(socket.id);
|
|
1985
|
+
if (!session) return;
|
|
1986
|
+
session.sendText(data.text);
|
|
1987
|
+
});
|
|
1988
|
+
socket.on("voice.interrupt", () => {
|
|
1989
|
+
const session = activeSessions.get(socket.id);
|
|
1990
|
+
if (!session) return;
|
|
1991
|
+
session.interrupt();
|
|
1992
|
+
});
|
|
1993
|
+
socket.on("voice.stop", async () => {
|
|
1994
|
+
const session = activeSessions.get(socket.id);
|
|
1995
|
+
if (!session) return;
|
|
1996
|
+
await session.close();
|
|
1997
|
+
activeSessions.delete(socket.id);
|
|
1998
|
+
socket.emit("voice.stopped");
|
|
1999
|
+
});
|
|
2000
|
+
socket.on("disconnect", async () => {
|
|
2001
|
+
const session = activeSessions.get(socket.id);
|
|
2002
|
+
if (session) {
|
|
2003
|
+
try {
|
|
2004
|
+
await session.close();
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
console.warn(
|
|
2007
|
+
"[radaros/voice-gateway] Error closing session on disconnect:",
|
|
2008
|
+
err instanceof Error ? err.message : err
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
activeSessions.delete(socket.id);
|
|
2012
|
+
}
|
|
2013
|
+
});
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2017
|
+
0 && (module.exports = {
|
|
2018
|
+
MCPManager,
|
|
2019
|
+
buildMultiModalInput,
|
|
2020
|
+
createA2AServer,
|
|
2021
|
+
createAdminRouter,
|
|
2022
|
+
createAgentGateway,
|
|
2023
|
+
createAgentRouter,
|
|
2024
|
+
createBrowserGateway,
|
|
2025
|
+
createFileUploadMiddleware,
|
|
2026
|
+
createVoiceGateway,
|
|
2027
|
+
errorHandler,
|
|
2028
|
+
generateAgentCard,
|
|
2029
|
+
generateMultiAgentCard,
|
|
2030
|
+
generateOpenAPISpec,
|
|
2031
|
+
requestLogger
|
|
2032
|
+
});
|