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,561 @@
1
+ """Tests for A2A transport implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from unittest.mock import AsyncMock, MagicMock, patch
8
+
9
+ import pytest
10
+ from aiohttp import web
11
+ from aiohttp.test_utils import AioHTTPTestCase, TestServer, TestClient
12
+
13
+ from agent_relay.communicate.a2a_types import (
14
+ A2AAgentCard,
15
+ A2AConfig,
16
+ A2AMessage,
17
+ A2APart,
18
+ A2ASkill,
19
+ A2ATask,
20
+ A2ATaskStatus,
21
+ make_jsonrpc_request,
22
+ make_jsonrpc_response,
23
+ )
24
+ from agent_relay.communicate.a2a_transport import A2AError, A2ATransport
25
+ from agent_relay.communicate.types import Message
26
+
27
+
28
+ # === Fixtures ===
29
+
30
+
31
+ def _make_agent_card(name: str = "remote-agent", url: str = "http://localhost:9999") -> dict:
32
+ return A2AAgentCard(
33
+ name=name,
34
+ description=f"Test agent: {name}",
35
+ url=url,
36
+ skills=[A2ASkill(id="s1", name="Echo", description="Echo messages")],
37
+ ).to_dict()
38
+
39
+
40
+ def _make_remote_a2a_app() -> web.Application:
41
+ """Create a mock A2A remote agent app for testing client-side calls."""
42
+ app = web.Application()
43
+ # agent_card will be patched after we know the port
44
+ app["agent_card"] = None
45
+
46
+ async def handle_agent_card(request: web.Request) -> web.Response:
47
+ return web.json_response(request.app["agent_card"])
48
+
49
+ async def handle_jsonrpc(request: web.Request) -> web.Response:
50
+ body = await request.json()
51
+ method = body.get("method", "")
52
+ rpc_id = body.get("id")
53
+ params = body.get("params", {})
54
+
55
+ if method == "message/send":
56
+ msg = params.get("message", {})
57
+ text = " ".join(
58
+ p.get("text", "") for p in msg.get("parts", []) if p.get("text")
59
+ )
60
+ task = A2ATask(
61
+ id="task-001",
62
+ contextId="ctx-001",
63
+ status=A2ATaskStatus(state="completed"),
64
+ messages=[
65
+ A2AMessage.from_dict(msg),
66
+ A2AMessage(
67
+ role="agent",
68
+ parts=[A2APart(text=f"Echo: {text}")],
69
+ messageId="resp-1",
70
+ ),
71
+ ],
72
+ )
73
+ return web.json_response(make_jsonrpc_response(task.to_dict(), rpc_id))
74
+
75
+ return web.json_response({
76
+ "jsonrpc": "2.0",
77
+ "error": {"code": -32601, "message": f"Method not found: {method}"},
78
+ "id": rpc_id,
79
+ })
80
+
81
+ app.router.add_get("/.well-known/agent.json", handle_agent_card)
82
+ app.router.add_post("/", handle_jsonrpc)
83
+ return app
84
+
85
+
86
+ @pytest.fixture
87
+ async def remote_agent():
88
+ """Start a mock remote A2A agent server."""
89
+ app = _make_remote_a2a_app()
90
+ runner = web.AppRunner(app)
91
+ await runner.setup()
92
+ site = web.TCPSite(runner, "127.0.0.1", 0)
93
+ await site.start()
94
+
95
+ # Get the actual port and set the agent card with correct URL
96
+ sockets = site._server.sockets
97
+ port = sockets[0].getsockname()[1]
98
+ base_url = f"http://127.0.0.1:{port}"
99
+ app["agent_card"] = _make_agent_card(url=base_url)
100
+
101
+ yield base_url
102
+
103
+ await site.stop()
104
+ await runner.cleanup()
105
+
106
+
107
+ @pytest.fixture
108
+ def a2a_config():
109
+ """Create a basic A2A config for testing."""
110
+ return A2AConfig(
111
+ server_port=0, # Use 0 for auto-assign
112
+ server_host="127.0.0.1",
113
+ agent_description="Test agent",
114
+ )
115
+
116
+
117
+ # === A2ATransport registration tests ===
118
+
119
+
120
+ class TestA2ATransportRegister:
121
+ @pytest.mark.asyncio
122
+ async def test_register_starts_server(self):
123
+ config = A2AConfig(server_port=0, server_host="127.0.0.1")
124
+ transport = A2ATransport(config)
125
+ try:
126
+ result = await transport.register("test-agent")
127
+ assert result["name"] == "test-agent"
128
+ assert result["type"] == "a2a"
129
+ assert transport.agent_name == "test-agent"
130
+ assert transport.agent_card is not None
131
+ assert transport.agent_card.name == "test-agent"
132
+ finally:
133
+ await transport.unregister()
134
+
135
+ @pytest.mark.asyncio
136
+ async def test_unregister_stops_server(self):
137
+ config = A2AConfig(server_port=0, server_host="127.0.0.1")
138
+ transport = A2ATransport(config)
139
+ await transport.register("test-agent")
140
+ await transport.unregister()
141
+ assert transport._site is None
142
+ assert transport._runner is None
143
+ assert transport._app is None
144
+
145
+
146
+ # === A2ATransport server-side tests (incoming JSON-RPC) ===
147
+
148
+
149
+ class TestA2ATransportServer:
150
+ @pytest.mark.asyncio
151
+ async def test_agent_card_endpoint(self):
152
+ config = A2AConfig(server_port=0, server_host="127.0.0.1")
153
+ transport = A2ATransport(config)
154
+ try:
155
+ await transport.register("card-test-agent")
156
+
157
+ # Get the server port
158
+ sockets = transport._site._server.sockets
159
+ port = sockets[0].getsockname()[1]
160
+
161
+ import aiohttp
162
+ async with aiohttp.ClientSession() as session:
163
+ async with session.get(f"http://127.0.0.1:{port}/.well-known/agent.json") as resp:
164
+ assert resp.status == 200
165
+ data = await resp.json()
166
+ assert data["name"] == "card-test-agent"
167
+ assert data["version"] == "1.0.0"
168
+ finally:
169
+ await transport.unregister()
170
+
171
+ @pytest.mark.asyncio
172
+ async def test_message_send_jsonrpc(self):
173
+ config = A2AConfig(server_port=0, server_host="127.0.0.1")
174
+ transport = A2ATransport(config)
175
+ received: list[Message] = []
176
+ transport.on_message(lambda msg: received.append(msg))
177
+
178
+ try:
179
+ await transport.register("rpc-test-agent")
180
+
181
+ sockets = transport._site._server.sockets
182
+ port = sockets[0].getsockname()[1]
183
+
184
+ rpc_request = make_jsonrpc_request(
185
+ "message/send",
186
+ {
187
+ "message": {
188
+ "role": "user",
189
+ "parts": [{"text": "hello from test"}],
190
+ "messageId": "msg-test-1",
191
+ }
192
+ },
193
+ id="req-1",
194
+ )
195
+
196
+ import aiohttp
197
+ async with aiohttp.ClientSession() as session:
198
+ async with session.post(
199
+ f"http://127.0.0.1:{port}/",
200
+ json=rpc_request,
201
+ ) as resp:
202
+ assert resp.status == 200
203
+ data = await resp.json()
204
+ assert data["jsonrpc"] == "2.0"
205
+ assert data["id"] == "req-1"
206
+ result = data["result"]
207
+ assert result["status"]["state"] == "completed"
208
+ assert len(result["messages"]) == 1
209
+
210
+ # Verify callback was invoked
211
+ assert len(received) == 1
212
+ assert received[0].text == "hello from test"
213
+ finally:
214
+ await transport.unregister()
215
+
216
+ @pytest.mark.asyncio
217
+ async def test_async_message_callback(self):
218
+ config = A2AConfig(server_port=0, server_host="127.0.0.1")
219
+ transport = A2ATransport(config)
220
+ received: list[Message] = []
221
+
222
+ async def async_cb(msg: Message) -> None:
223
+ received.append(msg)
224
+
225
+ transport.on_message(async_cb)
226
+
227
+ try:
228
+ await transport.register("async-cb-agent")
229
+ sockets = transport._site._server.sockets
230
+ port = sockets[0].getsockname()[1]
231
+
232
+ rpc_request = make_jsonrpc_request(
233
+ "message/send",
234
+ {"message": {"role": "user", "parts": [{"text": "async test"}], "messageId": "m1"}},
235
+ id="r1",
236
+ )
237
+
238
+ import aiohttp
239
+ async with aiohttp.ClientSession() as session:
240
+ async with session.post(f"http://127.0.0.1:{port}/", json=rpc_request) as resp:
241
+ assert resp.status == 200
242
+
243
+ assert len(received) == 1
244
+ assert received[0].text == "async test"
245
+ finally:
246
+ await transport.unregister()
247
+
248
+ @pytest.mark.asyncio
249
+ async def test_tasks_get(self):
250
+ config = A2AConfig(server_port=0, server_host="127.0.0.1")
251
+ transport = A2ATransport(config)
252
+
253
+ try:
254
+ await transport.register("tasks-get-agent")
255
+ sockets = transport._site._server.sockets
256
+ port = sockets[0].getsockname()[1]
257
+
258
+ import aiohttp
259
+ async with aiohttp.ClientSession() as session:
260
+ # First send a message to create a task
261
+ send_req = make_jsonrpc_request(
262
+ "message/send",
263
+ {"message": {"role": "user", "parts": [{"text": "create task"}], "messageId": "m1"}},
264
+ id="r1",
265
+ )
266
+ async with session.post(f"http://127.0.0.1:{port}/", json=send_req) as resp:
267
+ send_data = await resp.json()
268
+ task_id = send_data["result"]["id"]
269
+
270
+ # Then get the task
271
+ get_req = make_jsonrpc_request("tasks/get", {"id": task_id}, id="r2")
272
+ async with session.post(f"http://127.0.0.1:{port}/", json=get_req) as resp:
273
+ data = await resp.json()
274
+ assert data["result"]["id"] == task_id
275
+ assert data["result"]["status"]["state"] == "completed"
276
+ finally:
277
+ await transport.unregister()
278
+
279
+ @pytest.mark.asyncio
280
+ async def test_tasks_get_not_found(self):
281
+ config = A2AConfig(server_port=0, server_host="127.0.0.1")
282
+ transport = A2ATransport(config)
283
+
284
+ try:
285
+ await transport.register("tasks-404-agent")
286
+ sockets = transport._site._server.sockets
287
+ port = sockets[0].getsockname()[1]
288
+
289
+ import aiohttp
290
+ async with aiohttp.ClientSession() as session:
291
+ req = make_jsonrpc_request("tasks/get", {"id": "nonexistent"}, id="r1")
292
+ async with session.post(f"http://127.0.0.1:{port}/", json=req) as resp:
293
+ data = await resp.json()
294
+ assert "error" in data
295
+ assert data["error"]["code"] == -32001
296
+
297
+ finally:
298
+ await transport.unregister()
299
+
300
+ @pytest.mark.asyncio
301
+ async def test_tasks_cancel(self):
302
+ config = A2AConfig(server_port=0, server_host="127.0.0.1")
303
+ transport = A2ATransport(config)
304
+
305
+ try:
306
+ await transport.register("cancel-agent")
307
+
308
+ # Manually create a task in working state
309
+ task = A2ATask(
310
+ id="cancel-task-1",
311
+ status=A2ATaskStatus(state="working"),
312
+ )
313
+ transport.tasks["cancel-task-1"] = task
314
+
315
+ sockets = transport._site._server.sockets
316
+ port = sockets[0].getsockname()[1]
317
+
318
+ import aiohttp
319
+ async with aiohttp.ClientSession() as session:
320
+ req = make_jsonrpc_request("tasks/cancel", {"id": "cancel-task-1"}, id="r1")
321
+ async with session.post(f"http://127.0.0.1:{port}/", json=req) as resp:
322
+ data = await resp.json()
323
+ assert data["result"]["status"]["state"] == "canceled"
324
+ finally:
325
+ await transport.unregister()
326
+
327
+ @pytest.mark.asyncio
328
+ async def test_method_not_found(self):
329
+ config = A2AConfig(server_port=0, server_host="127.0.0.1")
330
+ transport = A2ATransport(config)
331
+
332
+ try:
333
+ await transport.register("method-404-agent")
334
+ sockets = transport._site._server.sockets
335
+ port = sockets[0].getsockname()[1]
336
+
337
+ import aiohttp
338
+ async with aiohttp.ClientSession() as session:
339
+ req = make_jsonrpc_request("unknown/method", {}, id="r1")
340
+ async with session.post(f"http://127.0.0.1:{port}/", json=req) as resp:
341
+ data = await resp.json()
342
+ assert "error" in data
343
+ assert data["error"]["code"] == -32601
344
+ finally:
345
+ await transport.unregister()
346
+
347
+ @pytest.mark.asyncio
348
+ async def test_parse_error(self):
349
+ config = A2AConfig(server_port=0, server_host="127.0.0.1")
350
+ transport = A2ATransport(config)
351
+
352
+ try:
353
+ await transport.register("parse-err-agent")
354
+ sockets = transport._site._server.sockets
355
+ port = sockets[0].getsockname()[1]
356
+
357
+ import aiohttp
358
+ async with aiohttp.ClientSession() as session:
359
+ async with session.post(
360
+ f"http://127.0.0.1:{port}/",
361
+ data="not valid json",
362
+ headers={"Content-Type": "application/json"},
363
+ ) as resp:
364
+ data = await resp.json()
365
+ assert "error" in data
366
+ assert data["error"]["code"] == -32700
367
+ finally:
368
+ await transport.unregister()
369
+
370
+
371
+ # === A2ATransport client-side tests ===
372
+
373
+
374
+ class TestA2ATransportClient:
375
+ @pytest.mark.asyncio
376
+ async def test_discover_agent(self, remote_agent):
377
+ config = A2AConfig()
378
+ transport = A2ATransport(config)
379
+ try:
380
+ card = await transport._discover_agent(remote_agent)
381
+ assert card.name == "remote-agent"
382
+ assert len(card.skills) == 1
383
+ assert card.skills[0].name == "Echo"
384
+ finally:
385
+ await transport._close_session()
386
+
387
+ @pytest.mark.asyncio
388
+ async def test_discover_agent_caches(self, remote_agent):
389
+ config = A2AConfig()
390
+ transport = A2ATransport(config)
391
+ try:
392
+ card1 = await transport._discover_agent(remote_agent)
393
+ card2 = await transport._discover_agent(remote_agent)
394
+ assert card1 is card2 # Same object from cache
395
+ finally:
396
+ await transport._close_session()
397
+
398
+ @pytest.mark.asyncio
399
+ async def test_send_dm(self, remote_agent):
400
+ config = A2AConfig()
401
+ transport = A2ATransport(config)
402
+ try:
403
+ result = await transport.send_dm(remote_agent, "test message")
404
+ assert result["text"] == "Echo: test message"
405
+ assert result["task_id"] == "task-001"
406
+ assert result["status"] == "completed"
407
+ finally:
408
+ await transport._close_session()
409
+
410
+ @pytest.mark.asyncio
411
+ async def test_list_agents(self, remote_agent):
412
+ config = A2AConfig(registry=[remote_agent])
413
+ transport = A2ATransport(config)
414
+ try:
415
+ agents = await transport.list_agents()
416
+ assert len(agents) == 1
417
+ assert agents[0]["name"] == "remote-agent"
418
+ assert agents[0]["description"] == "Test agent: remote-agent"
419
+ finally:
420
+ await transport._close_session()
421
+
422
+ @pytest.mark.asyncio
423
+ async def test_list_agents_skips_unreachable(self):
424
+ config = A2AConfig(registry=["http://127.0.0.1:1"])
425
+ transport = A2ATransport(config)
426
+ try:
427
+ agents = await transport.list_agents()
428
+ assert agents == []
429
+ finally:
430
+ await transport._close_session()
431
+
432
+
433
+ # === Message conversion tests ===
434
+
435
+
436
+ class TestMessageConversion:
437
+ def test_relay_msg_to_a2a(self):
438
+ a2a_msg = A2ATransport._relay_msg_to_a2a("hello world", "sender-1")
439
+ assert a2a_msg.role == "user"
440
+ assert len(a2a_msg.parts) == 1
441
+ assert a2a_msg.parts[0].text == "hello world"
442
+
443
+ def test_a2a_to_relay_msg(self):
444
+ a2a_msg = A2AMessage(
445
+ role="agent",
446
+ parts=[A2APart(text="response text")],
447
+ messageId="msg-1",
448
+ contextId="ctx-1",
449
+ )
450
+ relay_msg = A2ATransport._a2a_to_relay_msg(a2a_msg, sender="remote-agent")
451
+ assert relay_msg.sender == "remote-agent"
452
+ assert relay_msg.text == "response text"
453
+ assert relay_msg.thread_id == "ctx-1"
454
+ assert relay_msg.message_id == "msg-1"
455
+
456
+ def test_a2a_to_relay_msg_multi_part(self):
457
+ a2a_msg = A2AMessage(
458
+ role="agent",
459
+ parts=[A2APart(text="hello"), A2APart(text="world")],
460
+ messageId="msg-2",
461
+ )
462
+ relay_msg = A2ATransport._a2a_to_relay_msg(a2a_msg, sender="agent")
463
+ assert relay_msg.text == "hello world"
464
+
465
+ def test_roundtrip_conversion(self):
466
+ original_text = "roundtrip test message"
467
+ a2a_msg = A2ATransport._relay_msg_to_a2a(original_text, "sender")
468
+ relay_msg = A2ATransport._a2a_to_relay_msg(a2a_msg, sender="sender")
469
+ assert relay_msg.text == original_text
470
+
471
+ def test_a2a_result_to_relay(self):
472
+ result = {
473
+ "id": "task-1",
474
+ "status": {"state": "completed"},
475
+ "messages": [
476
+ {"role": "user", "parts": [{"text": "input"}]},
477
+ {"role": "agent", "parts": [{"text": "output"}]},
478
+ ],
479
+ }
480
+ relay_result = A2ATransport._a2a_result_to_relay(result, "remote")
481
+ assert relay_result["sender"] == "remote"
482
+ assert relay_result["text"] == "output"
483
+ assert relay_result["task_id"] == "task-1"
484
+ assert relay_result["status"] == "completed"
485
+
486
+ def test_a2a_result_to_relay_empty_messages(self):
487
+ result = {"id": "task-1", "status": {"state": "completed"}, "messages": []}
488
+ relay_result = A2ATransport._a2a_result_to_relay(result, "remote")
489
+ assert relay_result["text"] == ""
490
+
491
+
492
+ # === connect_ws is a no-op ===
493
+
494
+
495
+ class TestConnectWs:
496
+ @pytest.mark.asyncio
497
+ async def test_connect_ws_is_noop(self):
498
+ config = A2AConfig()
499
+ transport = A2ATransport(config)
500
+ await transport.connect_ws() # Should not raise
501
+
502
+
503
+ # === on_message ===
504
+
505
+
506
+ class TestOnMessage:
507
+ def test_registers_callback(self):
508
+ config = A2AConfig()
509
+ transport = A2ATransport(config)
510
+ cb = lambda msg: None
511
+ transport.on_message(cb)
512
+ assert cb in transport._message_callbacks
513
+
514
+ def test_multiple_callbacks(self):
515
+ config = A2AConfig()
516
+ transport = A2ATransport(config)
517
+ cb1 = lambda msg: None
518
+ cb2 = lambda msg: None
519
+ transport.on_message(cb1)
520
+ transport.on_message(cb2)
521
+ assert len(transport._message_callbacks) == 2
522
+
523
+
524
+ # === Auth headers ===
525
+
526
+
527
+ class TestAuthHeaders:
528
+ def test_no_auth(self):
529
+ config = A2AConfig()
530
+ transport = A2ATransport(config)
531
+ headers = transport._auth_headers()
532
+ assert headers == {}
533
+
534
+ def test_bearer_auth(self):
535
+ config = A2AConfig(auth_scheme="bearer", auth_token="my-token")
536
+ transport = A2ATransport(config)
537
+ headers = transport._auth_headers()
538
+ assert headers["Authorization"] == "Bearer my-token"
539
+
540
+ def test_api_key_auth(self):
541
+ config = A2AConfig(auth_scheme="api_key", auth_token="key-123")
542
+ transport = A2ATransport(config)
543
+ headers = transport._auth_headers()
544
+ assert headers["X-API-Key"] == "key-123"
545
+
546
+ def test_default_auth_scheme(self):
547
+ config = A2AConfig(auth_token="fallback-token")
548
+ transport = A2ATransport(config)
549
+ headers = transport._auth_headers()
550
+ assert headers["Authorization"] == "Bearer fallback-token"
551
+
552
+
553
+ # === A2AError ===
554
+
555
+
556
+ class TestA2AError:
557
+ def test_error_attributes(self):
558
+ err = A2AError(-32001, "Task not found")
559
+ assert err.code == -32001
560
+ assert err.message == "Task not found"
561
+ assert "A2A error -32001" in str(err)