agent-relay 3.2.2 → 3.2.4

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.
Files changed (246) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/dist/index.cjs +1358 -941
  6. package/dist/src/cli/commands/agent-management.d.ts +2 -2
  7. package/dist/src/cli/commands/agent-management.d.ts.map +1 -1
  8. package/dist/src/cli/commands/agent-management.js +41 -240
  9. package/dist/src/cli/commands/agent-management.js.map +1 -1
  10. package/dist/src/cli/commands/messaging.d.ts +1 -1
  11. package/dist/src/cli/commands/messaging.d.ts.map +1 -1
  12. package/dist/src/cli/commands/messaging.js +14 -5
  13. package/dist/src/cli/commands/messaging.js.map +1 -1
  14. package/dist/src/cli/lib/agent-management-listing.d.ts +4 -1
  15. package/dist/src/cli/lib/agent-management-listing.d.ts.map +1 -1
  16. package/dist/src/cli/lib/agent-management-listing.js +27 -2
  17. package/dist/src/cli/lib/agent-management-listing.js.map +1 -1
  18. package/package.json +11 -10
  19. package/packages/acp-bridge/package.json +2 -2
  20. package/packages/config/package.json +1 -1
  21. package/packages/hooks/package.json +4 -4
  22. package/packages/memory/package.json +2 -2
  23. package/packages/openclaw/package.json +2 -2
  24. package/packages/policy/package.json +2 -2
  25. package/packages/sdk/ADAPTER_REVIEW.md +109 -0
  26. package/packages/sdk/dist/client.d.ts +66 -0
  27. package/packages/sdk/dist/client.d.ts.map +1 -1
  28. package/packages/sdk/dist/client.js +230 -0
  29. package/packages/sdk/dist/client.js.map +1 -1
  30. package/packages/sdk/dist/communicate/a2a-bridge.d.ts +25 -0
  31. package/packages/sdk/dist/communicate/a2a-bridge.d.ts.map +1 -0
  32. package/packages/sdk/dist/communicate/a2a-bridge.js +89 -0
  33. package/packages/sdk/dist/communicate/a2a-bridge.js.map +1 -0
  34. package/packages/sdk/dist/communicate/a2a-server.d.ts +31 -0
  35. package/packages/sdk/dist/communicate/a2a-server.d.ts.map +1 -0
  36. package/packages/sdk/dist/communicate/a2a-server.js +220 -0
  37. package/packages/sdk/dist/communicate/a2a-server.js.map +1 -0
  38. package/packages/sdk/dist/communicate/a2a-transport.d.ts +48 -0
  39. package/packages/sdk/dist/communicate/a2a-transport.d.ts.map +1 -0
  40. package/packages/sdk/dist/communicate/a2a-transport.js +302 -0
  41. package/packages/sdk/dist/communicate/a2a-transport.js.map +1 -0
  42. package/packages/sdk/dist/communicate/a2a-types.d.ts +107 -0
  43. package/packages/sdk/dist/communicate/a2a-types.d.ts.map +1 -0
  44. package/packages/sdk/dist/communicate/a2a-types.js +209 -0
  45. package/packages/sdk/dist/communicate/a2a-types.js.map +1 -0
  46. package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts +28 -0
  47. package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts.map +1 -0
  48. package/packages/sdk/dist/communicate/adapters/claude-sdk.js +47 -0
  49. package/packages/sdk/dist/communicate/adapters/claude-sdk.js.map +1 -0
  50. package/packages/sdk/dist/communicate/adapters/crewai.d.ts +42 -0
  51. package/packages/sdk/dist/communicate/adapters/crewai.d.ts.map +1 -0
  52. package/packages/sdk/dist/communicate/adapters/crewai.js +95 -0
  53. package/packages/sdk/dist/communicate/adapters/crewai.js.map +1 -0
  54. package/packages/sdk/dist/communicate/adapters/google-adk.d.ts +53 -0
  55. package/packages/sdk/dist/communicate/adapters/google-adk.d.ts.map +1 -0
  56. package/packages/sdk/dist/communicate/adapters/google-adk.js +77 -0
  57. package/packages/sdk/dist/communicate/adapters/google-adk.js.map +1 -0
  58. package/packages/sdk/dist/communicate/adapters/index.d.ts +7 -0
  59. package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -0
  60. package/packages/sdk/dist/communicate/adapters/index.js +7 -0
  61. package/packages/sdk/dist/communicate/adapters/index.js.map +1 -0
  62. package/packages/sdk/dist/communicate/adapters/langgraph.d.ts +40 -0
  63. package/packages/sdk/dist/communicate/adapters/langgraph.d.ts.map +1 -0
  64. package/packages/sdk/dist/communicate/adapters/langgraph.js +77 -0
  65. package/packages/sdk/dist/communicate/adapters/langgraph.js.map +1 -0
  66. package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts +25 -0
  67. package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts.map +1 -0
  68. package/packages/sdk/dist/communicate/adapters/openai-agents.js +70 -0
  69. package/packages/sdk/dist/communicate/adapters/openai-agents.js.map +1 -0
  70. package/packages/sdk/dist/communicate/adapters/pi.d.ts +45 -0
  71. package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -0
  72. package/packages/sdk/dist/communicate/adapters/pi.js +59 -0
  73. package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -0
  74. package/packages/sdk/dist/communicate/core.d.ts +58 -0
  75. package/packages/sdk/dist/communicate/core.d.ts.map +1 -0
  76. package/packages/sdk/dist/communicate/core.js +128 -0
  77. package/packages/sdk/dist/communicate/core.js.map +1 -0
  78. package/packages/sdk/dist/communicate/index.d.ts +4 -0
  79. package/packages/sdk/dist/communicate/index.d.ts.map +1 -0
  80. package/packages/sdk/dist/communicate/index.js +4 -0
  81. package/packages/sdk/dist/communicate/index.js.map +1 -0
  82. package/packages/sdk/dist/communicate/transport.d.ts +36 -0
  83. package/packages/sdk/dist/communicate/transport.d.ts.map +1 -0
  84. package/packages/sdk/dist/communicate/transport.js +371 -0
  85. package/packages/sdk/dist/communicate/transport.js.map +1 -0
  86. package/packages/sdk/dist/communicate/types.d.ts +58 -0
  87. package/packages/sdk/dist/communicate/types.d.ts.map +1 -0
  88. package/packages/sdk/dist/communicate/types.js +66 -0
  89. package/packages/sdk/dist/communicate/types.js.map +1 -0
  90. package/packages/sdk/dist/workflows/builder.d.ts +35 -5
  91. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  92. package/packages/sdk/dist/workflows/builder.js +81 -7
  93. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  94. package/packages/sdk/dist/workflows/cli.js +14 -1
  95. package/packages/sdk/dist/workflows/cli.js.map +1 -1
  96. package/packages/sdk/dist/workflows/runner.d.ts +10 -2
  97. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  98. package/packages/sdk/dist/workflows/runner.js +95 -1
  99. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  100. package/packages/sdk/dist/workflows/types.d.ts +11 -0
  101. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  102. package/packages/sdk/examples/communicate/claude_sdk_example.ts +5 -0
  103. package/packages/sdk/examples/communicate/pi_example.ts +8 -0
  104. package/packages/sdk/package.json +48 -2
  105. package/packages/sdk/src/__tests__/builder-deterministic.test.ts +132 -0
  106. package/packages/sdk/src/__tests__/communicate/a2a-bridge.test.ts +211 -0
  107. package/packages/sdk/src/__tests__/communicate/a2a-server.test.ts +359 -0
  108. package/packages/sdk/src/__tests__/communicate/a2a-transport.test.ts +537 -0
  109. package/packages/sdk/src/__tests__/communicate/a2a-types.test.ts +297 -0
  110. package/packages/sdk/src/__tests__/communicate/adapters/claude-sdk.test.ts +163 -0
  111. package/packages/sdk/src/__tests__/communicate/adapters/crewai.test.ts +219 -0
  112. package/packages/sdk/src/__tests__/communicate/adapters/e2e-crewai.test.ts +101 -0
  113. package/packages/sdk/src/__tests__/communicate/adapters/e2e-google-adk.test.ts +166 -0
  114. package/packages/sdk/src/__tests__/communicate/adapters/e2e-langgraph.test.ts +181 -0
  115. package/packages/sdk/src/__tests__/communicate/adapters/e2e-openai-agents.test.ts +137 -0
  116. package/packages/sdk/src/__tests__/communicate/adapters/e2e-pi.test.ts +140 -0
  117. package/packages/sdk/src/__tests__/communicate/adapters/google-adk.test.ts +200 -0
  118. package/packages/sdk/src/__tests__/communicate/adapters/langgraph.test.ts +162 -0
  119. package/packages/sdk/src/__tests__/communicate/adapters/openai-agents.test.ts +166 -0
  120. package/packages/sdk/src/__tests__/communicate/adapters/pi.test.ts +140 -0
  121. package/packages/sdk/src/__tests__/communicate/core.test.ts +574 -0
  122. package/packages/sdk/src/__tests__/communicate/integration/cross-framework.test.ts +353 -0
  123. package/packages/sdk/src/__tests__/communicate/transport.test.ts +613 -0
  124. package/packages/sdk/src/__tests__/start-from.test.ts +346 -0
  125. package/packages/sdk/src/client.ts +301 -0
  126. package/packages/sdk/src/communicate/a2a-bridge.ts +111 -0
  127. package/packages/sdk/src/communicate/a2a-server.ts +277 -0
  128. package/packages/sdk/src/communicate/a2a-transport.ts +395 -0
  129. package/packages/sdk/src/communicate/a2a-types.ts +338 -0
  130. package/packages/sdk/src/communicate/adapters/claude-sdk.ts +85 -0
  131. package/packages/sdk/src/communicate/adapters/crewai.ts +141 -0
  132. package/packages/sdk/src/communicate/adapters/google-adk.ts +139 -0
  133. package/packages/sdk/src/communicate/adapters/index.ts +6 -0
  134. package/packages/sdk/src/communicate/adapters/langgraph.ts +112 -0
  135. package/packages/sdk/src/communicate/adapters/openai-agents.ts +113 -0
  136. package/packages/sdk/src/communicate/adapters/pi.ts +105 -0
  137. package/packages/sdk/src/communicate/core.ts +157 -0
  138. package/packages/sdk/src/communicate/index.ts +3 -0
  139. package/packages/sdk/src/communicate/transport.ts +489 -0
  140. package/packages/sdk/src/communicate/types.ts +106 -0
  141. package/packages/sdk/src/examples/workflows/fix-dashboard-user-registration.yaml +182 -0
  142. package/packages/sdk/src/workflows/builder.ts +97 -9
  143. package/packages/sdk/src/workflows/cli.ts +16 -1
  144. package/packages/sdk/src/workflows/runner.ts +110 -1
  145. package/packages/sdk/src/workflows/types.ts +14 -0
  146. package/packages/sdk/tsconfig.build.json +1 -7
  147. package/packages/sdk/tsconfig.json +1 -7
  148. package/packages/sdk-py/README.md +67 -25
  149. package/packages/sdk-py/examples/communicate/agno_example.py +8 -0
  150. package/packages/sdk-py/examples/communicate/claude_sdk_example.py +6 -0
  151. package/packages/sdk-py/examples/communicate/crewai_example.py +7 -0
  152. package/packages/sdk-py/examples/communicate/google_adk_example.py +7 -0
  153. package/packages/sdk-py/examples/communicate/openai_agents_example.py +8 -0
  154. package/packages/sdk-py/examples/communicate/swarms_example.py +7 -0
  155. package/packages/sdk-py/pyproject.toml +12 -1
  156. package/packages/sdk-py/src/agent_relay/__init__.py +8 -0
  157. package/packages/sdk-py/src/agent_relay/builder.py +65 -26
  158. package/packages/sdk-py/src/agent_relay/communicate/__init__.py +6 -0
  159. package/packages/sdk-py/src/agent_relay/communicate/a2a_bridge.py +138 -0
  160. package/packages/sdk-py/src/agent_relay/communicate/a2a_server.py +242 -0
  161. package/packages/sdk-py/src/agent_relay/communicate/a2a_transport.py +366 -0
  162. package/packages/sdk-py/src/agent_relay/communicate/a2a_types.py +294 -0
  163. package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +10 -0
  164. package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +74 -0
  165. package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +78 -0
  166. package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +143 -0
  167. package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +69 -0
  168. package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +86 -0
  169. package/packages/sdk-py/src/agent_relay/communicate/adapters/pi.py +175 -0
  170. package/packages/sdk-py/src/agent_relay/communicate/adapters/swarms.py +44 -0
  171. package/packages/sdk-py/src/agent_relay/communicate/core.py +293 -0
  172. package/packages/sdk-py/src/agent_relay/communicate/transport.py +502 -0
  173. package/packages/sdk-py/src/agent_relay/communicate/types.py +89 -0
  174. package/packages/sdk-py/src/agent_relay/types.py +2 -1
  175. package/packages/sdk-py/tests/communicate/__init__.py +0 -0
  176. package/packages/sdk-py/tests/communicate/adapters/__init__.py +0 -0
  177. package/packages/sdk-py/tests/communicate/adapters/e2e_test_agno.py +154 -0
  178. package/packages/sdk-py/tests/communicate/adapters/e2e_test_claude_sdk.py +428 -0
  179. package/packages/sdk-py/tests/communicate/adapters/e2e_test_crewai.py +234 -0
  180. package/packages/sdk-py/tests/communicate/adapters/e2e_test_google_adk.py +182 -0
  181. package/packages/sdk-py/tests/communicate/adapters/e2e_test_langgraph.py +262 -0
  182. package/packages/sdk-py/tests/communicate/adapters/e2e_test_openai_agents.py +88 -0
  183. package/packages/sdk-py/tests/communicate/adapters/e2e_test_pi.py +156 -0
  184. package/packages/sdk-py/tests/communicate/adapters/e2e_test_swarms.py +239 -0
  185. package/packages/sdk-py/tests/communicate/adapters/test_agno.py +140 -0
  186. package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +147 -0
  187. package/packages/sdk-py/tests/communicate/adapters/test_crewai.py +136 -0
  188. package/packages/sdk-py/tests/communicate/adapters/test_google_adk.py +125 -0
  189. package/packages/sdk-py/tests/communicate/adapters/test_openai_agents.py +99 -0
  190. package/packages/sdk-py/tests/communicate/adapters/test_pi.py +270 -0
  191. package/packages/sdk-py/tests/communicate/adapters/test_swarms.py +113 -0
  192. package/packages/sdk-py/tests/communicate/conftest.py +555 -0
  193. package/packages/sdk-py/tests/communicate/integration/__init__.py +1 -0
  194. package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +331 -0
  195. package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +151 -0
  196. package/packages/sdk-py/tests/communicate/test_a2a_bridge.py +363 -0
  197. package/packages/sdk-py/tests/communicate/test_a2a_server.py +346 -0
  198. package/packages/sdk-py/tests/communicate/test_a2a_transport.py +561 -0
  199. package/packages/sdk-py/tests/communicate/test_a2a_types.py +342 -0
  200. package/packages/sdk-py/tests/communicate/test_auto_detect.py +67 -0
  201. package/packages/sdk-py/tests/communicate/test_core.py +331 -0
  202. package/packages/sdk-py/tests/communicate/test_transport.py +373 -0
  203. package/packages/sdk-py/tests/communicate/test_types.py +285 -0
  204. package/packages/sdk-py/tests/test_builder_deterministic.py +118 -0
  205. package/packages/telemetry/package.json +1 -1
  206. package/packages/trajectory/package.json +2 -2
  207. package/packages/user-directory/package.json +2 -2
  208. package/packages/utils/package.json +2 -2
  209. package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts +0 -14
  210. package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts.map +0 -1
  211. package/packages/sdk/dist/__tests__/completion-pipeline.test.js +0 -1476
  212. package/packages/sdk/dist/__tests__/completion-pipeline.test.js.map +0 -1
  213. package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts +0 -2
  214. package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts.map +0 -1
  215. package/packages/sdk/dist/__tests__/contract-fixtures.test.js +0 -152
  216. package/packages/sdk/dist/__tests__/contract-fixtures.test.js.map +0 -1
  217. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts +0 -16
  218. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts.map +0 -1
  219. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +0 -640
  220. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +0 -1
  221. package/packages/sdk/dist/__tests__/facade.test.d.ts +0 -2
  222. package/packages/sdk/dist/__tests__/facade.test.d.ts.map +0 -1
  223. package/packages/sdk/dist/__tests__/facade.test.js +0 -305
  224. package/packages/sdk/dist/__tests__/facade.test.js.map +0 -1
  225. package/packages/sdk/dist/__tests__/integration.test.d.ts +0 -2
  226. package/packages/sdk/dist/__tests__/integration.test.d.ts.map +0 -1
  227. package/packages/sdk/dist/__tests__/integration.test.js +0 -205
  228. package/packages/sdk/dist/__tests__/integration.test.js.map +0 -1
  229. package/packages/sdk/dist/__tests__/pty.test.d.ts +0 -2
  230. package/packages/sdk/dist/__tests__/pty.test.d.ts.map +0 -1
  231. package/packages/sdk/dist/__tests__/pty.test.js +0 -20
  232. package/packages/sdk/dist/__tests__/pty.test.js.map +0 -1
  233. package/packages/sdk/dist/__tests__/quickstart.test.d.ts +0 -2
  234. package/packages/sdk/dist/__tests__/quickstart.test.d.ts.map +0 -1
  235. package/packages/sdk/dist/__tests__/quickstart.test.js +0 -176
  236. package/packages/sdk/dist/__tests__/quickstart.test.js.map +0 -1
  237. package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts +0 -2
  238. package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts.map +0 -1
  239. package/packages/sdk/dist/__tests__/spawn-from-env.test.js +0 -222
  240. package/packages/sdk/dist/__tests__/spawn-from-env.test.js.map +0 -1
  241. package/packages/sdk/dist/__tests__/unit.test.d.ts +0 -2
  242. package/packages/sdk/dist/__tests__/unit.test.d.ts.map +0 -1
  243. package/packages/sdk/dist/__tests__/unit.test.js +0 -357
  244. package/packages/sdk/dist/__tests__/unit.test.js.map +0 -1
  245. package/packages/sdk-py/agent_relay/__init__.py +0 -21
  246. package/packages/sdk-py/agent_relay/models.py +0 -398
