@radaros/transport 0.1.0 → 0.3.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 +36 -2
- package/dist/index.js +350 -0
- package/package.json +2 -2
- package/src/a2a/a2a-server.ts +391 -0
- package/src/a2a/agent-card.ts +95 -0
- package/src/a2a/types.ts +11 -0
- package/src/index.ts +5 -0
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.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.
|
|
23
|
+
"@radaros/core": "^0.3.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
|
+
}
|
package/src/a2a/types.ts
ADDED
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";
|