@radaros/transport 0.3.13 → 0.3.15

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/README.md CHANGED
@@ -29,8 +29,9 @@ app.listen(3000);
29
29
 
30
30
  ## Features
31
31
 
32
- - **Express Router** — REST API with streaming support
33
- - **Socket.IO Gateway** — Real-time WebSocket communication
32
+ - **Auto-Discovery** — Reads from the global `Registry` at request time; agents created after server start are immediately available
33
+ - **Express Router** — REST API with streaming support and list endpoints (`GET /agents`, `/teams`, `/workflows`, `/tools`)
34
+ - **Socket.IO Gateway** — Real-time WebSocket communication with dynamic agent/team lookup and tool discovery
34
35
  - **A2A Server** — Agent-to-Agent protocol support
35
36
  - **CORS & Rate Limiting** — Built-in security middleware
36
37
  - **Swagger** — Auto-generated API documentation
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Agent, A2AAgentCard, Team, Workflow, EventBus, VoiceAgent } from '@radaros/core';
1
+ import { Agent, A2AAgentCard, Registry, Servable, Team, Workflow, ToolDef, Toolkit, EventBus, VoiceAgent } from '@radaros/core';
2
2
 
3
3
  interface A2AServerOptions {
4
4
  agents: Record<string, Agent>;
@@ -71,6 +71,24 @@ interface SwaggerOptions {
71
71
  specPath?: string;
72
72
  }
73
73
  interface RouterOptions {
74
+ /**
75
+ * Use a Registry for live auto-discovery. The router creates dynamic routes
76
+ * that resolve agents/teams/workflows at request time — any instance created
77
+ * after the router is mounted is automatically available.
78
+ *
79
+ * When omitted, falls back to the global registry from `@radaros/core`.
80
+ * Pass `false` to disable registry-based routing entirely (use explicit maps only).
81
+ *
82
+ * @example
83
+ * createAgentRouter({ cors: true });
84
+ * new Agent({ name: "bot", model: openai("gpt-4o") }); // immediately routable
85
+ */
86
+ registry?: Registry | false;
87
+ /**
88
+ * Auto-discover agents, teams, and workflows from a mixed array.
89
+ * Each item is classified by its `.kind` and keyed by `.name`.
90
+ */
91
+ serve?: Servable[];
74
92
  agents?: Record<string, Agent>;
75
93
  teams?: Record<string, Team>;
76
94
  workflows?: Record<string, Workflow<any>>;
@@ -86,6 +104,10 @@ interface RouterOptions {
86
104
  windowMs?: number;
87
105
  max?: number;
88
106
  } | boolean;
107
+ /** Named tool library exposed via GET /tools. Tools from toolkits are auto-collected. */
108
+ toolLibrary?: Record<string, ToolDef>;
109
+ /** Toolkit instances whose tools are exposed via GET /tools. Merged with toolLibrary. */
110
+ toolkits?: Toolkit[];
89
111
  }
90
112
 
91
113
  declare function createAgentRouter(opts: RouterOptions): any;
@@ -175,6 +197,24 @@ interface BrowserGatewayOptions {
175
197
  declare function createBrowserGateway(opts: BrowserGatewayOptions): void;
176
198
 
177
199
  interface GatewayOptions {
200
+ /**
201
+ * Use a Registry for live auto-discovery. The gateway resolves agents/teams
202
+ * at event time — any instance created after the gateway starts is automatically
203
+ * reachable.
204
+ *
205
+ * When omitted, falls back to the global registry from `@radaros/core`.
206
+ * Pass `false` to disable registry-based lookup (use explicit maps only).
207
+ *
208
+ * @example
209
+ * createAgentGateway({ io });
210
+ * new Agent({ name: "bot", model: openai("gpt-4o") }); // immediately reachable
211
+ */
212
+ registry?: Registry | false;
213
+ /**
214
+ * Auto-discover agents and teams from a mixed array.
215
+ * Each item is classified by its `.kind` and keyed by `.name`.
216
+ */
217
+ serve?: Servable[];
178
218
  agents?: Record<string, Agent>;
179
219
  teams?: Record<string, Team>;
180
220
  io: any;
@@ -182,6 +222,10 @@ interface GatewayOptions {
182
222
  authMiddleware?: (socket: any, next: (err?: Error) => void) => void;
183
223
  /** Max requests per minute per socket. Default: 60 */
184
224
  maxRequestsPerMinute?: number;
225
+ /** Named tool library exposed via tools.list event. */
226
+ toolLibrary?: Record<string, ToolDef>;
227
+ /** Toolkit instances whose tools are exposed via tools.list. Merged with toolLibrary. */
228
+ toolkits?: Toolkit[];
185
229
  }
186
230
 
187
231
  declare function createAgentGateway(opts: GatewayOptions): void;
package/dist/index.js CHANGED
@@ -431,6 +431,7 @@ function requestLogger(options) {
431
431
 
432
432
  // src/express/router-factory.ts
433
433
  import { createRequire as createRequire4 } from "module";
434
+ import { classifyServables, collectToolkitTools, describeToolLibrary, registry as globalRegistry } from "@radaros/core";
434
435
 
435
436
  // src/express/swagger.ts
436
437
  import { createRequire as createRequire3 } from "module";
@@ -955,6 +956,16 @@ function extractApiKey(req, agent) {
955
956
  return req.body?.apiKey ?? void 0;
956
957
  }
957
958
  function createAgentRouter(opts) {
959
+ if (opts.serve?.length) {
960
+ const discovered = classifyServables(opts.serve);
961
+ opts = {
962
+ ...opts,
963
+ agents: { ...discovered.agents, ...opts.agents },
964
+ teams: { ...discovered.teams, ...opts.teams },
965
+ workflows: { ...discovered.workflows, ...opts.workflows }
966
+ };
967
+ }
968
+ const reg = opts.registry === false ? null : opts.registry ?? globalRegistry;
958
969
  let express;
959
970
  try {
960
971
  express = _require4("express");
@@ -1144,6 +1155,154 @@ function createAgentRouter(opts) {
1144
1155
  });
1145
1156
  }
1146
1157
  }
1158
+ if (reg) {
1159
+ router.post(
1160
+ "/agents/:name/run",
1161
+ withUpload(async (req, res) => {
1162
+ const agent = reg.getAgent(req.params.name);
1163
+ if (!agent) return res.status(404).json({ error: `Agent "${req.params.name}" not found` });
1164
+ try {
1165
+ const validated = validateBody(req.body, { input: "string", sessionId: "string?", userId: "string?" });
1166
+ const input = buildMultiModalInput(req.body, req.files) ?? validated.input;
1167
+ if (!input) return res.status(400).json({ error: "input is required" });
1168
+ const apiKey = extractApiKey(req, agent);
1169
+ const result = await agent.run(input, {
1170
+ sessionId: validated.sessionId,
1171
+ userId: validated.userId,
1172
+ apiKey
1173
+ });
1174
+ res.json(result);
1175
+ } catch (error) {
1176
+ res.status(400).json({ error: error.message });
1177
+ }
1178
+ })
1179
+ );
1180
+ router.post("/agents/:name/stream", async (req, res) => {
1181
+ const agent = reg.getAgent(req.params.name);
1182
+ if (!agent) return res.status(404).json({ error: `Agent "${req.params.name}" not found` });
1183
+ try {
1184
+ const validated = validateBody(req.body, { input: "string", sessionId: "string?", userId: "string?" });
1185
+ const input = validated.input;
1186
+ if (!input) return res.status(400).json({ error: "input is required" });
1187
+ const apiKey = extractApiKey(req, agent);
1188
+ res.writeHead(200, {
1189
+ "Content-Type": "text/event-stream",
1190
+ "Cache-Control": "no-cache",
1191
+ Connection: "keep-alive"
1192
+ });
1193
+ for await (const chunk of agent.stream(input, {
1194
+ sessionId: validated.sessionId,
1195
+ userId: validated.userId,
1196
+ apiKey
1197
+ })) {
1198
+ res.write(`data: ${JSON.stringify(chunk)}
1199
+
1200
+ `);
1201
+ }
1202
+ res.write("data: [DONE]\n\n");
1203
+ res.end();
1204
+ } catch (error) {
1205
+ if (!res.headersSent) res.status(500).json({ error: error.message });
1206
+ else {
1207
+ res.write(`data: ${JSON.stringify({ type: "error", error: error.message })}
1208
+
1209
+ `);
1210
+ res.end();
1211
+ }
1212
+ }
1213
+ });
1214
+ router.post("/teams/:name/run", async (req, res) => {
1215
+ const team = reg.getTeam(req.params.name);
1216
+ if (!team) return res.status(404).json({ error: `Team "${req.params.name}" not found` });
1217
+ try {
1218
+ const validated = validateBody(req.body, { input: "string", sessionId: "string?", userId: "string?" });
1219
+ const input = validated.input;
1220
+ if (!input) return res.status(400).json({ error: "input is required" });
1221
+ const apiKey = req.headers["x-api-key"] ?? req.body?.apiKey;
1222
+ const result = await team.run(input, {
1223
+ sessionId: validated.sessionId,
1224
+ userId: validated.userId,
1225
+ apiKey
1226
+ });
1227
+ res.json(result);
1228
+ } catch (error) {
1229
+ res.status(500).json({ error: error.message });
1230
+ }
1231
+ });
1232
+ router.post("/teams/:name/stream", async (req, res) => {
1233
+ const team = reg.getTeam(req.params.name);
1234
+ if (!team) return res.status(404).json({ error: `Team "${req.params.name}" not found` });
1235
+ try {
1236
+ const validated = validateBody(req.body, { input: "string", sessionId: "string?", userId: "string?" });
1237
+ const input = validated.input;
1238
+ if (!input) return res.status(400).json({ error: "input is required" });
1239
+ const apiKey = req.headers["x-api-key"] ?? req.body?.apiKey;
1240
+ res.writeHead(200, {
1241
+ "Content-Type": "text/event-stream",
1242
+ "Cache-Control": "no-cache",
1243
+ Connection: "keep-alive"
1244
+ });
1245
+ for await (const chunk of team.stream(input, {
1246
+ sessionId: validated.sessionId,
1247
+ userId: validated.userId,
1248
+ apiKey
1249
+ })) {
1250
+ res.write(`data: ${JSON.stringify(chunk)}
1251
+
1252
+ `);
1253
+ }
1254
+ res.write("data: [DONE]\n\n");
1255
+ res.end();
1256
+ } catch (error) {
1257
+ if (!res.headersSent) res.status(500).json({ error: error.message });
1258
+ else {
1259
+ res.write(`data: ${JSON.stringify({ type: "error", error: error.message })}
1260
+
1261
+ `);
1262
+ res.end();
1263
+ }
1264
+ }
1265
+ });
1266
+ router.post("/workflows/:name/run", async (req, res) => {
1267
+ const workflow = reg.getWorkflow(req.params.name);
1268
+ if (!workflow) return res.status(404).json({ error: `Workflow "${req.params.name}" not found` });
1269
+ try {
1270
+ const { sessionId, userId } = req.body ?? {};
1271
+ const result = await workflow.run({ sessionId, userId });
1272
+ res.json(result);
1273
+ } catch (error) {
1274
+ res.status(500).json({ error: error.message });
1275
+ }
1276
+ });
1277
+ router.get("/agents", (_req, res) => {
1278
+ res.json(reg.describeAgents());
1279
+ });
1280
+ router.get("/teams", (_req, res) => {
1281
+ res.json(reg.describeTeams());
1282
+ });
1283
+ router.get("/workflows", (_req, res) => {
1284
+ res.json(reg.describeWorkflows());
1285
+ });
1286
+ router.get("/registry", (_req, res) => {
1287
+ res.json(reg.list());
1288
+ });
1289
+ }
1290
+ const fromToolkits = opts.toolkits ? collectToolkitTools(opts.toolkits) : {};
1291
+ const mergedTools = { ...fromToolkits, ...opts.toolLibrary ?? {} };
1292
+ if (Object.keys(mergedTools).length > 0) {
1293
+ router.get("/tools", (_req, res) => {
1294
+ res.json(describeToolLibrary(mergedTools));
1295
+ });
1296
+ router.get("/tools/:name", (req, res) => {
1297
+ const tool = mergedTools[req.params.name];
1298
+ if (!tool) return res.status(404).json({ error: `Tool "${req.params.name}" not found` });
1299
+ res.json({
1300
+ name: tool.name,
1301
+ description: tool.description,
1302
+ parameters: Object.keys(tool.parameters.shape ?? {})
1303
+ });
1304
+ });
1305
+ }
1147
1306
  return router;
1148
1307
  }
1149
1308
 
@@ -1261,6 +1420,7 @@ function createBrowserGateway(opts) {
1261
1420
  }
1262
1421
 
1263
1422
  // src/socketio/gateway.ts
1423
+ import { classifyServables as classifyServables2, collectToolkitTools as collectToolkitTools2, describeToolLibrary as describeToolLibrary2, registry as globalRegistry2 } from "@radaros/core";
1264
1424
  function createSocketRateLimiter(maxPerMinute = 60) {
1265
1425
  return () => {
1266
1426
  let count = 0;
@@ -1277,6 +1437,15 @@ function createSocketRateLimiter(maxPerMinute = 60) {
1277
1437
  };
1278
1438
  }
1279
1439
  function createAgentGateway(opts) {
1440
+ if (opts.serve?.length) {
1441
+ const discovered = classifyServables2(opts.serve);
1442
+ opts = {
1443
+ ...opts,
1444
+ agents: { ...discovered.agents, ...opts.agents },
1445
+ teams: { ...discovered.teams, ...opts.teams }
1446
+ };
1447
+ }
1448
+ const reg = opts.registry === false ? null : opts.registry ?? globalRegistry2;
1280
1449
  const ns = opts.io.of(opts.namespace ?? "/radaros");
1281
1450
  if (opts.authMiddleware) {
1282
1451
  ns.use(opts.authMiddleware);
@@ -1297,7 +1466,7 @@ function createAgentGateway(opts) {
1297
1466
  socket.emit("agent.error", { error: "Invalid sessionId: must be a string" });
1298
1467
  return;
1299
1468
  }
1300
- const agent = opts.agents?.[data.name];
1469
+ const agent = opts.agents?.[data.name] ?? reg?.getAgent(data.name);
1301
1470
  if (!agent) {
1302
1471
  socket.emit("agent.error", {
1303
1472
  error: `Agent "${data.name}" not found`
@@ -1329,10 +1498,70 @@ function createAgentGateway(opts) {
1329
1498
  socket.emit("agent.error", { error: error.message });
1330
1499
  }
1331
1500
  });
1501
+ socket.on("agents.list", (_data, ack) => {
1502
+ if (reg) {
1503
+ ack?.(reg.describeAgents());
1504
+ } else {
1505
+ const names = Object.keys(opts.agents ?? {});
1506
+ ack?.(names.map((n) => ({ name: n })));
1507
+ }
1508
+ });
1509
+ socket.on("teams.list", (_data, ack) => {
1510
+ if (reg) {
1511
+ ack?.(reg.describeTeams());
1512
+ } else {
1513
+ const names = Object.keys(opts.teams ?? {});
1514
+ ack?.(names.map((n) => ({ name: n })));
1515
+ }
1516
+ });
1517
+ socket.on("workflows.list", (_data, ack) => {
1518
+ if (reg) {
1519
+ ack?.(reg.describeWorkflows());
1520
+ } else {
1521
+ ack?.([]);
1522
+ }
1523
+ });
1524
+ socket.on("registry.list", (_data, ack) => {
1525
+ if (reg) {
1526
+ ack?.(reg.list());
1527
+ } else {
1528
+ ack?.({
1529
+ agents: Object.keys(opts.agents ?? {}),
1530
+ teams: Object.keys(opts.teams ?? {}),
1531
+ workflows: []
1532
+ });
1533
+ }
1534
+ });
1535
+ const fromToolkits = opts.toolkits ? collectToolkitTools2(opts.toolkits) : {};
1536
+ const mergedTools = { ...fromToolkits, ...opts.toolLibrary ?? {} };
1537
+ socket.on("tools.list", (_data, ack) => {
1538
+ ack?.(describeToolLibrary2(mergedTools));
1539
+ });
1540
+ socket.on("tools.get", (data, ack) => {
1541
+ const tool = mergedTools[data?.name];
1542
+ if (!tool) return ack?.({ error: `Tool "${data?.name}" not found` });
1543
+ ack?.({
1544
+ name: tool.name,
1545
+ description: tool.description,
1546
+ parameters: Object.keys(tool.parameters.shape ?? {})
1547
+ });
1548
+ });
1332
1549
  socket.on("disconnect", () => {
1333
1550
  });
1334
1551
  socket.on("team.run", async (data) => {
1335
- const team = opts.teams?.[data.name];
1552
+ if (!checkRate()) {
1553
+ socket.emit("agent.error", { error: "Rate limit exceeded" });
1554
+ return;
1555
+ }
1556
+ if (!data || typeof data.input !== "string" || !data.input.trim()) {
1557
+ socket.emit("agent.error", { error: "Invalid input: must be a non-empty string" });
1558
+ return;
1559
+ }
1560
+ if (data.sessionId !== void 0 && typeof data.sessionId !== "string") {
1561
+ socket.emit("agent.error", { error: "Invalid sessionId: must be a string" });
1562
+ return;
1563
+ }
1564
+ const team = opts.teams?.[data.name] ?? reg?.getTeam(data.name);
1336
1565
  if (!team) {
1337
1566
  socket.emit("agent.error", {
1338
1567
  error: `Team "${data.name}" not found`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radaros/transport",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "description": "HTTP and WebSocket transport layer for RadarOS agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -39,7 +39,7 @@
39
39
  "typescript": "^5.6.0"
40
40
  },
41
41
  "peerDependencies": {
42
- "@radaros/core": "^0.3.13",
42
+ "@radaros/core": "^0.3.15",
43
43
  "@types/express": "^4.0.0 || ^5.0.0",
44
44
  "express": "^4.0.0 || ^5.0.0",
45
45
  "multer": ">=2.0.0",