@@ -0,0 +1,242 @@
1
+ """A2A-compliant HTTP server that exposes a Relay agent as an A2A endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from dataclasses import asdict
7
+ from typing import Any, Callable, Awaitable
8
+
9
+ from aiohttp import web
10
+
11
+ from .a2a_types import (
12
+ A2AAgentCard,
13
+ A2AMessage,
14
+ A2APart,
15
+ A2ASkill,
16
+ A2ATask,
17
+ A2ATaskStatus,
18
+ A2AConfig,
19
+ )
20
+
21
+
22
+ class A2AServer:
23
+ """Lightweight HTTP server that exposes a Relay agent as an A2A endpoint.
24
+
25
+ Routes:
26
+ GET /.well-known/agent.json -> Agent Card
27
+ POST / -> JSON-RPC 2.0 dispatcher
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ agent_name: str,
33
+ port: int = 5000,
34
+ host: str = "0.0.0.0",
35
+ skills: list[A2ASkill] | None = None,
36
+ ) -> None:
37
+ self.agent_name = agent_name
38
+ self.port = port
39
+ self.host = host
40
+ self.skills = skills or []
41
+ self.tasks: dict[str, A2ATask] = {}
42
+ self._on_message: Callable[[A2AMessage], Awaitable[A2AMessage | None] | A2AMessage | None] | None = None
43
+
44
+ self._app = web.Application()
45
+ self._app.router.add_get("/.well-known/agent.json", self._handle_agent_card)
46
+ self._app.router.add_post("/", self._handle_jsonrpc)
47
+
48
+ self._runner: web.AppRunner | None = None
49
+ self._site: web.TCPSite | None = None
50
+ self._actual_port: int | None = None
51
+
52
+ @property
53
+ def url(self) -> str:
54
+ port = self._actual_port or self.port
55
+ return f"http://{self.host}:{port}"
56
+
57
+ def on_message(self, callback: Callable[[A2AMessage], Awaitable[A2AMessage | None] | A2AMessage | None]) -> None:
58
+ """Register callback for incoming A2A messages."""
59
+ self._on_message = callback
60
+
61
+ def get_agent_card(self) -> A2AAgentCard:
62
+ """Build Agent Card for this agent."""
63
+ return A2AAgentCard(
64
+ name=self.agent_name,
65
+ description=f"Agent Relay agent: {self.agent_name}",
66
+ url=self.url,
67
+ skills=list(self.skills),
68
+ )
69
+
70
+ async def handle_message_send(self, params: dict[str, Any]) -> dict[str, Any]:
71
+ """Handle JSON-RPC message/send.
72
+
73
+ 1. Extract message from params
74
+ 2. Create or update Task
75
+ 3. Call on_message callback
76
+ 4. Return Task response
77
+ """
78
+ message_data = params.get("message", {})
79
+ parts = [A2APart(text=p.get("text")) for p in message_data.get("parts", [])]
80
+ incoming = A2AMessage(
81
+ role=message_data.get("role", "user"),
82
+ parts=parts,
83
+ messageId=message_data.get("messageId") or str(uuid.uuid4()),
84
+ contextId=message_data.get("contextId"),
85
+ taskId=message_data.get("taskId"),
86
+ )
87
+
88
+ # Create or find task
89
+ task_id = incoming.taskId or str(uuid.uuid4())
90
+ context_id = incoming.contextId or str(uuid.uuid4())
91
+
92
+ if task_id in self.tasks:
93
+ task = self.tasks[task_id]
94
+ task.messages.append(incoming)
95
+ task.status = A2ATaskStatus(state="working")
96
+ else:
97
+ task = A2ATask(
98
+ id=task_id,
99
+ contextId=context_id,
100
+ status=A2ATaskStatus(state="working"),
101
+ messages=[incoming],
102
+ )
103
+ self.tasks[task_id] = task
104
+
105
+ # Invoke callback
106
+ response_msg: A2AMessage | None = None
107
+ if self._on_message is not None:
108
+ result = self._on_message(incoming)
109
+ if hasattr(result, "__await__"):
110
+ response_msg = await result # type: ignore[union-attr]
111
+ else:
112
+ response_msg = result # type: ignore[assignment]
113
+
114
+ if response_msg is not None:
115
+ task.messages.append(response_msg)
116
+ task.status = A2ATaskStatus(state="completed", message=response_msg)
117
+ else:
118
+ task.status = A2ATaskStatus(state="completed")
119
+
120
+ return self._task_to_dict(task)
121
+
122
+ async def handle_tasks_get(self, task_id: str) -> dict[str, Any]:
123
+ """JSON-RPC: tasks/get — return task state."""
124
+ task = self.tasks.get(task_id)
125
+ if task is None:
126
+ raise KeyError(f"Task not found: {task_id}")
127
+ return self._task_to_dict(task)
128
+
129
+ async def handle_tasks_cancel(self, task_id: str) -> dict[str, Any]:
130
+ """JSON-RPC: tasks/cancel — cancel a running task."""
131
+ task = self.tasks.get(task_id)
132
+ if task is None:
133
+ raise KeyError(f"Task not found: {task_id}")
134
+ task.status = A2ATaskStatus(state="canceled")
135
+ return self._task_to_dict(task)
136
+
137
+ async def start(self) -> None:
138
+ """Start aiohttp server."""
139
+ self._runner = web.AppRunner(self._app)
140
+ await self._runner.setup()
141
+ self._site = web.TCPSite(self._runner, self.host, self.port)
142
+ await self._site.start()
143
+
144
+ # Resolve actual port (useful when port=0)
145
+ server = getattr(self._site, "_server", None)
146
+ if server is not None and server.sockets:
147
+ self._actual_port = server.sockets[0].getsockname()[1]
148
+
149
+ async def stop(self) -> None:
150
+ """Stop server."""
151
+ if self._runner is not None:
152
+ await self._runner.cleanup()
153
+ self._runner = None
154
+ self._site = None
155
+
156
+ # --- HTTP Handlers ---
157
+
158
+ async def _handle_agent_card(self, request: web.Request) -> web.Response:
159
+ card = self.get_agent_card()
160
+ return web.json_response(self._agent_card_to_dict(card))
161
+
162
+ async def _handle_jsonrpc(self, request: web.Request) -> web.Response:
163
+ try:
164
+ body = await request.json()
165
+ except Exception:
166
+ return web.json_response(
167
+ {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": None},
168
+ status=400,
169
+ )
170
+
171
+ method = body.get("method", "")
172
+ params = body.get("params", {})
173
+ rpc_id = body.get("id")
174
+
175
+ try:
176
+ if method == "message/send":
177
+ result = await self.handle_message_send(params)
178
+ elif method == "tasks/get":
179
+ task_id = params.get("id") or params.get("taskId", "")
180
+ result = await self.handle_tasks_get(task_id)
181
+ elif method == "tasks/cancel":
182
+ task_id = params.get("id") or params.get("taskId", "")
183
+ result = await self.handle_tasks_cancel(task_id)
184
+ else:
185
+ return web.json_response(
186
+ {"jsonrpc": "2.0", "error": {"code": -32601, "message": f"Method not found: {method}"}, "id": rpc_id},
187
+ status=400,
188
+ )
189
+ except KeyError as exc:
190
+ return web.json_response(
191
+ {"jsonrpc": "2.0", "error": {"code": -32602, "message": str(exc)}, "id": rpc_id},
192
+ status=404,
193
+ )
194
+
195
+ return web.json_response({"jsonrpc": "2.0", "result": result, "id": rpc_id})
196
+
197
+ # --- Serialization helpers ---
198
+
199
+ @staticmethod
200
+ def _task_to_dict(task: A2ATask) -> dict[str, Any]:
201
+ status_dict: dict[str, Any] = {"state": task.status.state}
202
+ if task.status.message is not None:
203
+ status_dict["message"] = {
204
+ "role": task.status.message.role,
205
+ "parts": [{"text": p.text} for p in task.status.message.parts],
206
+ }
207
+ if task.status.message.messageId:
208
+ status_dict["message"]["messageId"] = task.status.message.messageId
209
+
210
+ messages = []
211
+ for m in task.messages:
212
+ md: dict[str, Any] = {
213
+ "role": m.role,
214
+ "parts": [{"text": p.text} for p in m.parts],
215
+ }
216
+ if m.messageId:
217
+ md["messageId"] = m.messageId
218
+ messages.append(md)
219
+
220
+ return {
221
+ "id": task.id,
222
+ "contextId": task.contextId,
223
+ "status": status_dict,
224
+ "messages": messages,
225
+ "artifacts": task.artifacts,
226
+ }
227
+
228
+ @staticmethod
229
+ def _agent_card_to_dict(card: A2AAgentCard) -> dict[str, Any]:
230
+ return {
231
+ "name": card.name,
232
+ "description": card.description,
233
+ "url": card.url,
234
+ "version": card.version,
235
+ "capabilities": card.capabilities,
236
+ "skills": [{"id": s.id, "name": s.name, "description": s.description} for s in card.skills],
237
+ "defaultInputModes": card.defaultInputModes,
238
+ "defaultOutputModes": card.defaultOutputModes,
239
+ }
240
+
241
+
242
+ __all__ = ["A2AServer"]
@@ -0,0 +1,366 @@
1
+ """A2A (Agent-to-Agent) protocol transport implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import uuid
8
+ from contextlib import suppress
9
+ from inspect import isawaitable
10
+ from typing import Any, Callable
11
+
12
+ try:
13
+ import aiohttp
14
+ from aiohttp import web
15
+ except ImportError:
16
+ raise ImportError(
17
+ "A2A transport requires 'aiohttp'. "
18
+ "Install it with: pip install agent-relay-sdk[communicate]"
19
+ )
20
+
21
+ from .a2a_types import (
22
+ A2AAgentCard,
23
+ A2AConfig,
24
+ A2AMessage,
25
+ A2APart,
26
+ A2ASkill,
27
+ A2ATask,
28
+ A2ATaskStatus,
29
+ A2A_TASK_NOT_CANCELABLE,
30
+ A2A_TASK_NOT_FOUND,
31
+ JSONRPC_INTERNAL_ERROR,
32
+ JSONRPC_INVALID_PARAMS,
33
+ JSONRPC_METHOD_NOT_FOUND,
34
+ JSONRPC_PARSE_ERROR,
35
+ make_jsonrpc_error,
36
+ make_jsonrpc_request,
37
+ make_jsonrpc_response,
38
+ )
39
+ from .types import Message, MessageCallback
40
+
41
+
42
+ class A2ATransport:
43
+ """
44
+ Transport that speaks A2A protocol instead of Relaycast API.
45
+
46
+ Client side: sends JSON-RPC 2.0 to external A2A agent endpoints.
47
+ Server side: runs a local HTTP server accepting A2A JSON-RPC calls.
48
+ """
49
+
50
+ def __init__(self, config: A2AConfig) -> None:
51
+ self.config = config
52
+ self.agent_name: str | None = None
53
+ self.agent_card: A2AAgentCard | None = None
54
+ self.tasks: dict[str, A2ATask] = {}
55
+ self._message_callbacks: list[MessageCallback] = []
56
+ self._session: aiohttp.ClientSession | None = None
57
+ self._app: web.Application | None = None
58
+ self._runner: web.AppRunner | None = None
59
+ self._site: web.TCPSite | None = None
60
+ self._discovered_cards: dict[str, A2AAgentCard] = {}
61
+ self._closing = False
62
+
63
+ # === Transport interface ===
64
+
65
+ async def register(self, name: str) -> dict[str, Any]:
66
+ """
67
+ Register by starting an HTTP server that serves:
68
+ - GET /.well-known/agent.json -> AgentCard
69
+ - POST / -> JSON-RPC 2.0 endpoint
70
+ """
71
+ self.agent_name = name
72
+ self.agent_card = A2AAgentCard(
73
+ name=name,
74
+ description=self.config.agent_description or f"Agent Relay agent: {name}",
75
+ url=f"http://{self.config.server_host}:{self.config.server_port}",
76
+ skills=list(self.config.skills),
77
+ )
78
+
79
+ self._app = web.Application()
80
+ self._app.router.add_get("/.well-known/agent.json", self._handle_agent_card)
81
+ self._app.router.add_post("/", self._handle_jsonrpc_http)
82
+
83
+ self._runner = web.AppRunner(self._app)
84
+ await self._runner.setup()
85
+ self._site = web.TCPSite(self._runner, self.config.server_host, self.config.server_port)
86
+ await self._site.start()
87
+
88
+ return {
89
+ "name": name,
90
+ "url": self.agent_card.url,
91
+ "type": "a2a",
92
+ }
93
+
94
+ async def unregister(self) -> None:
95
+ """Stop the HTTP server and clean up."""
96
+ self._closing = True
97
+ if self._site is not None:
98
+ await self._site.stop()
99
+ self._site = None
100
+ if self._runner is not None:
101
+ await self._runner.cleanup()
102
+ self._runner = None
103
+ self._app = None
104
+ await self._close_session()
105
+ self._closing = False
106
+
107
+ async def send_dm(self, target: str, text: str) -> dict[str, Any]:
108
+ """
109
+ Send a message to an external A2A agent.
110
+
111
+ target: URL of the A2A agent endpoint
112
+ text: message text to send
113
+ """
114
+ card = await self._discover_agent(target)
115
+
116
+ message = A2AMessage(
117
+ role="user",
118
+ parts=[A2APart(text=text)],
119
+ )
120
+
121
+ rpc_request = make_jsonrpc_request(
122
+ "message/send",
123
+ {"message": message.to_dict()},
124
+ )
125
+
126
+ session = await self._ensure_session()
127
+ headers = self._auth_headers()
128
+ headers["Content-Type"] = "application/json"
129
+
130
+ async with session.post(card.url, json=rpc_request, headers=headers) as resp:
131
+ body = await resp.json()
132
+
133
+ if "error" in body:
134
+ err = body["error"]
135
+ raise A2AError(err.get("code", -1), err.get("message", "Unknown error"))
136
+
137
+ result = body.get("result", {})
138
+ return self._a2a_result_to_relay(result, card.name)
139
+
140
+ async def list_agents(self) -> list[dict[str, Any]]:
141
+ """List known A2A agents from registry."""
142
+ agents: list[dict[str, Any]] = []
143
+ for url in self.config.registry:
144
+ try:
145
+ card = await self._discover_agent(url)
146
+ agents.append({
147
+ "name": card.name,
148
+ "url": card.url,
149
+ "description": card.description,
150
+ "skills": [s.to_dict() for s in card.skills],
151
+ })
152
+ except Exception:
153
+ continue
154
+ return agents
155
+
156
+ def on_message(self, callback: MessageCallback) -> None:
157
+ """Register callback for incoming A2A messages."""
158
+ self._message_callbacks.append(callback)
159
+
160
+ async def connect_ws(self) -> None:
161
+ """
162
+ A2A uses HTTP, not WebSocket. This is a no-op.
163
+ The HTTP server started in register() handles incoming calls.
164
+ """
165
+ pass
166
+
167
+ # === HTTP handlers for incoming A2A requests ===
168
+
169
+ async def _handle_agent_card(self, request: web.Request) -> web.Response:
170
+ """Serve the Agent Card at /.well-known/agent.json."""
171
+ if self.agent_card is None:
172
+ return web.json_response({"error": "Not registered"}, status=503)
173
+ return web.json_response(self.agent_card.to_dict())
174
+
175
+ async def _handle_jsonrpc_http(self, request: web.Request) -> web.Response:
176
+ """Handle incoming JSON-RPC 2.0 requests over HTTP."""
177
+ try:
178
+ body = await request.json()
179
+ except (json.JSONDecodeError, Exception):
180
+ error = make_jsonrpc_error(JSONRPC_PARSE_ERROR, "Parse error", None)
181
+ return web.json_response(error)
182
+
183
+ result = await self._dispatch_jsonrpc(body)
184
+ return web.json_response(result)
185
+
186
+ async def _dispatch_jsonrpc(self, request: dict[str, Any]) -> dict[str, Any]:
187
+ """Dispatch a JSON-RPC request to the appropriate handler."""
188
+ rpc_id = request.get("id")
189
+ method = request.get("method", "")
190
+ params = request.get("params", {})
191
+
192
+ handlers = {
193
+ "message/send": self._handle_message_send,
194
+ "tasks/get": self._handle_tasks_get,
195
+ "tasks/cancel": self._handle_tasks_cancel,
196
+ }
197
+
198
+ handler = handlers.get(method)
199
+ if handler is None:
200
+ return make_jsonrpc_error(JSONRPC_METHOD_NOT_FOUND, f"Method not found: {method}", rpc_id)
201
+
202
+ try:
203
+ result = await handler(params)
204
+ return make_jsonrpc_response(result, rpc_id)
205
+ except A2AError as exc:
206
+ return make_jsonrpc_error(exc.code, exc.message, rpc_id)
207
+ except Exception as exc:
208
+ return make_jsonrpc_error(JSONRPC_INTERNAL_ERROR, str(exc), rpc_id)
209
+
210
+ async def _handle_message_send(self, params: dict[str, Any]) -> dict[str, Any]:
211
+ """Handle message/send JSON-RPC method."""
212
+ msg_data = params.get("message")
213
+ if not msg_data:
214
+ raise A2AError(JSONRPC_INVALID_PARAMS, "Missing 'message' in params")
215
+
216
+ a2a_msg = A2AMessage.from_dict(msg_data)
217
+
218
+ # Create or update task
219
+ task_id = a2a_msg.taskId or str(uuid.uuid4())
220
+ context_id = a2a_msg.contextId or str(uuid.uuid4())
221
+
222
+ if task_id in self.tasks:
223
+ task = self.tasks[task_id]
224
+ task.messages.append(a2a_msg)
225
+ task.status = A2ATaskStatus(state="working")
226
+ else:
227
+ task = A2ATask(
228
+ id=task_id,
229
+ contextId=context_id,
230
+ status=A2ATaskStatus(state="working"),
231
+ messages=[a2a_msg],
232
+ )
233
+ self.tasks[task_id] = task
234
+
235
+ # Convert to Relay message and invoke callbacks
236
+ relay_msg = self._a2a_to_relay_msg(a2a_msg, sender="a2a-client")
237
+ await self._invoke_callbacks(relay_msg)
238
+
239
+ # Mark completed
240
+ task.status = A2ATaskStatus(state="completed")
241
+
242
+ return task.to_dict()
243
+
244
+ async def _handle_tasks_get(self, params: dict[str, Any]) -> dict[str, Any]:
245
+ """Handle tasks/get JSON-RPC method."""
246
+ task_id = params.get("id")
247
+ if not task_id or task_id not in self.tasks:
248
+ raise A2AError(A2A_TASK_NOT_FOUND, f"Task not found: {task_id}")
249
+ return self.tasks[task_id].to_dict()
250
+
251
+ async def _handle_tasks_cancel(self, params: dict[str, Any]) -> dict[str, Any]:
252
+ """Handle tasks/cancel JSON-RPC method."""
253
+ task_id = params.get("id")
254
+ if not task_id or task_id not in self.tasks:
255
+ raise A2AError(A2A_TASK_NOT_FOUND, f"Task not found: {task_id}")
256
+
257
+ task = self.tasks[task_id]
258
+ if task.status.state in ("completed", "failed", "canceled"):
259
+ raise A2AError(A2A_TASK_NOT_CANCELABLE, f"Task {task_id} is already {task.status.state}")
260
+
261
+ task.status = A2ATaskStatus(state="canceled")
262
+ return task.to_dict()
263
+
264
+ # === Agent discovery ===
265
+
266
+ async def _discover_agent(self, url: str) -> A2AAgentCard:
267
+ """Fetch and parse Agent Card from /.well-known/agent.json."""
268
+ url = url.rstrip("/")
269
+
270
+ if url in self._discovered_cards:
271
+ return self._discovered_cards[url]
272
+
273
+ session = await self._ensure_session()
274
+ card_url = f"{url}/.well-known/agent.json"
275
+
276
+ async with session.get(card_url) as resp:
277
+ if resp.status != 200:
278
+ raise A2AError(-1, f"Failed to discover agent at {card_url}: HTTP {resp.status}")
279
+ data = await resp.json()
280
+
281
+ card = A2AAgentCard.from_dict(data)
282
+ self._discovered_cards[url] = card
283
+ return card
284
+
285
+ # === Message conversion ===
286
+
287
+ @staticmethod
288
+ def _relay_msg_to_a2a(text: str, sender: str) -> A2AMessage:
289
+ """Convert Relay message text to A2A Message."""
290
+ return A2AMessage(
291
+ role="user",
292
+ parts=[A2APart(text=text)],
293
+ )
294
+
295
+ @staticmethod
296
+ def _a2a_to_relay_msg(msg: A2AMessage, sender: str = "unknown") -> Message:
297
+ """Convert A2A Message to Relay Message format."""
298
+ text = msg.get_text()
299
+ return Message(
300
+ sender=sender,
301
+ text=text,
302
+ channel=None,
303
+ thread_id=msg.contextId,
304
+ message_id=msg.messageId,
305
+ )
306
+
307
+ @staticmethod
308
+ def _a2a_result_to_relay(result: dict[str, Any], sender: str) -> dict[str, Any]:
309
+ """Convert A2A task/message result to Relay-compatible dict."""
310
+ # Result could be a Task dict
311
+ messages = result.get("messages", [])
312
+ text = ""
313
+ if messages:
314
+ last_msg = messages[-1]
315
+ parts = last_msg.get("parts", [])
316
+ text = " ".join(p.get("text", "") for p in parts if p.get("text"))
317
+
318
+ return {
319
+ "sender": sender,
320
+ "text": text,
321
+ "task_id": result.get("id"),
322
+ "status": result.get("status", {}).get("state"),
323
+ }
324
+
325
+ # === Internal helpers ===
326
+
327
+ async def _invoke_callbacks(self, msg: Message) -> None:
328
+ """Invoke all registered message callbacks."""
329
+ for cb in self._message_callbacks:
330
+ result = cb(msg)
331
+ if isawaitable(result):
332
+ await result
333
+
334
+ def _auth_headers(self) -> dict[str, str]:
335
+ """Build auth headers from config."""
336
+ headers: dict[str, str] = {}
337
+ if self.config.auth_token:
338
+ if self.config.auth_scheme == "bearer":
339
+ headers["Authorization"] = f"Bearer {self.config.auth_token}"
340
+ elif self.config.auth_scheme == "api_key":
341
+ headers["X-API-Key"] = self.config.auth_token
342
+ else:
343
+ headers["Authorization"] = f"Bearer {self.config.auth_token}"
344
+ return headers
345
+
346
+ async def _ensure_session(self) -> aiohttp.ClientSession:
347
+ if self._session is None or self._session.closed:
348
+ self._session = aiohttp.ClientSession()
349
+ return self._session
350
+
351
+ async def _close_session(self) -> None:
352
+ if self._session is not None and not self._session.closed:
353
+ await self._session.close()
354
+ self._session = None
355
+
356
+
357
+ class A2AError(Exception):
358
+ """Error raised during A2A protocol operations."""
359
+
360
+ def __init__(self, code: int, message: str) -> None:
361
+ self.code = code
362
+ self.message = message
363
+ super().__init__(f"A2A error {code}: {message}")
364
+
365
+
366
+ __all__ = ["A2AError", "A2ATransport"]