@radaros/transport 0.3.5 → 0.3.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radaros/transport",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -10,9 +10,13 @@
10
10
  "types": "./dist/index.d.ts"
11
11
  }
12
12
  },
13
+ "files": [
14
+ "dist"
15
+ ],
13
16
  "scripts": {
14
17
  "build": "tsup src/index.ts --format esm --dts --clean",
15
- "dev": "tsup src/index.ts --format esm --dts --watch"
18
+ "dev": "tsup src/index.ts --format esm --dts --watch",
19
+ "prepublishOnly": "npm run build"
16
20
  },
17
21
  "devDependencies": {
18
22
  "@types/node": "^25.3.1",
@@ -20,7 +24,7 @@
20
24
  "typescript": "^5.6.0"
21
25
  },
22
26
  "peerDependencies": {
23
- "@radaros/core": "^0.3.5",
27
+ "@radaros/core": "^0.3.6",
24
28
  "@types/express": "^4.0.0 || ^5.0.0",
25
29
  "express": "^4.0.0 || ^5.0.0",
26
30
  "multer": ">=1.4.0",
@@ -1,391 +0,0 @@
1
- import { createRequire } from "node:module";
2
- import { randomUUID } from "node:crypto";
3
- import type { Agent } from "@radaros/core";
4
- import type {
5
- A2ATask,
6
- A2AMessage,
7
- A2APart,
8
- A2AJsonRpcRequest,
9
- A2AJsonRpcResponse,
10
- A2ATaskState,
11
- } from "@radaros/core";
12
- import { generateMultiAgentCard } from "./agent-card.js";
13
- import type { A2AServerOptions } from "./types.js";
14
-
15
- const _require = createRequire(import.meta.url);
16
-
17
- /**
18
- * In-memory task store. Can be replaced with a persistent implementation.
19
- */
20
- class TaskStore {
21
- private tasks = new Map<string, A2ATask>();
22
-
23
- get(id: string): A2ATask | undefined {
24
- return this.tasks.get(id);
25
- }
26
-
27
- set(task: A2ATask): void {
28
- this.tasks.set(task.id, task);
29
- }
30
-
31
- updateState(id: string, state: A2ATaskState, message?: A2AMessage): void {
32
- const task = this.tasks.get(id);
33
- if (!task) return;
34
- task.status = {
35
- state,
36
- message,
37
- timestamp: new Date().toISOString(),
38
- };
39
- }
40
- }
41
-
42
- function a2aPartsToText(parts: A2APart[]): string {
43
- return parts
44
- .filter((p): p is { kind: "text"; text: string } => p.kind === "text")
45
- .map((p) => p.text)
46
- .join("\n");
47
- }
48
-
49
- function textToA2AParts(text: string): A2APart[] {
50
- return [{ kind: "text", text }];
51
- }
52
-
53
- function jsonRpcError(
54
- id: string | number,
55
- code: number,
56
- message: string
57
- ): A2AJsonRpcResponse {
58
- return { jsonrpc: "2.0", id, error: { code, message } };
59
- }
60
-
61
- function resolveAgent(
62
- agents: Record<string, Agent>,
63
- message: A2AMessage
64
- ): Agent | null {
65
- const meta = message.metadata as Record<string, unknown> | undefined;
66
- const agentName = meta?.agentName as string | undefined;
67
-
68
- if (agentName && agents[agentName]) {
69
- return agents[agentName];
70
- }
71
-
72
- const names = Object.keys(agents);
73
- if (names.length === 1) {
74
- return agents[names[0]];
75
- }
76
-
77
- const textContent = a2aPartsToText(message.parts).toLowerCase();
78
- for (const [name, agent] of Object.entries(agents)) {
79
- if (textContent.includes(name.toLowerCase())) {
80
- return agent;
81
- }
82
- }
83
-
84
- return agents[names[0]] ?? null;
85
- }
86
-
87
- /**
88
- * Mount an A2A-compliant server on an Express app.
89
- *
90
- * - Serves `/.well-known/agent.json` with the Agent Card
91
- * - Handles JSON-RPC 2.0 requests at the basePath for message/send, message/stream, tasks/get, tasks/cancel
92
- */
93
- export function createA2AServer(app: any, opts: A2AServerOptions): void {
94
- const express = _require("express");
95
- const basePath = opts.basePath ?? "/";
96
- const taskStore = new TaskStore();
97
-
98
- const serverUrl =
99
- basePath === "/" ? "" : basePath;
100
-
101
- const agentCard = generateMultiAgentCard(
102
- opts.agents,
103
- serverUrl || "/",
104
- opts.provider,
105
- opts.version
106
- );
107
-
108
- app.get("/.well-known/agent.json", (_req: any, res: any) => {
109
- res.json(agentCard);
110
- });
111
-
112
- app.use(basePath, express.json());
113
-
114
- app.post(basePath, async (req: any, res: any) => {
115
- const body: A2AJsonRpcRequest = req.body;
116
-
117
- if (!body || body.jsonrpc !== "2.0" || !body.method) {
118
- return res.status(400).json(
119
- jsonRpcError(body?.id ?? 0, -32600, "Invalid JSON-RPC request")
120
- );
121
- }
122
-
123
- try {
124
- switch (body.method) {
125
- case "message/send":
126
- return await handleMessageSend(req, res, body, opts.agents, taskStore);
127
- case "message/stream":
128
- return await handleMessageStream(req, res, body, opts.agents, taskStore);
129
- case "tasks/get":
130
- return handleTasksGet(res, body, taskStore);
131
- case "tasks/cancel":
132
- return handleTasksCancel(res, body, taskStore);
133
- default:
134
- return res.json(
135
- jsonRpcError(body.id, -32601, `Method '${body.method}' not found`)
136
- );
137
- }
138
- } catch (err: any) {
139
- return res.json(
140
- jsonRpcError(body.id, -32000, err.message ?? "Internal error")
141
- );
142
- }
143
- });
144
- }
145
-
146
- async function handleMessageSend(
147
- _req: any,
148
- res: any,
149
- body: A2AJsonRpcRequest,
150
- agents: Record<string, Agent>,
151
- store: TaskStore
152
- ): Promise<void> {
153
- const params = body.params as any;
154
- const message: A2AMessage = params?.message;
155
-
156
- if (!message?.parts?.length) {
157
- return res.json(jsonRpcError(body.id, -32602, "Missing message.parts"));
158
- }
159
-
160
- const agent = resolveAgent(agents, message);
161
- if (!agent) {
162
- return res.json(jsonRpcError(body.id, -32602, "No matching agent found"));
163
- }
164
-
165
- const taskId = randomUUID();
166
- const input = a2aPartsToText(message.parts);
167
-
168
- const task: A2ATask = {
169
- id: taskId,
170
- sessionId: (params?.sessionId as string) ?? undefined,
171
- status: { state: "submitted", timestamp: new Date().toISOString() },
172
- history: [message],
173
- metadata: { agentName: agent.name },
174
- };
175
- store.set(task);
176
- store.updateState(taskId, "working");
177
-
178
- try {
179
- const result = await agent.run(input, {
180
- sessionId: task.sessionId,
181
- });
182
-
183
- const responseParts: A2APart[] = textToA2AParts(result.text);
184
-
185
- if (result.structured) {
186
- responseParts.push({
187
- kind: "data",
188
- data: result.structured as Record<string, unknown>,
189
- });
190
- }
191
-
192
- const agentMessage: A2AMessage = {
193
- role: "agent",
194
- parts: responseParts,
195
- messageId: randomUUID(),
196
- taskId,
197
- };
198
-
199
- task.history!.push(agentMessage);
200
-
201
- if (result.toolCalls?.length) {
202
- task.artifacts = result.toolCalls.map((tc) => ({
203
- artifactId: tc.toolCallId,
204
- name: tc.toolName,
205
- parts: [
206
- {
207
- kind: "text" as const,
208
- text: typeof tc.result === "string" ? tc.result : tc.result.content,
209
- },
210
- ],
211
- }));
212
- }
213
-
214
- store.updateState(taskId, "completed", agentMessage);
215
-
216
- const response: A2AJsonRpcResponse = {
217
- jsonrpc: "2.0",
218
- id: body.id,
219
- result: store.get(taskId),
220
- };
221
-
222
- res.json(response);
223
- } catch (err: any) {
224
- const errorMessage: A2AMessage = {
225
- role: "agent",
226
- parts: [{ kind: "text", text: `Error: ${err.message}` }],
227
- taskId,
228
- };
229
- store.updateState(taskId, "failed", errorMessage);
230
-
231
- res.json(jsonRpcError(body.id, -32000, err.message));
232
- }
233
- }
234
-
235
- async function handleMessageStream(
236
- _req: any,
237
- res: any,
238
- body: A2AJsonRpcRequest,
239
- agents: Record<string, Agent>,
240
- store: TaskStore
241
- ): Promise<void> {
242
- const params = body.params as any;
243
- const message: A2AMessage = params?.message;
244
-
245
- if (!message?.parts?.length) {
246
- return res.json(jsonRpcError(body.id, -32602, "Missing message.parts"));
247
- }
248
-
249
- const agent = resolveAgent(agents, message);
250
- if (!agent) {
251
- return res.json(jsonRpcError(body.id, -32602, "No matching agent found"));
252
- }
253
-
254
- const taskId = randomUUID();
255
- const input = a2aPartsToText(message.parts);
256
-
257
- const task: A2ATask = {
258
- id: taskId,
259
- sessionId: (params?.sessionId as string) ?? undefined,
260
- status: { state: "submitted", timestamp: new Date().toISOString() },
261
- history: [message],
262
- metadata: { agentName: agent.name },
263
- };
264
- store.set(task);
265
-
266
- res.writeHead(200, {
267
- "Content-Type": "text/event-stream",
268
- "Cache-Control": "no-cache",
269
- Connection: "keep-alive",
270
- });
271
-
272
- const sendEvent = (data: any) => {
273
- res.write(`data: ${JSON.stringify(data)}\n\n`);
274
- };
275
-
276
- store.updateState(taskId, "working");
277
- sendEvent({
278
- jsonrpc: "2.0",
279
- id: body.id,
280
- result: store.get(taskId),
281
- });
282
-
283
- try {
284
- let fullText = "";
285
- for await (const chunk of agent.stream(input, {
286
- sessionId: task.sessionId,
287
- })) {
288
- if (chunk.type === "text") {
289
- fullText += chunk.text;
290
- sendEvent({
291
- jsonrpc: "2.0",
292
- id: body.id,
293
- result: {
294
- id: taskId,
295
- status: {
296
- state: "working",
297
- message: {
298
- role: "agent",
299
- parts: [{ kind: "text", text: chunk.text }],
300
- },
301
- timestamp: new Date().toISOString(),
302
- },
303
- },
304
- });
305
- }
306
- }
307
-
308
- const agentMessage: A2AMessage = {
309
- role: "agent",
310
- parts: textToA2AParts(fullText),
311
- messageId: randomUUID(),
312
- taskId,
313
- };
314
- task.history!.push(agentMessage);
315
- store.updateState(taskId, "completed", agentMessage);
316
-
317
- sendEvent({
318
- jsonrpc: "2.0",
319
- id: body.id,
320
- result: store.get(taskId),
321
- });
322
-
323
- res.end();
324
- } catch (err: any) {
325
- const errorMessage: A2AMessage = {
326
- role: "agent",
327
- parts: [{ kind: "text", text: `Error: ${err.message}` }],
328
- taskId,
329
- };
330
- store.updateState(taskId, "failed", errorMessage);
331
-
332
- sendEvent({
333
- jsonrpc: "2.0",
334
- id: body.id,
335
- result: store.get(taskId),
336
- });
337
- res.end();
338
- }
339
- }
340
-
341
- function handleTasksGet(
342
- res: any,
343
- body: A2AJsonRpcRequest,
344
- store: TaskStore
345
- ): void {
346
- const params = body.params as any;
347
- const taskId = params?.id;
348
-
349
- if (!taskId) {
350
- return res.json(jsonRpcError(body.id, -32602, "Missing task id"));
351
- }
352
-
353
- const task = store.get(taskId);
354
- if (!task) {
355
- return res.json(jsonRpcError(body.id, -32602, `Task '${taskId}' not found`));
356
- }
357
-
358
- const historyLength = params?.historyLength as number | undefined;
359
- const result = { ...task };
360
- if (historyLength && result.history) {
361
- result.history = result.history.slice(-historyLength);
362
- }
363
-
364
- res.json({ jsonrpc: "2.0", id: body.id, result } as A2AJsonRpcResponse);
365
- }
366
-
367
- function handleTasksCancel(
368
- res: any,
369
- body: A2AJsonRpcRequest,
370
- store: TaskStore
371
- ): void {
372
- const params = body.params as any;
373
- const taskId = params?.id;
374
-
375
- if (!taskId) {
376
- return res.json(jsonRpcError(body.id, -32602, "Missing task id"));
377
- }
378
-
379
- const task = store.get(taskId);
380
- if (!task) {
381
- return res.json(jsonRpcError(body.id, -32602, `Task '${taskId}' not found`));
382
- }
383
-
384
- store.updateState(taskId, "canceled");
385
-
386
- res.json({
387
- jsonrpc: "2.0",
388
- id: body.id,
389
- result: store.get(taskId),
390
- } as A2AJsonRpcResponse);
391
- }
@@ -1,95 +0,0 @@
1
- import type { Agent } from "@radaros/core";
2
- import type {
3
- A2AAgentCard,
4
- A2ASkill,
5
- } from "@radaros/core";
6
-
7
- /**
8
- * Generate an A2A Agent Card from a RadarOS Agent.
9
- * The card is served at /.well-known/agent.json per the A2A spec.
10
- */
11
- export function generateAgentCard(
12
- agent: Agent,
13
- serverUrl: string,
14
- provider?: { organization: string; url?: string },
15
- version?: string
16
- ): A2AAgentCard {
17
- const skills: A2ASkill[] = agent.tools.map((tool) => ({
18
- id: tool.name,
19
- name: tool.name,
20
- description: tool.description,
21
- }));
22
-
23
- if (skills.length === 0) {
24
- skills.push({
25
- id: "general",
26
- name: "General",
27
- description:
28
- typeof agent.instructions === "string"
29
- ? agent.instructions.slice(0, 200)
30
- : "General-purpose agent",
31
- });
32
- }
33
-
34
- const description =
35
- typeof agent.instructions === "string"
36
- ? agent.instructions
37
- : `RadarOS agent: ${agent.name}`;
38
-
39
- return {
40
- name: agent.name,
41
- description,
42
- url: serverUrl,
43
- version: version ?? "1.0.0",
44
- provider,
45
- capabilities: {
46
- streaming: true,
47
- pushNotifications: false,
48
- stateTransitionHistory: true,
49
- },
50
- skills,
51
- defaultInputModes: ["text/plain"],
52
- defaultOutputModes: ["text/plain"],
53
- supportedInputModes: ["text/plain", "application/json"],
54
- supportedOutputModes: ["text/plain", "application/json"],
55
- };
56
- }
57
-
58
- /**
59
- * Generate a combined Agent Card that lists multiple agents as skills.
60
- */
61
- export function generateMultiAgentCard(
62
- agents: Record<string, Agent>,
63
- serverUrl: string,
64
- provider?: { organization: string; url?: string },
65
- version?: string
66
- ): A2AAgentCard {
67
- const skills: A2ASkill[] = Object.entries(agents).map(
68
- ([name, agent]) => ({
69
- id: name,
70
- name: agent.name,
71
- description:
72
- typeof agent.instructions === "string"
73
- ? agent.instructions.slice(0, 200)
74
- : `Agent: ${name}`,
75
- })
76
- );
77
-
78
- return {
79
- name: "RadarOS Agent Server",
80
- description: `Multi-agent server with ${Object.keys(agents).length} agents: ${Object.keys(agents).join(", ")}`,
81
- url: serverUrl,
82
- version: version ?? "1.0.0",
83
- provider,
84
- capabilities: {
85
- streaming: true,
86
- pushNotifications: false,
87
- stateTransitionHistory: true,
88
- },
89
- skills,
90
- defaultInputModes: ["text/plain"],
91
- defaultOutputModes: ["text/plain"],
92
- supportedInputModes: ["text/plain", "application/json"],
93
- supportedOutputModes: ["text/plain", "application/json"],
94
- };
95
- }
package/src/a2a/types.ts DELETED
@@ -1,11 +0,0 @@
1
- import type { Agent } from "@radaros/core";
2
-
3
- export interface A2AServerOptions {
4
- agents: Record<string, Agent>;
5
- basePath?: string;
6
- provider?: {
7
- organization: string;
8
- url?: string;
9
- };
10
- version?: string;
11
- }
@@ -1,88 +0,0 @@
1
- import { createRequire } from "node:module";
2
-
3
- const _require = createRequire(import.meta.url);
4
-
5
- const MIME_TO_PART_TYPE: Record<string, "image" | "audio" | "file"> = {
6
- "image/png": "image",
7
- "image/jpeg": "image",
8
- "image/jpg": "image",
9
- "image/gif": "image",
10
- "image/webp": "image",
11
- "audio/mpeg": "audio",
12
- "audio/mp3": "audio",
13
- "audio/wav": "audio",
14
- "audio/ogg": "audio",
15
- "audio/webm": "audio",
16
- "audio/flac": "audio",
17
- "audio/aac": "audio",
18
- "audio/mp4": "audio",
19
- };
20
-
21
- function getPartType(mimeType: string): "image" | "audio" | "file" {
22
- return MIME_TO_PART_TYPE[mimeType] ?? "file";
23
- }
24
-
25
- export interface FileUploadOptions {
26
- maxFileSize?: number;
27
- maxFiles?: number;
28
- allowedMimeTypes?: string[];
29
- }
30
-
31
- export function createFileUploadMiddleware(opts: FileUploadOptions = {}) {
32
- let multer: any;
33
- try {
34
- multer = _require("multer");
35
- } catch {
36
- throw new Error(
37
- "multer is required for file uploads. Install it: npm install multer"
38
- );
39
- }
40
-
41
- const storage = multer.memoryStorage();
42
- const upload = multer({
43
- storage,
44
- limits: {
45
- fileSize: opts.maxFileSize ?? 50 * 1024 * 1024,
46
- files: opts.maxFiles ?? 10,
47
- },
48
- fileFilter: opts.allowedMimeTypes
49
- ? (_req: any, file: any, cb: any) => {
50
- if (opts.allowedMimeTypes!.includes(file.mimetype)) {
51
- cb(null, true);
52
- } else {
53
- cb(new Error(`File type ${file.mimetype} is not allowed`));
54
- }
55
- }
56
- : undefined,
57
- });
58
-
59
- return upload.array("files", opts.maxFiles ?? 10);
60
- }
61
-
62
- export function filesToContentParts(files: any[]): any[] {
63
- return files.map((file) => {
64
- const base64 = file.buffer.toString("base64");
65
- const partType = getPartType(file.mimetype);
66
-
67
- return {
68
- type: partType,
69
- data: base64,
70
- mimeType: file.mimetype,
71
- ...(partType === "file" ? { fileName: file.originalname } : {}),
72
- };
73
- });
74
- }
75
-
76
- export function buildMultiModalInput(body: any, files?: any[]): string | any[] {
77
- const textInput = body?.input;
78
- if (!files || files.length === 0) {
79
- return textInput;
80
- }
81
-
82
- const parts: any[] = [];
83
- if (textInput) {
84
- parts.push({ type: "text", text: textInput });
85
- }
86
- parts.push(...filesToContentParts(files));
87
- return parts;
88
- }
@@ -1,15 +0,0 @@
1
- export function errorHandler() {
2
- return (err: any, _req: any, res: any, _next: any) => {
3
- console.error("[radaros:transport] Error:", err.message);
4
- res.status(err.statusCode ?? 500).json({
5
- error: err.message ?? "Internal server error",
6
- });
7
- };
8
- }
9
-
10
- export function requestLogger() {
11
- return (req: any, _res: any, next: any) => {
12
- console.log(`[radaros:transport] ${req.method} ${req.path}`);
13
- next();
14
- };
15
- }