@radaros/transport 0.3.12 → 0.3.14

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 ADDED
@@ -0,0 +1,45 @@
1
+ # @radaros/transport
2
+
3
+ HTTP and WebSocket transport layer for deploying RadarOS agents as APIs.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @radaros/transport
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import express from "express";
15
+ import { Agent, openai } from "@radaros/core";
16
+ import { createAgentRouter } from "@radaros/transport";
17
+
18
+ const app = express();
19
+ app.use(express.json());
20
+
21
+ const agent = new Agent({
22
+ name: "assistant",
23
+ model: openai("gpt-4o"),
24
+ });
25
+
26
+ app.use("/api", createAgentRouter({ agents: { assistant: agent } }));
27
+ app.listen(3000);
28
+ ```
29
+
30
+ ## Features
31
+
32
+ - **Express Router** — REST API with streaming support
33
+ - **Socket.IO Gateway** — Real-time WebSocket communication
34
+ - **A2A Server** — Agent-to-Agent protocol support
35
+ - **CORS & Rate Limiting** — Built-in security middleware
36
+ - **Swagger** — Auto-generated API documentation
37
+ - **File Upload** — Multipart form data support
38
+
39
+ ## Documentation
40
+
41
+ Full docs at [radaros.mintlify.dev](https://radaros.mintlify.dev)
42
+
43
+ ## License
44
+
45
+ MIT
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, EventBus, VoiceAgent } from '@radaros/core';
2
2
 
3
3
  interface A2AServerOptions {
4
4
  agents: Record<string, Agent>;
@@ -42,8 +42,12 @@ interface FileUploadOptions {
42
42
  declare function createFileUploadMiddleware(opts?: FileUploadOptions): any;
43
43
  declare function buildMultiModalInput(body: any, files?: any[]): string | any[];
44
44
 
45
- declare function errorHandler(): (err: any, _req: any, res: any, _next: any) => void;
46
- declare function requestLogger(): (req: any, _res: any, next: any) => void;
45
+ declare function errorHandler(options?: {
46
+ logger?: Pick<Console, "error">;
47
+ }): (err: any, _req: any, res: any, _next: any) => void;
48
+ declare function requestLogger(options?: {
49
+ logger?: Pick<Console, "log">;
50
+ }): (req: any, _res: any, next: any) => void;
47
51
 
48
52
  interface SwaggerOptions {
49
53
  /** Enable Swagger UI at /docs. Default: false */
@@ -67,6 +71,24 @@ interface SwaggerOptions {
67
71
  specPath?: string;
68
72
  }
69
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[];
70
92
  agents?: Record<string, Agent>;
71
93
  teams?: Record<string, Team>;
72
94
  workflows?: Record<string, Workflow<any>>;
@@ -75,6 +97,13 @@ interface RouterOptions {
75
97
  swagger?: SwaggerOptions;
76
98
  /** File upload configuration for multi-modal inputs */
77
99
  fileUpload?: boolean | FileUploadOptions;
100
+ /** CORS configuration. Pass true or '*' for permissive, a string for a single origin, or an array for multiple origins. */
101
+ cors?: string | string[] | boolean;
102
+ /** Rate limiting configuration. Pass true for defaults (100 req/min), or an object to customize. */
103
+ rateLimit?: {
104
+ windowMs?: number;
105
+ max?: number;
106
+ } | boolean;
78
107
  }
79
108
 
80
109
  declare function createAgentRouter(opts: RouterOptions): any;
@@ -164,11 +193,31 @@ interface BrowserGatewayOptions {
164
193
  declare function createBrowserGateway(opts: BrowserGatewayOptions): void;
165
194
 
166
195
  interface GatewayOptions {
196
+ /**
197
+ * Use a Registry for live auto-discovery. The gateway resolves agents/teams
198
+ * at event time — any instance created after the gateway starts is automatically
199
+ * reachable.
200
+ *
201
+ * When omitted, falls back to the global registry from `@radaros/core`.
202
+ * Pass `false` to disable registry-based lookup (use explicit maps only).
203
+ *
204
+ * @example
205
+ * createAgentGateway({ io });
206
+ * new Agent({ name: "bot", model: openai("gpt-4o") }); // immediately reachable
207
+ */
208
+ registry?: Registry | false;
209
+ /**
210
+ * Auto-discover agents and teams from a mixed array.
211
+ * Each item is classified by its `.kind` and keyed by `.name`.
212
+ */
213
+ serve?: Servable[];
167
214
  agents?: Record<string, Agent>;
168
215
  teams?: Record<string, Team>;
169
216
  io: any;
170
217
  namespace?: string;
171
218
  authMiddleware?: (socket: any, next: (err?: Error) => void) => void;
219
+ /** Max requests per minute per socket. Default: 60 */
220
+ maxRequestsPerMinute?: number;
172
221
  }
173
222
 
174
223
  declare function createAgentGateway(opts: GatewayOptions): void;
package/dist/index.js CHANGED
@@ -412,23 +412,26 @@ function buildMultiModalInput(body, files) {
412
412
  }
413
413
 
414
414
  // src/express/middleware.ts
415
- function errorHandler() {
415
+ function errorHandler(options) {
416
+ const log = options?.logger ?? console;
416
417
  return (err, _req, res, _next) => {
417
- console.error("[radaros:transport] Error:", err.message);
418
+ log.error("[radaros:transport] Error:", err.message);
418
419
  res.status(err.statusCode ?? 500).json({
419
420
  error: err.message ?? "Internal server error"
420
421
  });
421
422
  };
422
423
  }
423
- function requestLogger() {
424
+ function requestLogger(options) {
425
+ const log = options?.logger ?? console;
424
426
  return (req, _res, next) => {
425
- console.log(`[radaros:transport] ${req.method} ${req.path}`);
427
+ log.log(`[radaros:transport] ${req.method} ${req.path}`);
426
428
  next();
427
429
  };
428
430
  }
429
431
 
430
432
  // src/express/router-factory.ts
431
433
  import { createRequire as createRequire4 } from "module";
434
+ import { classifyServables, registry as globalRegistry } from "@radaros/core";
432
435
 
433
436
  // src/express/swagger.ts
434
437
  import { createRequire as createRequire3 } from "module";
@@ -873,6 +876,53 @@ function serveSwaggerUI(spec) {
873
876
 
874
877
  // src/express/router-factory.ts
875
878
  var _require4 = createRequire4(import.meta.url);
879
+ function corsMiddleware(origins) {
880
+ return (req, res, next) => {
881
+ const origin = req.headers.origin;
882
+ let allowed = false;
883
+ if (origins === true || origins === "*") {
884
+ allowed = true;
885
+ res.setHeader("Access-Control-Allow-Origin", "*");
886
+ } else if (typeof origins === "string") {
887
+ allowed = origin === origins;
888
+ if (allowed) res.setHeader("Access-Control-Allow-Origin", origin);
889
+ } else if (Array.isArray(origins)) {
890
+ allowed = origins.includes(origin);
891
+ if (allowed) res.setHeader("Access-Control-Allow-Origin", origin);
892
+ }
893
+ if (allowed) {
894
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
895
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key");
896
+ res.setHeader("Access-Control-Max-Age", "86400");
897
+ }
898
+ if (req.method === "OPTIONS") {
899
+ res.status(204).end();
900
+ return;
901
+ }
902
+ next();
903
+ };
904
+ }
905
+ function rateLimitMiddleware(config = {}) {
906
+ const windowMs = config.windowMs ?? 6e4;
907
+ const max = config.max ?? 100;
908
+ const hits = /* @__PURE__ */ new Map();
909
+ return (req, res, next) => {
910
+ const key = req.ip ?? req.socket?.remoteAddress ?? "unknown";
911
+ const now = Date.now();
912
+ const record = hits.get(key);
913
+ if (!record || now > record.resetTime) {
914
+ hits.set(key, { count: 1, resetTime: now + windowMs });
915
+ next();
916
+ return;
917
+ }
918
+ record.count++;
919
+ if (record.count > max) {
920
+ res.status(429).json({ error: "Too many requests, please try again later" });
921
+ return;
922
+ }
923
+ next();
924
+ };
925
+ }
876
926
  var API_KEY_HEADERS = {
877
927
  "x-openai-api-key": "openai",
878
928
  "x-google-api-key": "google",
@@ -906,6 +956,16 @@ function extractApiKey(req, agent) {
906
956
  return req.body?.apiKey ?? void 0;
907
957
  }
908
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;
909
969
  let express;
910
970
  try {
911
971
  express = _require4("express");
@@ -913,6 +973,13 @@ function createAgentRouter(opts) {
913
973
  throw new Error("express is required for createAgentRouter. Install it: npm install express");
914
974
  }
915
975
  const router = express.Router();
976
+ if (opts.cors) {
977
+ router.use(corsMiddleware(opts.cors));
978
+ }
979
+ if (opts.rateLimit) {
980
+ const config = opts.rateLimit === true ? {} : opts.rateLimit;
981
+ router.use(rateLimitMiddleware(config));
982
+ }
916
983
  if (opts.middleware) {
917
984
  for (const mw of opts.middleware) {
918
985
  router.use(mw);
@@ -1088,6 +1155,138 @@ function createAgentRouter(opts) {
1088
1155
  });
1089
1156
  }
1090
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
+ }
1091
1290
  return router;
1092
1291
  }
1093
1292
 
@@ -1205,13 +1404,44 @@ function createBrowserGateway(opts) {
1205
1404
  }
1206
1405
 
1207
1406
  // src/socketio/gateway.ts
1407
+ import { classifyServables as classifyServables2, registry as globalRegistry2 } from "@radaros/core";
1408
+ function createSocketRateLimiter(maxPerMinute = 60) {
1409
+ return () => {
1410
+ let count = 0;
1411
+ let resetTime = Date.now() + 6e4;
1412
+ return () => {
1413
+ const now = Date.now();
1414
+ if (now > resetTime) {
1415
+ count = 0;
1416
+ resetTime = now + 6e4;
1417
+ }
1418
+ count++;
1419
+ return count <= maxPerMinute;
1420
+ };
1421
+ };
1422
+ }
1208
1423
  function createAgentGateway(opts) {
1424
+ if (opts.serve?.length) {
1425
+ const discovered = classifyServables2(opts.serve);
1426
+ opts = {
1427
+ ...opts,
1428
+ agents: { ...discovered.agents, ...opts.agents },
1429
+ teams: { ...discovered.teams, ...opts.teams }
1430
+ };
1431
+ }
1432
+ const reg = opts.registry === false ? null : opts.registry ?? globalRegistry2;
1209
1433
  const ns = opts.io.of(opts.namespace ?? "/radaros");
1210
1434
  if (opts.authMiddleware) {
1211
1435
  ns.use(opts.authMiddleware);
1212
1436
  }
1437
+ const rateLimiterFactory = createSocketRateLimiter(opts.maxRequestsPerMinute ?? 60);
1213
1438
  ns.on("connection", (socket) => {
1439
+ const checkRate = rateLimiterFactory();
1214
1440
  socket.on("agent.run", async (data) => {
1441
+ if (!checkRate()) {
1442
+ socket.emit("agent.error", { error: "Rate limit exceeded" });
1443
+ return;
1444
+ }
1215
1445
  if (!data || typeof data.input !== "string" || !data.input.trim()) {
1216
1446
  socket.emit("agent.error", { error: "Invalid input: must be a non-empty string" });
1217
1447
  return;
@@ -1220,7 +1450,7 @@ function createAgentGateway(opts) {
1220
1450
  socket.emit("agent.error", { error: "Invalid sessionId: must be a string" });
1221
1451
  return;
1222
1452
  }
1223
- const agent = opts.agents?.[data.name];
1453
+ const agent = opts.agents?.[data.name] ?? reg?.getAgent(data.name);
1224
1454
  if (!agent) {
1225
1455
  socket.emit("agent.error", {
1226
1456
  error: `Agent "${data.name}" not found`
@@ -1255,7 +1485,19 @@ function createAgentGateway(opts) {
1255
1485
  socket.on("disconnect", () => {
1256
1486
  });
1257
1487
  socket.on("team.run", async (data) => {
1258
- const team = opts.teams?.[data.name];
1488
+ if (!checkRate()) {
1489
+ socket.emit("agent.error", { error: "Rate limit exceeded" });
1490
+ return;
1491
+ }
1492
+ if (!data || typeof data.input !== "string" || !data.input.trim()) {
1493
+ socket.emit("agent.error", { error: "Invalid input: must be a non-empty string" });
1494
+ return;
1495
+ }
1496
+ if (data.sessionId !== void 0 && typeof data.sessionId !== "string") {
1497
+ socket.emit("agent.error", { error: "Invalid sessionId: must be a string" });
1498
+ return;
1499
+ }
1500
+ const team = opts.teams?.[data.name] ?? reg?.getTeam(data.name);
1259
1501
  if (!team) {
1260
1502
  socket.emit("agent.error", {
1261
1503
  error: `Team "${data.name}" not found`
@@ -1380,7 +1622,11 @@ function createVoiceGateway(opts) {
1380
1622
  if (session) {
1381
1623
  try {
1382
1624
  await session.close();
1383
- } catch {
1625
+ } catch (err) {
1626
+ console.warn(
1627
+ "[radaros/voice-gateway] Error closing session on disconnect:",
1628
+ err instanceof Error ? err.message : err
1629
+ );
1384
1630
  }
1385
1631
  activeSessions.delete(socket.id);
1386
1632
  }
package/package.json CHANGED
@@ -1,6 +1,21 @@
1
1
  {
2
2
  "name": "@radaros/transport",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
+ "description": "HTTP and WebSocket transport layer for RadarOS agents",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/xhipment/agentOs.git",
9
+ "directory": "packages/transport"
10
+ },
11
+ "keywords": [
12
+ "ai",
13
+ "agents",
14
+ "express",
15
+ "socketio",
16
+ "api",
17
+ "transport"
18
+ ],
4
19
  "type": "module",
5
20
  "main": "./dist/index.js",
6
21
  "types": "./dist/index.d.ts",
@@ -24,7 +39,7 @@
24
39
  "typescript": "^5.6.0"
25
40
  },
26
41
  "peerDependencies": {
27
- "@radaros/core": "^0.3.12",
42
+ "@radaros/core": "^0.3.14",
28
43
  "@types/express": "^4.0.0 || ^5.0.0",
29
44
  "express": "^4.0.0 || ^5.0.0",
30
45
  "multer": ">=2.0.0",