@radaros/transport 0.1.0

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.
@@ -0,0 +1,424 @@
1
+ import { createRequire } from "node:module";
2
+ import type { RouterOptions, SwaggerOptions } from "./types.js";
3
+
4
+ const _require = createRequire(import.meta.url);
5
+
6
+ interface OpenAPISpec {
7
+ openapi: string;
8
+ info: {
9
+ title: string;
10
+ description: string;
11
+ version: string;
12
+ };
13
+ servers?: Array<{ url: string; description?: string }>;
14
+ paths: Record<string, Record<string, unknown>>;
15
+ components: {
16
+ schemas: Record<string, unknown>;
17
+ securitySchemes?: Record<string, unknown>;
18
+ };
19
+ security?: Array<Record<string, string[]>>;
20
+ tags: Array<{ name: string; description: string }>;
21
+ }
22
+
23
+ const SCHEMAS = {
24
+ RunRequest: {
25
+ type: "object",
26
+ required: ["input"],
27
+ properties: {
28
+ input: {
29
+ oneOf: [
30
+ { type: "string", description: "Text input" },
31
+ {
32
+ type: "array",
33
+ description: "Multi-modal content parts",
34
+ items: {
35
+ oneOf: [
36
+ {
37
+ type: "object",
38
+ properties: {
39
+ type: { type: "string", enum: ["text"] },
40
+ text: { type: "string" },
41
+ },
42
+ required: ["type", "text"],
43
+ },
44
+ {
45
+ type: "object",
46
+ properties: {
47
+ type: { type: "string", enum: ["image"] },
48
+ data: { type: "string", description: "Base64 data or URL" },
49
+ mimeType: { type: "string", enum: ["image/png", "image/jpeg", "image/gif", "image/webp"] },
50
+ },
51
+ required: ["type", "data"],
52
+ },
53
+ {
54
+ type: "object",
55
+ properties: {
56
+ type: { type: "string", enum: ["audio"] },
57
+ data: { type: "string", description: "Base64 data or URL" },
58
+ mimeType: { type: "string" },
59
+ },
60
+ required: ["type", "data"],
61
+ },
62
+ {
63
+ type: "object",
64
+ properties: {
65
+ type: { type: "string", enum: ["file"] },
66
+ data: { type: "string", description: "Base64 data or URL" },
67
+ mimeType: { type: "string" },
68
+ fileName: { type: "string" },
69
+ },
70
+ required: ["type", "data"],
71
+ },
72
+ ],
73
+ },
74
+ },
75
+ ],
76
+ },
77
+ sessionId: { type: "string", description: "Session ID for conversation continuity" },
78
+ userId: { type: "string", description: "User identifier" },
79
+ },
80
+ },
81
+ MultipartRunRequest: {
82
+ type: "object",
83
+ properties: {
84
+ input: { type: "string", description: "Text input" },
85
+ sessionId: { type: "string" },
86
+ userId: { type: "string" },
87
+ files: {
88
+ type: "array",
89
+ items: { type: "string", format: "binary" },
90
+ description: "Files to include as multi-modal input (images, audio, documents)",
91
+ },
92
+ },
93
+ required: ["input"],
94
+ },
95
+ RunOutput: {
96
+ type: "object",
97
+ properties: {
98
+ text: { type: "string", description: "Agent response text" },
99
+ toolCalls: {
100
+ type: "array",
101
+ items: {
102
+ type: "object",
103
+ properties: {
104
+ toolCallId: { type: "string" },
105
+ toolName: { type: "string" },
106
+ result: {},
107
+ },
108
+ },
109
+ },
110
+ usage: {
111
+ type: "object",
112
+ properties: {
113
+ promptTokens: { type: "number" },
114
+ completionTokens: { type: "number" },
115
+ totalTokens: { type: "number" },
116
+ },
117
+ },
118
+ structured: { description: "Parsed structured output (if schema is configured)" },
119
+ durationMs: { type: "number" },
120
+ },
121
+ },
122
+ StreamChunk: {
123
+ type: "object",
124
+ description: "Server-Sent Event data",
125
+ properties: {
126
+ type: { type: "string", enum: ["text", "tool_call_start", "tool_call_delta", "tool_call_end", "finish"] },
127
+ text: { type: "string" },
128
+ },
129
+ },
130
+ Error: {
131
+ type: "object",
132
+ properties: {
133
+ error: { type: "string" },
134
+ },
135
+ },
136
+ WorkflowRunRequest: {
137
+ type: "object",
138
+ properties: {
139
+ sessionId: { type: "string" },
140
+ userId: { type: "string" },
141
+ },
142
+ },
143
+ };
144
+
145
+ function buildAgentDescription(agent: any): string {
146
+ const parts: string[] = [];
147
+ parts.push(`**Model:** \`${agent.providerId}/${agent.modelId}\``);
148
+
149
+ if (typeof agent.instructions === "string") {
150
+ const instr = agent.instructions.length > 200
151
+ ? agent.instructions.slice(0, 200) + "…"
152
+ : agent.instructions;
153
+ parts.push(`**Instructions:** ${instr}`);
154
+ }
155
+
156
+ if (agent.tools?.length > 0) {
157
+ const toolNames = agent.tools.map((t: any) => `\`${t.name}\``).join(", ");
158
+ parts.push(`**Tools:** ${toolNames}`);
159
+ }
160
+
161
+ if (agent.hasStructuredOutput) {
162
+ parts.push("**Structured Output:** Enabled");
163
+ }
164
+
165
+ return parts.join("\n\n");
166
+ }
167
+
168
+ export function generateOpenAPISpec(
169
+ routerOpts: RouterOptions,
170
+ swaggerOpts: SwaggerOptions = {}
171
+ ): OpenAPISpec {
172
+ const prefix = swaggerOpts.routePrefix ?? "";
173
+
174
+ const providers = new Set<string>();
175
+ if (routerOpts.agents) {
176
+ for (const agent of Object.values(routerOpts.agents)) {
177
+ providers.add((agent as any).providerId ?? "unknown");
178
+ }
179
+ }
180
+
181
+ const securitySchemes: Record<string, unknown> = {};
182
+ const securityRequirements: Array<Record<string, string[]>> = [];
183
+
184
+ if (providers.has("openai")) {
185
+ securitySchemes.OpenAIKey = {
186
+ type: "apiKey",
187
+ in: "header",
188
+ name: "x-openai-api-key",
189
+ description: "OpenAI API key (sk-...)",
190
+ };
191
+ securityRequirements.push({ OpenAIKey: [] });
192
+ }
193
+ if (providers.has("google")) {
194
+ securitySchemes.GoogleKey = {
195
+ type: "apiKey",
196
+ in: "header",
197
+ name: "x-google-api-key",
198
+ description: "Google AI API key (AIza...)",
199
+ };
200
+ securityRequirements.push({ GoogleKey: [] });
201
+ }
202
+ if (providers.has("anthropic")) {
203
+ securitySchemes.AnthropicKey = {
204
+ type: "apiKey",
205
+ in: "header",
206
+ name: "x-anthropic-api-key",
207
+ description: "Anthropic API key (sk-ant-...)",
208
+ };
209
+ securityRequirements.push({ AnthropicKey: [] });
210
+ }
211
+
212
+ securitySchemes.GenericKey = {
213
+ type: "apiKey",
214
+ in: "header",
215
+ name: "x-api-key",
216
+ description: "Generic API key (used if provider-specific key is not set)",
217
+ };
218
+
219
+ const spec: OpenAPISpec = {
220
+ openapi: "3.0.3",
221
+ info: {
222
+ title: swaggerOpts.title ?? "RadarOS API",
223
+ description: swaggerOpts.description ?? "Auto-generated API documentation for RadarOS agents, teams, and workflows.",
224
+ version: swaggerOpts.version ?? "1.0.0",
225
+ },
226
+ paths: {},
227
+ components: {
228
+ schemas: SCHEMAS as Record<string, unknown>,
229
+ securitySchemes,
230
+ },
231
+ security: securityRequirements,
232
+ tags: [],
233
+ };
234
+
235
+ if (swaggerOpts.servers) {
236
+ spec.servers = swaggerOpts.servers;
237
+ }
238
+
239
+ if (routerOpts.agents && Object.keys(routerOpts.agents).length > 0) {
240
+ spec.tags.push({ name: "Agents", description: "Agent endpoints for running and streaming AI agents" });
241
+
242
+ for (const [name, agent] of Object.entries(routerOpts.agents)) {
243
+ const agentDesc = buildAgentDescription(agent);
244
+
245
+ spec.paths[`${prefix}/agents/${name}/run`] = {
246
+ post: {
247
+ tags: ["Agents"],
248
+ summary: `Run agent: ${name}`,
249
+ description: agentDesc,
250
+ operationId: `runAgent_${name}`,
251
+ requestBody: {
252
+ required: true,
253
+ content: {
254
+ "application/json": {
255
+ schema: { $ref: "#/components/schemas/RunRequest" },
256
+ },
257
+ "multipart/form-data": {
258
+ schema: { $ref: "#/components/schemas/MultipartRunRequest" },
259
+ },
260
+ },
261
+ },
262
+ responses: {
263
+ "200": {
264
+ description: "Agent run result",
265
+ content: {
266
+ "application/json": {
267
+ schema: { $ref: "#/components/schemas/RunOutput" },
268
+ },
269
+ },
270
+ },
271
+ "400": {
272
+ description: "Bad request",
273
+ content: {
274
+ "application/json": {
275
+ schema: { $ref: "#/components/schemas/Error" },
276
+ },
277
+ },
278
+ },
279
+ "500": {
280
+ description: "Internal server error",
281
+ content: {
282
+ "application/json": {
283
+ schema: { $ref: "#/components/schemas/Error" },
284
+ },
285
+ },
286
+ },
287
+ },
288
+ },
289
+ };
290
+
291
+ spec.paths[`${prefix}/agents/${name}/stream`] = {
292
+ post: {
293
+ tags: ["Agents"],
294
+ summary: `Stream agent: ${name}`,
295
+ description: `Stream responses from agent **${name}** via Server-Sent Events.\n\n${agentDesc}`,
296
+ operationId: `streamAgent_${name}`,
297
+ requestBody: {
298
+ required: true,
299
+ content: {
300
+ "application/json": {
301
+ schema: { $ref: "#/components/schemas/RunRequest" },
302
+ },
303
+ },
304
+ },
305
+ responses: {
306
+ "200": {
307
+ description: "SSE stream of agent chunks",
308
+ content: {
309
+ "text/event-stream": {
310
+ schema: { $ref: "#/components/schemas/StreamChunk" },
311
+ },
312
+ },
313
+ },
314
+ "400": {
315
+ description: "Bad request",
316
+ content: {
317
+ "application/json": {
318
+ schema: { $ref: "#/components/schemas/Error" },
319
+ },
320
+ },
321
+ },
322
+ },
323
+ },
324
+ };
325
+ }
326
+ }
327
+
328
+ if (routerOpts.teams && Object.keys(routerOpts.teams).length > 0) {
329
+ spec.tags.push({ name: "Teams", description: "Team endpoints for multi-agent coordination" });
330
+
331
+ for (const name of Object.keys(routerOpts.teams)) {
332
+ spec.paths[`${prefix}/teams/${name}/run`] = {
333
+ post: {
334
+ tags: ["Teams"],
335
+ summary: `Run team: ${name}`,
336
+ operationId: `runTeam_${name}`,
337
+ requestBody: {
338
+ required: true,
339
+ content: {
340
+ "application/json": {
341
+ schema: { $ref: "#/components/schemas/RunRequest" },
342
+ },
343
+ },
344
+ },
345
+ responses: {
346
+ "200": {
347
+ description: "Team run result",
348
+ content: {
349
+ "application/json": {
350
+ schema: { $ref: "#/components/schemas/RunOutput" },
351
+ },
352
+ },
353
+ },
354
+ "400": { description: "Bad request", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } },
355
+ "500": { description: "Internal server error", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } },
356
+ },
357
+ },
358
+ };
359
+
360
+ spec.paths[`${prefix}/teams/${name}/stream`] = {
361
+ post: {
362
+ tags: ["Teams"],
363
+ summary: `Stream team: ${name}`,
364
+ operationId: `streamTeam_${name}`,
365
+ requestBody: {
366
+ required: true,
367
+ content: { "application/json": { schema: { $ref: "#/components/schemas/RunRequest" } } },
368
+ },
369
+ responses: {
370
+ "200": { description: "SSE stream", content: { "text/event-stream": { schema: { $ref: "#/components/schemas/StreamChunk" } } } },
371
+ "400": { description: "Bad request", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } },
372
+ },
373
+ },
374
+ };
375
+ }
376
+ }
377
+
378
+ if (routerOpts.workflows && Object.keys(routerOpts.workflows).length > 0) {
379
+ spec.tags.push({ name: "Workflows", description: "Workflow endpoints for step-based pipelines" });
380
+
381
+ for (const name of Object.keys(routerOpts.workflows)) {
382
+ spec.paths[`${prefix}/workflows/${name}/run`] = {
383
+ post: {
384
+ tags: ["Workflows"],
385
+ summary: `Run workflow: ${name}`,
386
+ operationId: `runWorkflow_${name}`,
387
+ requestBody: {
388
+ required: true,
389
+ content: {
390
+ "application/json": {
391
+ schema: { $ref: "#/components/schemas/WorkflowRunRequest" },
392
+ },
393
+ },
394
+ },
395
+ responses: {
396
+ "200": { description: "Workflow result", content: { "application/json": { schema: { type: "object" } } } },
397
+ "500": { description: "Internal server error", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } },
398
+ },
399
+ },
400
+ };
401
+ }
402
+ }
403
+
404
+ return spec;
405
+ }
406
+
407
+ export function serveSwaggerUI(spec: OpenAPISpec) {
408
+ let swaggerUiExpress: any;
409
+ try {
410
+ swaggerUiExpress = _require("swagger-ui-express");
411
+ } catch {
412
+ throw new Error(
413
+ "swagger-ui-express is required for Swagger UI. Install it: npm install swagger-ui-express"
414
+ );
415
+ }
416
+
417
+ return {
418
+ setup: swaggerUiExpress.setup(spec, {
419
+ customCss: ".swagger-ui .topbar { display: none }",
420
+ customSiteTitle: spec.info.title,
421
+ }),
422
+ serve: swaggerUiExpress.serve,
423
+ };
424
+ }
@@ -0,0 +1,32 @@
1
+ import type { Agent, Team, Workflow } from "@radaros/core";
2
+ import type { FileUploadOptions } from "./file-upload.js";
3
+
4
+ export interface SwaggerOptions {
5
+ /** Enable Swagger UI at /docs. Default: false */
6
+ enabled?: boolean;
7
+ /** API title shown in Swagger UI */
8
+ title?: string;
9
+ /** API description shown in Swagger UI */
10
+ description?: string;
11
+ /** API version string */
12
+ version?: string;
13
+ /** Route prefix used in path generation (e.g. "/api") */
14
+ routePrefix?: string;
15
+ /** Server URLs for the spec */
16
+ servers?: Array<{ url: string; description?: string }>;
17
+ /** Path to serve Swagger UI. Default: "/docs" */
18
+ docsPath?: string;
19
+ /** Path to serve the raw OpenAPI JSON spec. Default: "/docs/spec.json" */
20
+ specPath?: string;
21
+ }
22
+
23
+ export interface RouterOptions {
24
+ agents?: Record<string, Agent>;
25
+ teams?: Record<string, Team>;
26
+ workflows?: Record<string, Workflow<any>>;
27
+ middleware?: any[];
28
+ /** Swagger / OpenAPI configuration */
29
+ swagger?: SwaggerOptions;
30
+ /** File upload configuration for multi-modal inputs */
31
+ fileUpload?: boolean | FileUploadOptions;
32
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { createAgentRouter } from "./express/router-factory.js";
2
+ export { errorHandler, requestLogger } from "./express/middleware.js";
3
+ export { generateOpenAPISpec } from "./express/swagger.js";
4
+ export { createFileUploadMiddleware, buildMultiModalInput } from "./express/file-upload.js";
5
+ export type { RouterOptions, SwaggerOptions } from "./express/types.js";
6
+ export type { FileUploadOptions } from "./express/file-upload.js";
7
+
8
+ export { createAgentGateway } from "./socketio/gateway.js";
9
+ export type { GatewayOptions } from "./socketio/types.js";
@@ -0,0 +1,75 @@
1
+ import type { GatewayOptions } from "./types.js";
2
+
3
+ export function createAgentGateway(opts: GatewayOptions): void {
4
+ const ns = opts.io.of(opts.namespace ?? "/radaros");
5
+
6
+ if (opts.authMiddleware) {
7
+ ns.use(opts.authMiddleware);
8
+ }
9
+
10
+ ns.on("connection", (socket: any) => {
11
+ socket.on(
12
+ "agent.run",
13
+ async (data: { name: string; input: string; sessionId?: string; apiKey?: string }) => {
14
+ const agent = opts.agents?.[data.name];
15
+ if (!agent) {
16
+ socket.emit("agent.error", {
17
+ error: `Agent "${data.name}" not found`,
18
+ });
19
+ return;
20
+ }
21
+
22
+ try {
23
+ const apiKey = data.apiKey ?? socket.handshake?.auth?.apiKey;
24
+ const sessionId = data.sessionId ?? socket.id;
25
+
26
+ let fullText = "";
27
+ for await (const chunk of agent.stream(data.input, {
28
+ sessionId,
29
+ apiKey,
30
+ })) {
31
+ if (chunk.type === "text") {
32
+ fullText += chunk.text;
33
+ socket.emit("agent.chunk", { chunk: chunk.text });
34
+ } else if (chunk.type === "tool_call_start") {
35
+ socket.emit("agent.tool.call", {
36
+ toolName: chunk.toolCall.name,
37
+ args: null,
38
+ });
39
+ } else if (chunk.type === "tool_call_end") {
40
+ socket.emit("agent.tool.done", { toolCallId: chunk.toolCallId });
41
+ }
42
+ }
43
+
44
+ socket.emit("agent.done", { output: { text: fullText } });
45
+ } catch (error: any) {
46
+ socket.emit("agent.error", { error: error.message });
47
+ }
48
+ }
49
+ );
50
+
51
+ socket.on(
52
+ "team.run",
53
+ async (data: { name: string; input: string; sessionId?: string; apiKey?: string }) => {
54
+ const team = opts.teams?.[data.name];
55
+ if (!team) {
56
+ socket.emit("agent.error", {
57
+ error: `Team "${data.name}" not found`,
58
+ });
59
+ return;
60
+ }
61
+
62
+ try {
63
+ const apiKey = data.apiKey ?? socket.handshake?.auth?.apiKey;
64
+ const result = await team.run(data.input, {
65
+ sessionId: data.sessionId ?? socket.id,
66
+ apiKey,
67
+ });
68
+ socket.emit("agent.done", { output: result });
69
+ } catch (error: any) {
70
+ socket.emit("agent.error", { error: error.message });
71
+ }
72
+ }
73
+ );
74
+ });
75
+ }
@@ -0,0 +1 @@
1
+ export { createAgentGateway } from "./gateway.js";
@@ -0,0 +1,9 @@
1
+ import type { Agent, Team } from "@radaros/core";
2
+
3
+ export interface GatewayOptions {
4
+ agents?: Record<string, Agent>;
5
+ teams?: Record<string, Team>;
6
+ io: any;
7
+ namespace?: string;
8
+ authMiddleware?: (socket: any, next: (err?: Error) => void) => void;
9
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "paths": {
7
+ "@radaros/core": ["../core/dist/index.d.ts"]
8
+ }
9
+ },
10
+ "include": ["src"]
11
+ }