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,346 @@
1
+ """Tests for A2AServer — JSON-RPC dispatch, Agent Card serving, task lifecycle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import uuid
7
+
8
+ import aiohttp
9
+ import pytest
10
+ import pytest_asyncio
11
+
12
+ from agent_relay.communicate.a2a_server import A2AServer
13
+ from agent_relay.communicate.a2a_types import (
14
+ A2AMessage,
15
+ A2APart,
16
+ A2ASkill,
17
+ A2ATask,
18
+ A2ATaskStatus,
19
+ )
20
+
21
+
22
+ @pytest_asyncio.fixture
23
+ async def a2a_server():
24
+ """Start an A2AServer on a random port for testing."""
25
+ server = A2AServer(agent_name="test-agent", port=0, host="127.0.0.1")
26
+ await server.start()
27
+ try:
28
+ yield server
29
+ finally:
30
+ await server.stop()
31
+
32
+
33
+ @pytest.fixture
34
+ def server_url(a2a_server: A2AServer) -> str:
35
+ return a2a_server.url
36
+
37
+
38
+ # --- Agent Card tests ---
39
+
40
+
41
+ class TestAgentCard:
42
+ async def test_agent_card_served_at_well_known(self, server_url: str):
43
+ async with aiohttp.ClientSession() as session:
44
+ async with session.get(f"{server_url}/.well-known/agent.json") as resp:
45
+ assert resp.status == 200
46
+ data = await resp.json()
47
+ assert data["name"] == "test-agent"
48
+ assert "Agent Relay agent" in data["description"]
49
+ assert data["version"] == "1.0.0"
50
+ assert data["capabilities"]["streaming"] is True
51
+ assert data["defaultInputModes"] == ["text"]
52
+ assert data["defaultOutputModes"] == ["text"]
53
+
54
+ async def test_agent_card_includes_skills(self):
55
+ skills = [A2ASkill(id="s1", name="Skill One", description="Does thing one")]
56
+ server = A2AServer(agent_name="skilled-agent", port=0, host="127.0.0.1", skills=skills)
57
+ await server.start()
58
+ try:
59
+ async with aiohttp.ClientSession() as session:
60
+ async with session.get(f"{server.url}/.well-known/agent.json") as resp:
61
+ data = await resp.json()
62
+ assert len(data["skills"]) == 1
63
+ assert data["skills"][0]["id"] == "s1"
64
+ assert data["skills"][0]["name"] == "Skill One"
65
+ finally:
66
+ await server.stop()
67
+
68
+ async def test_get_agent_card_method(self, a2a_server: A2AServer):
69
+ card = a2a_server.get_agent_card()
70
+ assert card.name == "test-agent"
71
+ assert card.url == a2a_server.url
72
+
73
+
74
+ # --- JSON-RPC dispatch tests ---
75
+
76
+
77
+ class TestJsonRpcDispatch:
78
+ async def test_message_send_creates_task(self, server_url: str, a2a_server: A2AServer):
79
+ payload = {
80
+ "jsonrpc": "2.0",
81
+ "method": "message/send",
82
+ "params": {
83
+ "message": {
84
+ "role": "user",
85
+ "parts": [{"text": "Hello, agent!"}],
86
+ }
87
+ },
88
+ "id": "req-1",
89
+ }
90
+ async with aiohttp.ClientSession() as session:
91
+ async with session.post(server_url, json=payload) as resp:
92
+ assert resp.status == 200
93
+ data = await resp.json()
94
+ assert data["jsonrpc"] == "2.0"
95
+ assert data["id"] == "req-1"
96
+ result = data["result"]
97
+ assert "id" in result
98
+ assert result["status"]["state"] == "completed"
99
+ assert len(result["messages"]) >= 1
100
+ # Task should be stored
101
+ assert result["id"] in a2a_server.tasks
102
+
103
+ async def test_message_send_with_callback(self, server_url: str, a2a_server: A2AServer):
104
+ async def echo_handler(msg: A2AMessage) -> A2AMessage:
105
+ text = msg.parts[0].text if msg.parts else ""
106
+ return A2AMessage(
107
+ role="agent",
108
+ parts=[A2APart(text=f"Echo: {text}")],
109
+ messageId=str(uuid.uuid4()),
110
+ )
111
+
112
+ a2a_server.on_message(echo_handler)
113
+
114
+ payload = {
115
+ "jsonrpc": "2.0",
116
+ "method": "message/send",
117
+ "params": {
118
+ "message": {
119
+ "role": "user",
120
+ "parts": [{"text": "Hello!"}],
121
+ }
122
+ },
123
+ "id": "req-2",
124
+ }
125
+ async with aiohttp.ClientSession() as session:
126
+ async with session.post(server_url, json=payload) as resp:
127
+ data = await resp.json()
128
+ result = data["result"]
129
+ assert result["status"]["state"] == "completed"
130
+ # Should have both the user message and the agent response
131
+ assert len(result["messages"]) == 2
132
+ agent_msg = result["messages"][1]
133
+ assert agent_msg["role"] == "agent"
134
+ assert "Echo: Hello!" in agent_msg["parts"][0]["text"]
135
+
136
+ async def test_message_send_sync_callback(self, server_url: str, a2a_server: A2AServer):
137
+ """Test that synchronous (non-async) callbacks work."""
138
+
139
+ def sync_handler(msg: A2AMessage) -> A2AMessage:
140
+ return A2AMessage(
141
+ role="agent",
142
+ parts=[A2APart(text="sync response")],
143
+ )
144
+
145
+ a2a_server.on_message(sync_handler)
146
+
147
+ payload = {
148
+ "jsonrpc": "2.0",
149
+ "method": "message/send",
150
+ "params": {"message": {"role": "user", "parts": [{"text": "test"}]}},
151
+ "id": "req-sync",
152
+ }
153
+ async with aiohttp.ClientSession() as session:
154
+ async with session.post(server_url, json=payload) as resp:
155
+ data = await resp.json()
156
+ assert data["result"]["status"]["state"] == "completed"
157
+ assert data["result"]["messages"][1]["parts"][0]["text"] == "sync response"
158
+
159
+ async def test_tasks_get(self, server_url: str, a2a_server: A2AServer):
160
+ # First create a task via message/send
161
+ send_payload = {
162
+ "jsonrpc": "2.0",
163
+ "method": "message/send",
164
+ "params": {"message": {"role": "user", "parts": [{"text": "create task"}]}},
165
+ "id": "req-create",
166
+ }
167
+ async with aiohttp.ClientSession() as session:
168
+ async with session.post(server_url, json=send_payload) as resp:
169
+ created = (await resp.json())["result"]
170
+ task_id = created["id"]
171
+
172
+ # Now get the task
173
+ get_payload = {
174
+ "jsonrpc": "2.0",
175
+ "method": "tasks/get",
176
+ "params": {"id": task_id},
177
+ "id": "req-get",
178
+ }
179
+ async with session.post(server_url, json=get_payload) as resp:
180
+ data = await resp.json()
181
+ assert data["id"] == "req-get"
182
+ assert data["result"]["id"] == task_id
183
+ assert data["result"]["status"]["state"] == "completed"
184
+
185
+ async def test_tasks_get_not_found(self, server_url: str):
186
+ payload = {
187
+ "jsonrpc": "2.0",
188
+ "method": "tasks/get",
189
+ "params": {"id": "nonexistent-task"},
190
+ "id": "req-404",
191
+ }
192
+ async with aiohttp.ClientSession() as session:
193
+ async with session.post(server_url, json=payload) as resp:
194
+ assert resp.status == 404
195
+ data = await resp.json()
196
+ assert data["error"]["code"] == -32602
197
+
198
+ async def test_tasks_cancel(self, server_url: str, a2a_server: A2AServer):
199
+ # Create a task
200
+ send_payload = {
201
+ "jsonrpc": "2.0",
202
+ "method": "message/send",
203
+ "params": {"message": {"role": "user", "parts": [{"text": "cancel me"}]}},
204
+ "id": "req-c1",
205
+ }
206
+ async with aiohttp.ClientSession() as session:
207
+ async with session.post(server_url, json=send_payload) as resp:
208
+ task_id = (await resp.json())["result"]["id"]
209
+
210
+ # Cancel it
211
+ cancel_payload = {
212
+ "jsonrpc": "2.0",
213
+ "method": "tasks/cancel",
214
+ "params": {"id": task_id},
215
+ "id": "req-c2",
216
+ }
217
+ async with session.post(server_url, json=cancel_payload) as resp:
218
+ data = await resp.json()
219
+ assert data["result"]["status"]["state"] == "canceled"
220
+
221
+ # Verify stored task state
222
+ assert a2a_server.tasks[task_id].status.state == "canceled"
223
+
224
+ async def test_unknown_method(self, server_url: str):
225
+ payload = {
226
+ "jsonrpc": "2.0",
227
+ "method": "unknown/method",
228
+ "params": {},
229
+ "id": "req-unknown",
230
+ }
231
+ async with aiohttp.ClientSession() as session:
232
+ async with session.post(server_url, json=payload) as resp:
233
+ assert resp.status == 400
234
+ data = await resp.json()
235
+ assert data["error"]["code"] == -32601
236
+
237
+ async def test_invalid_json(self, server_url: str):
238
+ async with aiohttp.ClientSession() as session:
239
+ async with session.post(
240
+ server_url,
241
+ data=b"not json",
242
+ headers={"Content-Type": "application/json"},
243
+ ) as resp:
244
+ assert resp.status == 400
245
+ data = await resp.json()
246
+ assert data["error"]["code"] == -32700
247
+
248
+
249
+ # --- Task lifecycle tests ---
250
+
251
+
252
+ class TestTaskLifecycle:
253
+ async def test_task_submitted_to_completed(self, server_url: str, a2a_server: A2AServer):
254
+ """Task goes through working -> completed when callback responds."""
255
+
256
+ async def delayed_handler(msg: A2AMessage) -> A2AMessage:
257
+ return A2AMessage(role="agent", parts=[A2APart(text="done")])
258
+
259
+ a2a_server.on_message(delayed_handler)
260
+
261
+ payload = {
262
+ "jsonrpc": "2.0",
263
+ "method": "message/send",
264
+ "params": {"message": {"role": "user", "parts": [{"text": "work"}]}},
265
+ "id": "req-lc",
266
+ }
267
+ async with aiohttp.ClientSession() as session:
268
+ async with session.post(server_url, json=payload) as resp:
269
+ data = await resp.json()
270
+ assert data["result"]["status"]["state"] == "completed"
271
+
272
+ async def test_task_completed_without_callback(self, server_url: str, a2a_server: A2AServer):
273
+ """Task completes even with no callback registered."""
274
+ payload = {
275
+ "jsonrpc": "2.0",
276
+ "method": "message/send",
277
+ "params": {"message": {"role": "user", "parts": [{"text": "no handler"}]}},
278
+ "id": "req-no-cb",
279
+ }
280
+ async with aiohttp.ClientSession() as session:
281
+ async with session.post(server_url, json=payload) as resp:
282
+ data = await resp.json()
283
+ assert data["result"]["status"]["state"] == "completed"
284
+ assert len(data["result"]["messages"]) == 1
285
+
286
+ async def test_existing_task_updated_with_new_message(self, a2a_server: A2AServer):
287
+ """Sending a message with an existing taskId appends to that task."""
288
+ task_id = str(uuid.uuid4())
289
+
290
+ # First message
291
+ result1 = await a2a_server.handle_message_send({
292
+ "message": {
293
+ "role": "user",
294
+ "parts": [{"text": "first"}],
295
+ "taskId": task_id,
296
+ }
297
+ })
298
+ assert result1["id"] == task_id
299
+ assert len(result1["messages"]) == 1
300
+
301
+ # Second message to same task
302
+ result2 = await a2a_server.handle_message_send({
303
+ "message": {
304
+ "role": "user",
305
+ "parts": [{"text": "second"}],
306
+ "taskId": task_id,
307
+ }
308
+ })
309
+ assert result2["id"] == task_id
310
+ assert len(result2["messages"]) == 2
311
+
312
+ async def test_message_preserves_context_id(self, a2a_server: A2AServer):
313
+ ctx_id = "ctx-123"
314
+ result = await a2a_server.handle_message_send({
315
+ "message": {
316
+ "role": "user",
317
+ "parts": [{"text": "with context"}],
318
+ "contextId": ctx_id,
319
+ }
320
+ })
321
+ assert result["contextId"] == ctx_id
322
+
323
+
324
+ # --- Server start/stop ---
325
+
326
+
327
+ class TestServerLifecycle:
328
+ async def test_start_and_stop(self):
329
+ server = A2AServer(agent_name="lifecycle-test", port=0, host="127.0.0.1")
330
+ await server.start()
331
+ assert server._actual_port is not None
332
+ assert server._actual_port > 0
333
+
334
+ # Verify server is running
335
+ async with aiohttp.ClientSession() as session:
336
+ async with session.get(f"{server.url}/.well-known/agent.json") as resp:
337
+ assert resp.status == 200
338
+
339
+ await server.stop()
340
+ assert server._runner is None
341
+
342
+ async def test_double_stop_is_safe(self):
343
+ server = A2AServer(agent_name="double-stop", port=0, host="127.0.0.1")
344
+ await server.start()
345
+ await server.stop()
346
+ await server.stop() # Should not raise