@radaros/transport 0.1.0 → 0.2.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.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Agent, Team, Workflow } from '@radaros/core';
1
+ import { Agent, Team, Workflow, A2AAgentCard } from '@radaros/core';
2
2
 
3
3
  interface FileUploadOptions {
4
4
  maxFileSize?: number;
@@ -79,4 +79,38 @@ interface GatewayOptions {
79
79
 
80
80
  declare function createAgentGateway(opts: GatewayOptions): void;
81
81
 
82
- export { type FileUploadOptions, type GatewayOptions, type RouterOptions, type SwaggerOptions, buildMultiModalInput, createAgentGateway, createAgentRouter, createFileUploadMiddleware, errorHandler, generateOpenAPISpec, requestLogger };
82
+ interface A2AServerOptions {
83
+ agents: Record<string, Agent>;
84
+ basePath?: string;
85
+ provider?: {
86
+ organization: string;
87
+ url?: string;
88
+ };
89
+ version?: string;
90
+ }
91
+
92
+ /**
93
+ * Mount an A2A-compliant server on an Express app.
94
+ *
95
+ * - Serves `/.well-known/agent.json` with the Agent Card
96
+ * - Handles JSON-RPC 2.0 requests at the basePath for message/send, message/stream, tasks/get, tasks/cancel
97
+ */
98
+ declare function createA2AServer(app: any, opts: A2AServerOptions): void;
99
+
100
+ /**
101
+ * Generate an A2A Agent Card from a RadarOS Agent.
102
+ * The card is served at /.well-known/agent.json per the A2A spec.
103
+ */
104
+ declare function generateAgentCard(agent: Agent, serverUrl: string, provider?: {
105
+ organization: string;
106
+ url?: string;
107
+ }, version?: string): A2AAgentCard;
108
+ /**
109
+ * Generate a combined Agent Card that lists multiple agents as skills.
110
+ */
111
+ declare function generateMultiAgentCard(agents: Record<string, Agent>, serverUrl: string, provider?: {
112
+ organization: string;
113
+ url?: string;
114
+ }, version?: string): A2AAgentCard;
115
+
116
+ export { type A2AServerOptions, type FileUploadOptions, type GatewayOptions, type RouterOptions, type SwaggerOptions, buildMultiModalInput, createA2AServer, createAgentGateway, createAgentRouter, createFileUploadMiddleware, errorHandler, generateAgentCard, generateMultiAgentCard, generateOpenAPISpec, requestLogger };
package/dist/index.js CHANGED
@@ -717,12 +717,362 @@ function createAgentGateway(opts) {
717
717
  );
718
718
  });
719
719
  }
