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,363 @@
1
+ """Tests for A2ABridge — bidirectional message forwarding between A2A and Relay."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import uuid
8
+ from unittest.mock import AsyncMock, MagicMock, patch
9
+
10
+ import aiohttp
11
+ import pytest
12
+ import pytest_asyncio
13
+ from aiohttp import web
14
+
15
+ from agent_relay.communicate.a2a_bridge import A2ABridge
16
+ from agent_relay.communicate.a2a_server import A2AServer
17
+ from agent_relay.communicate.a2a_types import A2AMessage, A2APart, A2ASkill
18
+ from agent_relay.communicate.types import Message, RelayConfig
19
+
20
+
21
+ class MockA2AAgent:
22
+ """A minimal mock A2A agent server for testing the bridge."""
23
+
24
+ def __init__(self, name: str = "external-agent", response_text: str = "A2A response") -> None:
25
+ self.name = name
26
+ self.response_text = response_text
27
+ self.received_messages: list[dict] = []
28
+
29
+ self._app = web.Application()
30
+ self._app.router.add_get("/.well-known/agent.json", self._handle_card)
31
+ self._app.router.add_post("/", self._handle_jsonrpc)
32
+ self._runner: web.AppRunner | None = None
33
+ self._site: web.TCPSite | None = None
34
+ self.url = ""
35
+
36
+ async def _handle_card(self, request: web.Request) -> web.Response:
37
+ return web.json_response({
38
+ "name": self.name,
39
+ "description": f"Mock A2A agent: {self.name}",
40
+ "url": self.url,
41
+ "version": "1.0.0",
42
+ "capabilities": {"streaming": False, "pushNotifications": False},
43
+ "skills": [],
44
+ "defaultInputModes": ["text"],
45
+ "defaultOutputModes": ["text"],
46
+ })
47
+
48
+ async def _handle_jsonrpc(self, request: web.Request) -> web.Response:
49
+ body = await request.json()
50
+ self.received_messages.append(body)
51
+
52
+ method = body.get("method", "")
53
+ rpc_id = body.get("id")
54
+
55
+ if method == "message/send":
56
+ task_id = str(uuid.uuid4())
57
+ response_msg = {
58
+ "role": "agent",
59
+ "parts": [{"text": self.response_text}],
60
+ "messageId": str(uuid.uuid4()),
61
+ }
62
+ result = {
63
+ "id": task_id,
64
+ "contextId": str(uuid.uuid4()),
65
+ "status": {"state": "completed", "message": response_msg},
66
+ "messages": [
67
+ body.get("params", {}).get("message", {}),
68
+ response_msg,
69
+ ],
70
+ "artifacts": [],
71
+ }
72
+ return web.json_response({"jsonrpc": "2.0", "result": result, "id": rpc_id})
73
+
74
+ return web.json_response(
75
+ {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": rpc_id}
76
+ )
77
+
78
+ async def start(self) -> None:
79
+ self._runner = web.AppRunner(self._app)
80
+ await self._runner.setup()
81
+ self._site = web.TCPSite(self._runner, "127.0.0.1", 0)
82
+ await self._site.start()
83
+ server = getattr(self._site, "_server", None)
84
+ if server and server.sockets:
85
+ port = server.sockets[0].getsockname()[1]
86
+ self.url = f"http://127.0.0.1:{port}"
87
+
88
+ async def stop(self) -> None:
89
+ if self._runner is not None:
90
+ await self._runner.cleanup()
91
+
92
+
93
+ class MockA2AAgentEmpty:
94
+ """A mock A2A agent that returns no response text."""
95
+
96
+ def __init__(self) -> None:
97
+ self._app = web.Application()
98
+ self._app.router.add_post("/", self._handle_jsonrpc)
99
+ self._runner: web.AppRunner | None = None
100
+ self._site: web.TCPSite | None = None
101
+ self.url = ""
102
+
103
+ async def _handle_jsonrpc(self, request: web.Request) -> web.Response:
104
+ body = await request.json()
105
+ rpc_id = body.get("id")
106
+ result = {
107
+ "id": str(uuid.uuid4()),
108
+ "status": {"state": "completed"},
109
+ "messages": [],
110
+ "artifacts": [],
111
+ }
112
+ return web.json_response({"jsonrpc": "2.0", "result": result, "id": rpc_id})
113
+
114
+ async def start(self) -> None:
115
+ self._runner = web.AppRunner(self._app)
116
+ await self._runner.setup()
117
+ self._site = web.TCPSite(self._runner, "127.0.0.1", 0)
118
+ await self._site.start()
119
+ server = getattr(self._site, "_server", None)
120
+ if server and server.sockets:
121
+ port = server.sockets[0].getsockname()[1]
122
+ self.url = f"http://127.0.0.1:{port}"
123
+
124
+ async def stop(self) -> None:
125
+ if self._runner is not None:
126
+ await self._runner.cleanup()
127
+
128
+
129
+ @pytest_asyncio.fixture
130
+ async def mock_a2a_agent():
131
+ agent = MockA2AAgent(response_text="Hello from external A2A!")
132
+ await agent.start()
133
+ try:
134
+ yield agent
135
+ finally:
136
+ await agent.stop()
137
+
138
+
139
+ # --- Unit tests with mocked Relay ---
140
+
141
+
142
+ class TestA2ABridgeConstruction:
143
+ def test_bridge_creation(self):
144
+ config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
145
+ bridge = A2ABridge(
146
+ relay_config=config,
147
+ a2a_agent_url="http://localhost:8000",
148
+ proxy_name="proxy-agent",
149
+ )
150
+ assert bridge.proxy_name == "proxy-agent"
151
+ assert bridge.a2a_agent_url == "http://localhost:8000"
152
+ assert bridge.relay.agent_name == "proxy-agent"
153
+ assert bridge._started is False
154
+
155
+ def test_url_trailing_slash_stripped(self):
156
+ config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
157
+ bridge = A2ABridge(
158
+ relay_config=config,
159
+ a2a_agent_url="http://localhost:8000/",
160
+ proxy_name="proxy",
161
+ )
162
+ assert bridge.a2a_agent_url == "http://localhost:8000"
163
+
164
+
165
+ class TestA2ABridgeDiscovery:
166
+ async def test_discover_agent(self, mock_a2a_agent: MockA2AAgent):
167
+ config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
168
+ bridge = A2ABridge(
169
+ relay_config=config,
170
+ a2a_agent_url=mock_a2a_agent.url,
171
+ proxy_name="proxy",
172
+ )
173
+ try:
174
+ card = await bridge.discover_agent()
175
+ assert card.name == "external-agent"
176
+ assert card.url == mock_a2a_agent.url
177
+ assert card.version == "1.0.0"
178
+ finally:
179
+ if bridge._session and not bridge._session.closed:
180
+ await bridge._session.close()
181
+
182
+
183
+ class TestA2ABridgeSendMessage:
184
+ async def test_send_a2a_message(self, mock_a2a_agent: MockA2AAgent):
185
+ config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
186
+ bridge = A2ABridge(
187
+ relay_config=config,
188
+ a2a_agent_url=mock_a2a_agent.url,
189
+ proxy_name="proxy",
190
+ )
191
+ try:
192
+ response_text = await bridge.send_a2a_message("Hello, A2A agent!")
193
+ assert response_text == "Hello from external A2A!"
194
+
195
+ # Verify the mock received the JSON-RPC request
196
+ assert len(mock_a2a_agent.received_messages) == 1
197
+ req = mock_a2a_agent.received_messages[0]
198
+ assert req["method"] == "message/send"
199
+ assert req["params"]["message"]["role"] == "user"
200
+ assert req["params"]["message"]["parts"][0]["text"] == "Hello, A2A agent!"
201
+ finally:
202
+ if bridge._session and not bridge._session.closed:
203
+ await bridge._session.close()
204
+
205
+ async def test_send_uses_discovered_card_url(self, mock_a2a_agent: MockA2AAgent):
206
+ config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
207
+ bridge = A2ABridge(
208
+ relay_config=config,
209
+ a2a_agent_url=mock_a2a_agent.url,
210
+ proxy_name="proxy",
211
+ )
212
+ try:
213
+ # Discover first, then send
214
+ await bridge.discover_agent()
215
+ response = await bridge.send_a2a_message("After discovery")
216
+ assert response == "Hello from external A2A!"
217
+ finally:
218
+ if bridge._session and not bridge._session.closed:
219
+ await bridge._session.close()
220
+
221
+
222
+ class TestA2ABridgeRelayForwarding:
223
+ async def test_handle_relay_message_forwards_to_a2a(self, mock_a2a_agent: MockA2AAgent):
224
+ """Test that _handle_relay_message forwards to A2A and sends response back."""
225
+ config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
226
+ bridge = A2ABridge(
227
+ relay_config=config,
228
+ a2a_agent_url=mock_a2a_agent.url,
229
+ proxy_name="proxy",
230
+ )
231
+
232
+ # Mock relay.send so we can verify it's called with the A2A response
233
+ bridge.relay.send = AsyncMock()
234
+ bridge.relay._connected = True
235
+
236
+ try:
237
+ relay_msg = Message(sender="alice", text="Please process this")
238
+ await bridge._handle_relay_message(relay_msg)
239
+
240
+ # Verify A2A agent received the message
241
+ assert len(mock_a2a_agent.received_messages) == 1
242
+ assert mock_a2a_agent.received_messages[0]["params"]["message"]["parts"][0]["text"] == "Please process this"
243
+
244
+ # Verify bridge forwarded response back via Relay
245
+ bridge.relay.send.assert_called_once_with("alice", "Hello from external A2A!")
246
+ finally:
247
+ if bridge._session and not bridge._session.closed:
248
+ await bridge._session.close()
249
+
250
+ async def test_handle_relay_message_no_response(self):
251
+ """Test behavior when A2A agent returns no text in response."""
252
+ config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
253
+
254
+ # Create a mock A2A agent that returns empty response (no parts)
255
+ empty_agent = MockA2AAgentEmpty()
256
+ await empty_agent.start()
257
+ bridge = None
258
+ try:
259
+ bridge = A2ABridge(
260
+ relay_config=config,
261
+ a2a_agent_url=empty_agent.url,
262
+ proxy_name="proxy",
263
+ )
264
+ bridge.relay.send = AsyncMock()
265
+ bridge.relay._connected = True
266
+
267
+ relay_msg = Message(sender="bob", text="Hello")
268
+ await bridge._handle_relay_message(relay_msg)
269
+
270
+ # Should not call relay.send when no response text
271
+ bridge.relay.send.assert_not_called()
272
+ finally:
273
+ await empty_agent.stop()
274
+ if bridge and bridge._session and not bridge._session.closed:
275
+ await bridge._session.close()
276
+
277
+
278
+ class TestA2ABridgeContextManager:
279
+ async def test_context_manager(self):
280
+ config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
281
+ bridge = A2ABridge(
282
+ relay_config=config,
283
+ a2a_agent_url="http://localhost:8000",
284
+ proxy_name="proxy",
285
+ )
286
+
287
+ # Mock the relay to avoid actual connections
288
+ bridge.relay.__aenter__ = AsyncMock(return_value=bridge.relay)
289
+ bridge.relay.__aexit__ = AsyncMock(return_value=None)
290
+ bridge.relay.on_message = MagicMock()
291
+
292
+ async with bridge:
293
+ assert bridge._started is True
294
+
295
+ assert bridge._started is False
296
+
297
+
298
+ # --- Integration: A2AServer <-> A2ABridge ---
299
+
300
+
301
+ class TestA2AServerBridgeIntegration:
302
+ async def test_bridge_sends_to_a2a_server(self):
303
+ """A2ABridge can send messages to an A2AServer."""
304
+ # Set up an A2A server with an echo handler
305
+ server = A2AServer(agent_name="echo-server", port=0, host="127.0.0.1")
306
+
307
+ async def echo(msg: A2AMessage) -> A2AMessage:
308
+ text = msg.parts[0].text if msg.parts else ""
309
+ return A2AMessage(role="agent", parts=[A2APart(text=f"echo: {text}")])
310
+
311
+ server.on_message(echo)
312
+ await server.start()
313
+
314
+ try:
315
+ config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
316
+ bridge = A2ABridge(
317
+ relay_config=config,
318
+ a2a_agent_url=server.url,
319
+ proxy_name="proxy",
320
+ )
321
+ try:
322
+ # Discover and send
323
+ card = await bridge.discover_agent()
324
+ assert card.name == "echo-server"
325
+
326
+ response = await bridge.send_a2a_message("Hello server!")
327
+ assert response == "echo: Hello server!"
328
+ finally:
329
+ if bridge._session and not bridge._session.closed:
330
+ await bridge._session.close()
331
+ finally:
332
+ await server.stop()
333
+
334
+ async def test_bridge_full_relay_forwarding_to_a2a_server(self):
335
+ """Full integration: Relay message -> Bridge -> A2AServer -> response back."""
336
+ server = A2AServer(agent_name="responder", port=0, host="127.0.0.1")
337
+
338
+ async def respond(msg: A2AMessage) -> A2AMessage:
339
+ return A2AMessage(role="agent", parts=[A2APart(text="processed")])
340
+
341
+ server.on_message(respond)
342
+ await server.start()
343
+
344
+ try:
345
+ config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
346
+ bridge = A2ABridge(
347
+ relay_config=config,
348
+ a2a_agent_url=server.url,
349
+ proxy_name="proxy",
350
+ )
351
+ bridge.relay.send = AsyncMock()
352
+ bridge.relay._connected = True
353
+
354
+ try:
355
+ relay_msg = Message(sender="user-1", text="do something")
356
+ await bridge._handle_relay_message(relay_msg)
357
+
358
+ bridge.relay.send.assert_called_once_with("user-1", "processed")
359
+ finally:
360
+ if bridge._session and not bridge._session.closed:
361
+ await bridge._session.close()
362
+ finally:
363
+ await server.stop()