720
+
721
+ // src/a2a/a2a-server.ts
722
+ import { createRequire as createRequire4 } from "module";
723
+ import { randomUUID } from "crypto";
724
+
725
+ // src/a2a/agent-card.ts
726
+ function generateAgentCard(agent, serverUrl, provider, version) {
727
+ const skills = agent.tools.map((tool) => ({
728
+ id: tool.name,
729
+ name: tool.name,
730
+ description: tool.description
731
+ }));
732
+ if (skills.length === 0) {
733
+ skills.push({
734
+ id: "general",
735
+ name: "General",
736
+ description: typeof agent.instructions === "string" ? agent.instructions.slice(0, 200) : "General-purpose agent"
737
+ });
738
+ }
739
+ const description = typeof agent.instructions === "string" ? agent.instructions : `RadarOS agent: ${agent.name}`;
740
+ return {
741
+ name: agent.name,
742
+ description,
743
+ url: serverUrl,
744
+ version: version ?? "1.0.0",
745
+ provider,
746
+ capabilities: {
747
+ streaming: true,
748
+ pushNotifications: false,
749
+ stateTransitionHistory: true
750
+ },
751
+ skills,
752
+ defaultInputModes: ["text/plain"],
753
+ defaultOutputModes: ["text/plain"],
754
+ supportedInputModes: ["text/plain", "application/json"],
755
+ supportedOutputModes: ["text/plain", "application/json"]
756
+ };
757
+ }
758
+ function generateMultiAgentCard(agents, serverUrl, provider, version) {
759
+ const skills = Object.entries(agents).map(
760
+ ([name, agent]) => ({
761
+ id: name,
762
+ name: agent.name,
763
+ description: typeof agent.instructions === "string" ? agent.instructions.slice(0, 200) : `Agent: ${name}`
764
+ })
765
+ );
766
+ return {
767
+ name: "RadarOS Agent Server",
768
+ description: `Multi-agent server with ${Object.keys(agents).length} agents: ${Object.keys(agents).join(", ")}`,
769
+ url: serverUrl,
770
+ version: version ?? "1.0.0",
771
+ provider,
772
+ capabilities: {
773
+ streaming: true,
774
+ pushNotifications: false,
775
+ stateTransitionHistory: true
776
+ },
777
+ skills,
778
+ defaultInputModes: ["text/plain"],
779
+ defaultOutputModes: ["text/plain"],
780
+ supportedInputModes: ["text/plain", "application/json"],
781
+ supportedOutputModes: ["text/plain", "application/json"]
782
+ };
783
+ }
784
+
785
+ // src/a2a/a2a-server.ts
786
+ var _require4 = createRequire4(import.meta.url);
787
+ var TaskStore = class {
788
+ tasks = /* @__PURE__ */ new Map();
789
+ get(id) {
790
+ return this.tasks.get(id);
791
+ }
792
+ set(task) {
793
+ this.tasks.set(task.id, task);
794
+ }
795
+ updateState(id, state, message) {
796
+ const task = this.tasks.get(id);
797
+ if (!task) return;
798
+ task.status = {
799
+ state,
800
+ message,
801
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
802
+ };
803
+ }
804
+ };
805
+ function a2aPartsToText(parts) {
806
+ return parts.filter((p) => p.kind === "text").map((p) => p.text).join("\n");
807
+ }
808
+ function textToA2AParts(text) {
809
+ return [{ kind: "text", text }];
810
+ }
811
+ function jsonRpcError(id, code, message) {
812
+ return { jsonrpc: "2.0", id, error: { code, message } };
813
+ }
814
+ function resolveAgent(agents, message) {
815
+ const meta = message.metadata;
816
+ const agentName = meta?.agentName;
817
+ if (agentName && agents[agentName]) {
818
+ return agents[agentName];
819
+ }
820
+ const names = Object.keys(agents);
821
+ if (names.length === 1) {
822
+ return agents[names[0]];
823
+ }
824
+ const textContent = a2aPartsToText(message.parts).toLowerCase();
825
+ for (const [name, agent] of Object.entries(agents)) {
826
+ if (textContent.includes(name.toLowerCase())) {
827
+ return agent;
828
+ }
829
+ }
830
+ return agents[names[0]] ?? null;
831
+ }
832
+ function createA2AServer(app, opts) {
833
+ const express = _require4("express");
834
+ const basePath = opts.basePath ?? "/";
835
+ const taskStore = new TaskStore();
836
+ const serverUrl = basePath === "/" ? "" : basePath;
837
+ const agentCard = generateMultiAgentCard(
838
+ opts.agents,
839
+ serverUrl || "/",
840
+ opts.provider,
841
+ opts.version
842
+ );
843
+ app.get("/.well-known/agent.json", (_req, res) => {
844
+ res.json(agentCard);
845
+ });
846
+ app.use(basePath, express.json());
847
+ app.post(basePath, async (req, res) => {
848
+ const body = req.body;
849
+ if (!body || body.jsonrpc !== "2.0" || !body.method) {
850
+ return res.status(400).json(
851
+ jsonRpcError(body?.id ?? 0, -32600, "Invalid JSON-RPC request")
852
+ );
853
+ }
854
+ try {
855
+ switch (body.method) {
856
+ case "message/send":
857
+ return await handleMessageSend(req, res, body, opts.agents, taskStore);
858
+ case "message/stream":
859
+ return await handleMessageStream(req, res, body, opts.agents, taskStore);
860
+ case "tasks/get":
861
+ return handleTasksGet(res, body, taskStore);
862
+ case "tasks/cancel":
863
+ return handleTasksCancel(res, body, taskStore);
864
+ default:
865
+ return res.json(
866
+ jsonRpcError(body.id, -32601, `Method '${body.method}' not found`)
867
+ );
868
+ }
869
+ } catch (err) {
870
+ return res.json(
871
+ jsonRpcError(body.id, -32e3, err.message ?? "Internal error")
872
+ );
873
+ }
874
+ });
875
+ }
876
+ async function handleMessageSend(_req, res, body, agents, store) {
877
+ const params = body.params;
878
+ const message = params?.message;
879
+ if (!message?.parts?.length) {
880
+ return res.json(jsonRpcError(body.id, -32602, "Missing message.parts"));
881
+ }
882
+ const agent = resolveAgent(agents, message);
883
+ if (!agent) {
884
+ return res.json(jsonRpcError(body.id, -32602, "No matching agent found"));
885
+ }
886
+ const taskId = randomUUID();
887
+ const input = a2aPartsToText(message.parts);
888
+ const task = {
889
+ id: taskId,
890
+ sessionId: params?.sessionId ?? void 0,
891
+ status: { state: "submitted", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
892
+ history: [message],
893
+ metadata: { agentName: agent.name }
894
+ };
895
+ store.set(task);
896
+ store.updateState(taskId, "working");
897
+ try {
898
+ const result = await agent.run(input, {
899
+ sessionId: task.sessionId
900
+ });
901
+ const responseParts = textToA2AParts(result.text);
902
+ if (result.structured) {
903
+ responseParts.push({
904
+ kind: "data",
905
+ data: result.structured
906
+ });
907
+ }
908
+ const agentMessage = {
909
+ role: "agent",
910
+ parts: responseParts,
911
+ messageId: randomUUID(),
912
+ taskId
913
+ };
914
+ task.history.push(agentMessage);
915
+ if (result.toolCalls?.length) {
916
+ task.artifacts = result.toolCalls.map((tc) => ({
917
+ artifactId: tc.toolCallId,
918
+ name: tc.toolName,
919
+ parts: [
920
+ {
921
+ kind: "text",
922
+ text: typeof tc.result === "string" ? tc.result : tc.result.content
923
+ }
924
+ ]
925
+ }));
926
+ }
927
+ store.updateState(taskId, "completed", agentMessage);
928
+ const response = {
929
+ jsonrpc: "2.0",
930
+ id: body.id,
931
+ result: store.get(taskId)
932
+ };
933
+ res.json(response);
934
+ } catch (err) {
935
+ const errorMessage = {
936
+ role: "agent",
937
+ parts: [{ kind: "text", text: `Error: ${err.message}` }],
938
+ taskId
939
+ };
940
+ store.updateState(taskId, "failed", errorMessage);
941
+ res.json(jsonRpcError(body.id, -32e3, err.message));
942
+ }
943
+ }
944
+ async function handleMessageStream(_req, res, body, agents, store) {
945
+ const params = body.params;
946
+ const message = params?.message;
947
+ if (!message?.parts?.length) {
948
+ return res.json(jsonRpcError(body.id, -32602, "Missing message.parts"));
949
+ }
950
+ const agent = resolveAgent(agents, message);
951
+ if (!agent) {
952
+ return res.json(jsonRpcError(body.id, -32602, "No matching agent found"));
953
+ }
954
+ const taskId = randomUUID();
955
+ const input = a2aPartsToText(message.parts);
956
+ const task = {
957
+ id: taskId,
958
+ sessionId: params?.sessionId ?? void 0,
959
+ status: { state: "submitted", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
960
+ history: [message],
961
+ metadata: { agentName: agent.name }
962
+ };
963
+ store.set(task);
964
+ res.writeHead(200, {
965
+ "Content-Type": "text/event-stream",
966
+ "Cache-Control": "no-cache",
967
+ Connection: "keep-alive"
968
+ });
969
+ const sendEvent = (data) => {
970
+ res.write(`data: ${JSON.stringify(data)}
971
+
972
+ `);
973
+ };
974
+ store.updateState(taskId, "working");
975
+ sendEvent({
976
+ jsonrpc: "2.0",
977
+ id: body.id,
978
+ result: store.get(taskId)
979
+ });
980
+ try {
981
+ let fullText = "";
982
+ for await (const chunk of agent.stream(input, {
983
+ sessionId: task.sessionId
984
+ })) {
985
+ if (chunk.type === "text") {
986
+ fullText += chunk.text;
987
+ sendEvent({
988
+ jsonrpc: "2.0",
989
+ id: body.id,
990
+ result: {
991
+ id: taskId,
992
+ status: {
993
+ state: "working",
994
+ message: {
995
+ role: "agent",
996
+ parts: [{ kind: "text", text: chunk.text }]
997
+ },
998
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
999
+ }
1000
+ }
1001
+ });
1002
+ }
1003
+ }
1004
+ const agentMessage = {
1005
+ role: "agent",
1006
+ parts: textToA2AParts(fullText),
1007
+ messageId: randomUUID(),
1008
+ taskId
1009
+ };
1010
+ task.history.push(agentMessage);
1011
+ store.updateState(taskId, "completed", agentMessage);
1012
+ sendEvent({
1013
+ jsonrpc: "2.0",
1014
+ id: body.id,
1015
+ result: store.get(taskId)
1016
+ });
1017
+ res.end();
1018
+ } catch (err) {
1019
+ const errorMessage = {
1020
+ role: "agent",
1021
+ parts: [{ kind: "text", text: `Error: ${err.message}` }],
1022
+ taskId
1023
+ };
1024
+ store.updateState(taskId, "failed", errorMessage);
1025
+ sendEvent({
1026
+ jsonrpc: "2.0",
1027
+ id: body.id,
1028
+ result: store.get(taskId)
1029
+ });
1030
+ res.end();
1031
+ }
1032
+ }
1033
+ function handleTasksGet(res, body, store) {
1034
+ const params = body.params;
1035
+ const taskId = params?.id;
1036
+ if (!taskId) {
1037
+ return res.json(jsonRpcError(body.id, -32602, "Missing task id"));
1038
+ }
1039
+ const task = store.get(taskId);
1040
+ if (!task) {
1041
+ return res.json(jsonRpcError(body.id, -32602, `Task '${taskId}' not found`));
1042
+ }
1043
+ const historyLength = params?.historyLength;
1044
+ const result = { ...task };
1045
+ if (historyLength && result.history) {
1046
+ result.history = result.history.slice(-historyLength);
1047
+ }
1048
+ res.json({ jsonrpc: "2.0", id: body.id, result });
1049
+ }
1050
+ function handleTasksCancel(res, body, store) {
1051
+ const params = body.params;
1052
+ const taskId = params?.id;
1053
+ if (!taskId) {
1054
+ return res.json(jsonRpcError(body.id, -32602, "Missing task id"));
1055
+ }
1056
+ const task = store.get(taskId);
1057
+ if (!task) {
1058
+ return res.json(jsonRpcError(body.id, -32602, `Task '${taskId}' not found`));
1059
+ }
1060
+ store.updateState(taskId, "canceled");
1061
+ res.json({
1062
+ jsonrpc: "2.0",
1063
+ id: body.id,
1064
+ result: store.get(taskId)
1065
+ });
1066
+ }
720
1067
  export {
721
1068
  buildMultiModalInput,
1069
+ createA2AServer,
722
1070
  createAgentGateway,
723
1071
  createAgentRouter,
724
1072
  createFileUploadMiddleware,
725
1073
  errorHandler,
1074
+ generateAgentCard,
1075
+ generateMultiAgentCard,
726
1076
  generateOpenAPISpec,
727
1077
  requestLogger
728
1078
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radaros/transport",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -20,7 +20,7 @@
20
20
  "typescript": "^5.6.0"
21
21
  },
22
22
  "peerDependencies": {
23
- "@radaros/core": "^0.1.0",
23
+ "@radaros/core": "^0.2.0",
24
24
  "@types/express": "^4.0.0 || ^5.0.0",
25
25
  "express": "^4.0.0 || ^5.0.0",
26
26
  "multer": ">=1.4.0",
@@ -0,0 +1,391 @@
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
+ }
@@ -0,0 +1,95 @@
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
+ }
@@ -0,0 +1,11 @@
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
+ }
package/src/index.ts CHANGED
@@ -7,3 +7,8 @@ export type { FileUploadOptions } from "./express/file-upload.js";
7
7
 
8
8
  export { createAgentGateway } from "./socketio/gateway.js";
9
9
  export type { GatewayOptions } from "./socketio/types.js";
10
+
11
+ // A2A
12
+ export { createA2AServer } from "./a2a/a2a-server.js";
13
+ export { generateAgentCard, generateMultiAgentCard } from "./a2a/agent-card.js";
14
+ export type { A2AServerOptions } from "./a2a/types.js";