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,154 @@
1
+ """E2E test: Agno Python adapter against live Relaycast."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import sys
8
+ import time
9
+ import uuid
10
+ from types import ModuleType
11
+ from unittest.mock import MagicMock
12
+
13
+ import pytest
14
+
15
+ from agent_relay.communicate.core import Relay
16
+ from agent_relay.communicate.types import RelayConfig
17
+
18
+
19
+ def _live_config() -> RelayConfig:
20
+ return RelayConfig.resolve(
21
+ workspace=os.environ.get("RELAY_WORKSPACE"),
22
+ api_key=os.environ.get("RELAY_API_KEY"),
23
+ base_url=os.environ.get("RELAY_BASE_URL"),
24
+ channels=["general"],
25
+ auto_cleanup=False,
26
+ )
27
+
28
+
29
+ def _unique_name(prefix: str = "e2e-agno") -> str:
30
+ return f"{prefix}-{int(time.time() * 1000)}-{uuid.uuid4().hex[:6]}"
31
+
32
+
33
+ def _install_agno_module(monkeypatch):
34
+ """Inject a fake 'agno' module tree so the adapter resolves correctly."""
35
+ agno_mod = ModuleType("agno")
36
+ agno_agent_mod = ModuleType("agno.agent")
37
+ agno_agent_mod.Agent = type("Agent", (), {})
38
+ agno_mod.agent = agno_agent_mod
39
+ monkeypatch.setitem(sys.modules, "agno", agno_mod)
40
+ monkeypatch.setitem(sys.modules, "agno.agent", agno_agent_mod)
41
+ return agno_agent_mod
42
+
43
+
44
+ def _make_mock_agent(name: str, AgentCls):
45
+ agent = MagicMock(spec=AgentCls)
46
+ agent.name = name
47
+ agent.tools = []
48
+ agent.instructions = "You are a helpful Agno agent."
49
+ type(agent).__module__ = "agno.agent"
50
+ return agent
51
+
52
+
53
+ @pytest.mark.asyncio
54
+ async def test_agno_e2e(monkeypatch):
55
+ """Full round-trip: real Relay, mock Agno Agent, live Relaycast API."""
56
+ agno_agent_mod = _install_agno_module(monkeypatch)
57
+ from agent_relay.communicate.adapters.agno import on_relay
58
+
59
+ sender_name = _unique_name("e2e-agno-s")
60
+ receiver_name = _unique_name("e2e-agno-r")
61
+ config = _live_config()
62
+
63
+ sender_relay = Relay(sender_name, config)
64
+ receiver_relay = Relay(receiver_name, config)
65
+
66
+ sender_agent = _make_mock_agent(sender_name, agno_agent_mod.Agent)
67
+ receiver_agent = _make_mock_agent(receiver_name, agno_agent_mod.Agent)
68
+
69
+ try:
70
+ # Step 1: on_relay injects 4 tools
71
+ wrapped_s = on_relay(sender_agent, sender_relay)
72
+ wrapped_r = on_relay(receiver_agent, receiver_relay)
73
+ assert wrapped_s is sender_agent
74
+ assert wrapped_r is receiver_agent
75
+ assert len(sender_agent.tools) == 4
76
+ tool_names = {fn.__name__ for fn in sender_agent.tools}
77
+ assert tool_names == {"relay_send", "relay_inbox", "relay_post", "relay_agents"}
78
+
79
+ # Step 2: relay_agents tool lists the registered agent
80
+ agents_fn = next(f for f in sender_agent.tools if f.__name__ == "relay_agents")
81
+ result = await agents_fn()
82
+ assert isinstance(result, str)
83
+ assert sender_name in result
84
+
85
+ # Step 3: relay_post tool posts to a channel
86
+ post_fn = next(f for f in sender_agent.tools if f.__name__ == "relay_post")
87
+ post_result = await post_fn("general", f"agno-e2e-{uuid.uuid4().hex[:8]}")
88
+ assert post_result == "Message posted"
89
+
90
+ # Step 4: relay_send + relay_inbox across two agents
91
+ send_fn = next(f for f in sender_agent.tools if f.__name__ == "relay_send")
92
+ inbox_fn = next(f for f in receiver_agent.tools if f.__name__ == "relay_inbox")
93
+
94
+ # Ensure receiver is connected first
95
+ await receiver_relay.agents()
96
+
97
+ dm_text = f"agno-dm-{uuid.uuid4().hex[:8]}"
98
+ await send_fn(receiver_name, dm_text)
99
+
100
+ deadline = asyncio.get_event_loop().time() + 15.0
101
+ found = False
102
+ while asyncio.get_event_loop().time() < deadline:
103
+ result = await inbox_fn()
104
+ if sender_name in result and dm_text in result:
105
+ found = True
106
+ break
107
+ await asyncio.sleep(0.5)
108
+ assert found, f"DM from {sender_name} not received within timeout"
109
+
110
+ # Step 5: instructions wrapper returns base instructions when no pending msgs
111
+ assert callable(receiver_agent.instructions)
112
+ instr = await receiver_agent.instructions()
113
+ assert "You are a helpful Agno agent." in instr
114
+
115
+ # Step 6: instructions wrapper prepends messages when present
116
+ instr_text = f"instr-msg-{uuid.uuid4().hex[:8]}"
117
+ await sender_relay.send(receiver_name, instr_text)
118
+
119
+ deadline = asyncio.get_event_loop().time() + 15.0
120
+ found_instr = False
121
+ while asyncio.get_event_loop().time() < deadline:
122
+ msgs = await receiver_relay.peek()
123
+ if any(m.text == instr_text for m in msgs):
124
+ found_instr = True
125
+ break
126
+ await asyncio.sleep(0.5)
127
+ assert found_instr, "Instruction message did not arrive in receiver buffer"
128
+
129
+ instr_with_msgs = await receiver_agent.instructions()
130
+ assert sender_name in instr_with_msgs
131
+ assert instr_text in instr_with_msgs
132
+ assert "You are a helpful Agno agent." in instr_with_msgs
133
+
134
+ finally:
135
+ await asyncio.gather(sender_relay.close(), receiver_relay.close())
136
+
137
+
138
+ @pytest.mark.asyncio
139
+ async def test_agno_cleanup(monkeypatch):
140
+ """relay.close() disconnects the transport cleanly."""
141
+ agno_agent_mod = _install_agno_module(monkeypatch)
142
+ from agent_relay.communicate.adapters.agno import on_relay
143
+
144
+ agent_name = _unique_name("e2e-agno-cleanup")
145
+ config = _live_config()
146
+ relay = Relay(agent_name, config)
147
+ mock_agent = _make_mock_agent(agent_name, agno_agent_mod.Agent)
148
+
149
+ on_relay(mock_agent, relay)
150
+ agents = await relay.agents()
151
+ assert agent_name in agents
152
+
153
+ await relay.close()
154
+ assert not relay._connected
@@ -0,0 +1,428 @@
1
+ """E2E test: Claude Agent SDK Python adapter against live Relaycast."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import sys
8
+ import time
9
+ import uuid
10
+ from types import ModuleType, SimpleNamespace
11
+ from unittest.mock import MagicMock
12
+
13
+ import pytest
14
+
15
+ from agent_relay.communicate.core import Relay
16
+ from agent_relay.communicate.types import Message, RelayConfig
17
+
18
+
19
+ def _live_config() -> RelayConfig:
20
+ return RelayConfig.resolve(
21
+ workspace=os.environ.get("RELAY_WORKSPACE"),
22
+ api_key=os.environ.get("RELAY_API_KEY"),
23
+ base_url=os.environ.get("RELAY_BASE_URL"),
24
+ channels=[],
25
+ auto_cleanup=False,
26
+ )
27
+
28
+
29
+ def _unique_name(prefix: str = "e2e-claude-py") -> str:
30
+ return f"{prefix}-{int(time.time() * 1000)}-{uuid.uuid4().hex[:8]}"
31
+
32
+
33
+ def _install_claude_sdk_module(monkeypatch):
34
+ """Inject a fake 'claude_agent_sdk' module so the adapter can import HookResult."""
35
+ types_mod = ModuleType("claude_agent_sdk.types")
36
+ types_mod.HookResult = type(
37
+ "HookResult",
38
+ (),
39
+ {
40
+ "__init__": lambda self, system_message=None, should_continue=False: (
41
+ setattr(self, "system_message", system_message)
42
+ or setattr(self, "should_continue", should_continue)
43
+ ),
44
+ },
45
+ )
46
+ sdk_mod = ModuleType("claude_agent_sdk")
47
+ sdk_mod.types = types_mod
48
+ monkeypatch.setitem(sys.modules, "claude_agent_sdk", sdk_mod)
49
+ monkeypatch.setitem(sys.modules, "claude_agent_sdk.types", types_mod)
50
+ return types_mod
51
+
52
+
53
+ def _make_mock_options(name: str = "TestAgent") -> SimpleNamespace:
54
+ """Create a mock Claude SDK options object."""
55
+ return SimpleNamespace(name=name, hooks=None, mcp_servers=[])
56
+
57
+
58
+ class TestClaudeSdkAdapterE2E:
59
+ """E2E tests for the Claude SDK adapter with a real Relay connection."""
60
+
61
+ @pytest.mark.asyncio
62
+ async def test_on_relay_injects_mcp_and_hooks(self, monkeypatch):
63
+ """on_relay() injects the relaycast MCP config and wraps hooks."""
64
+ types_mod = _install_claude_sdk_module(monkeypatch)
65
+
66
+ from agent_relay.communicate.adapters.claude_sdk import on_relay
67
+
68
+ config = _live_config()
69
+ agent_name = _unique_name()
70
+ relay = Relay(agent_name, config)
71
+ options = _make_mock_options(agent_name)
72
+
73
+ try:
74
+ result = on_relay(agent_name, options, relay)
75
+
76
+ assert result is options
77
+ mcp_names = [s["name"] for s in options.mcp_servers]
78
+ assert "relaycast" in mcp_names
79
+ assert callable(options.hooks.post_tool_use)
80
+ assert callable(options.hooks.stop)
81
+ finally:
82
+ await relay.close()
83
+
84
+ @pytest.mark.asyncio
85
+ async def test_on_relay_preserves_existing_mcp_servers(self, monkeypatch):
86
+ """on_relay() appends relaycast MCP without clobbering existing servers."""
87
+ _install_claude_sdk_module(monkeypatch)
88
+
89
+ from agent_relay.communicate.adapters.claude_sdk import on_relay
90
+
91
+ config = _live_config()
92
+ agent_name = _unique_name()
93
+ relay = Relay(agent_name, config)
94
+ existing_mcp = {"name": "custom-tool", "command": "custom", "args": []}
95
+ options = SimpleNamespace(
96
+ name=agent_name,
97
+ hooks=None,
98
+ mcp_servers=[existing_mcp],
99
+ )
100
+
101
+ try:
102
+ on_relay(agent_name, options, relay)
103
+
104
+ assert len(options.mcp_servers) == 2
105
+ names = [s["name"] for s in options.mcp_servers]
106
+ assert "custom-tool" in names
107
+ assert "relaycast" in names
108
+ finally:
109
+ await relay.close()
110
+
111
+
112
+ class TestRelayRegistrationE2E:
113
+ """Verify Relay registration against live Relaycast via Claude adapter path."""
114
+
115
+ @pytest.mark.asyncio
116
+ async def test_relay_registers_agent(self):
117
+ """Relay.agents() includes the registered agent after on_relay wrapping."""
118
+ config = _live_config()
119
+ agent_name = _unique_name()
120
+ relay = Relay(agent_name, config)
121
+
122
+ try:
123
+ agents = await relay.agents()
124
+ assert isinstance(agents, list)
125
+ assert agent_name in agents
126
+ finally:
127
+ await relay.close()
128
+
129
+ @pytest.mark.asyncio
130
+ async def test_two_agents_register_simultaneously(self):
131
+ """Two agents with unique names can coexist."""
132
+ config = _live_config()
133
+ name_a = _unique_name("e2e-claude-a")
134
+ name_b = _unique_name("e2e-claude-b")
135
+ relay_a = Relay(name_a, config)
136
+ relay_b = Relay(name_b, config)
137
+
138
+ try:
139
+ agents_a = await relay_a.agents()
140
+ agents_b = await relay_b.agents()
141
+ assert name_a in agents_a
142
+ assert name_b in agents_b
143
+ finally:
144
+ await asyncio.gather(relay_a.close(), relay_b.close())
145
+
146
+
147
+ class TestRelayToolFunctionsE2E:
148
+ """Test relay operations (send, inbox, agents, post) against the real API."""
149
+
150
+ @pytest.mark.asyncio
151
+ async def test_send_and_inbox(self):
152
+ """relay.send() delivers a DM that relay.inbox() can retrieve."""
153
+ config = _live_config()
154
+ sender_name = _unique_name("e2e-claude-sender")
155
+ receiver_name = _unique_name("e2e-claude-recv")
156
+ sender = Relay(sender_name, config)
157
+ receiver = Relay(receiver_name, config)
158
+ text = f"claude-e2e-{uuid.uuid4().hex[:8]}"
159
+
160
+ try:
161
+ await sender.agents()
162
+ await receiver.agents()
163
+
164
+ await sender.send(receiver_name, text)
165
+
166
+ deadline = asyncio.get_event_loop().time() + 15.0
167
+ found = False
168
+ while asyncio.get_event_loop().time() < deadline:
169
+ messages = await receiver.inbox()
170
+ for msg in messages:
171
+ if msg.sender == sender_name and msg.text == text:
172
+ found = True
173
+ break
174
+ if found:
175
+ break
176
+ await asyncio.sleep(0.5)
177
+
178
+ assert found, f"DM from {sender_name} not received within timeout"
179
+ finally:
180
+ await asyncio.gather(sender.close(), receiver.close())
181
+
182
+ @pytest.mark.asyncio
183
+ async def test_post_to_channel(self):
184
+ """relay.post() succeeds when posting to a channel."""
185
+ config = _live_config()
186
+ agent_name = _unique_name("e2e-claude-post")
187
+ relay = Relay(agent_name, config)
188
+
189
+ try:
190
+ await relay.agents()
191
+ await relay.join("general")
192
+ await relay.post("general", f"claude-e2e-test-{uuid.uuid4().hex[:8]}")
193
+ finally:
194
+ await relay.close()
195
+
196
+ @pytest.mark.asyncio
197
+ async def test_agents_list(self):
198
+ """relay.agents() returns a list containing the registered agent."""
199
+ config = _live_config()
200
+ agent_name = _unique_name("e2e-claude-list")
201
+ relay = Relay(agent_name, config)
202
+
203
+ try:
204
+ agents = await relay.agents()
205
+ assert isinstance(agents, list)
206
+ assert agent_name in agents
207
+ finally:
208
+ await relay.close()
209
+
210
+
211
+ class TestHooksE2E:
212
+ """Test Claude SDK hook wrappers with a real Relay connection."""
213
+
214
+ @pytest.mark.asyncio
215
+ async def test_post_tool_use_hook_returns_none_when_no_messages(self, monkeypatch):
216
+ """post_tool_use hook returns None when inbox is empty."""
217
+ _install_claude_sdk_module(monkeypatch)
218
+
219
+ from agent_relay.communicate.adapters.claude_sdk import on_relay
220
+
221
+ config = _live_config()
222
+ agent_name = _unique_name("e2e-claude-hook")
223
+ relay = Relay(agent_name, config)
224
+ options = _make_mock_options(agent_name)
225
+
226
+ try:
227
+ on_relay(agent_name, options, relay)
228
+ await relay.agents()
229
+
230
+ # Drain any pre-existing messages
231
+ await relay.inbox()
232
+
233
+ result = await options.hooks.post_tool_use()
234
+ assert result is None
235
+ finally:
236
+ await relay.close()
237
+
238
+ @pytest.mark.asyncio
239
+ async def test_stop_hook_returns_none_when_no_messages(self, monkeypatch):
240
+ """stop hook returns None when inbox is empty."""
241
+ _install_claude_sdk_module(monkeypatch)
242
+
243
+ from agent_relay.communicate.adapters.claude_sdk import on_relay
244
+
245
+ config = _live_config()
246
+ agent_name = _unique_name("e2e-claude-stop")
247
+ relay = Relay(agent_name, config)
248
+ options = _make_mock_options(agent_name)
249
+
250
+ try:
251
+ on_relay(agent_name, options, relay)
252
+ await relay.agents()
253
+ await relay.inbox()
254
+
255
+ result = await options.hooks.stop()
256
+ assert result is None
257
+ finally:
258
+ await relay.close()
259
+
260
+ @pytest.mark.asyncio
261
+ async def test_stop_hook_returns_messages_with_should_continue(self, monkeypatch):
262
+ """stop hook returns HookResult with should_continue=True when inbox has messages."""
263
+ types_mod = _install_claude_sdk_module(monkeypatch)
264
+
265
+ from agent_relay.communicate.adapters.claude_sdk import on_relay
266
+
267
+ config = _live_config()
268
+ sender_name = _unique_name("e2e-claude-hook-s")
269
+ receiver_name = _unique_name("e2e-claude-hook-r")
270
+ sender = Relay(sender_name, config)
271
+ receiver = Relay(receiver_name, config)
272
+ options = _make_mock_options(receiver_name)
273
+ text = f"hook-test-{uuid.uuid4().hex[:8]}"
274
+
275
+ try:
276
+ on_relay(options, receiver, name=receiver_name)
277
+ await sender.agents()
278
+ await receiver.agents()
279
+ # Drain any stale messages
280
+ await receiver.inbox()
281
+
282
+ await sender.send(receiver_name, text)
283
+
284
+ # Wait for message to arrive
285
+ deadline = asyncio.get_event_loop().time() + 15.0
286
+ found = False
287
+ while asyncio.get_event_loop().time() < deadline:
288
+ msgs = await receiver.peek()
289
+ for m in msgs:
290
+ if m.sender == sender_name and m.text == text:
291
+ found = True
292
+ break
293
+ if found:
294
+ break
295
+ await asyncio.sleep(0.5)
296
+
297
+ assert found, f"DM from {sender_name} not received within timeout"
298
+
299
+ result = await options.hooks.stop()
300
+ assert result is not None
301
+ assert result.should_continue is True
302
+ assert sender_name in result.system_message
303
+ assert text in result.system_message
304
+ finally:
305
+ await asyncio.gather(sender.close(), receiver.close())
306
+
307
+ @pytest.mark.asyncio
308
+ async def test_post_tool_use_hook_drains_inbox(self, monkeypatch):
309
+ """post_tool_use hook returns HookResult with system_message containing DM text."""
310
+ types_mod = _install_claude_sdk_module(monkeypatch)
311
+
312
+ from agent_relay.communicate.adapters.claude_sdk import on_relay
313
+
314
+ config = _live_config()
315
+ sender_name = _unique_name("e2e-claude-ptu-s")
316
+ receiver_name = _unique_name("e2e-claude-ptu-r")
317
+ sender = Relay(sender_name, config)
318
+ receiver = Relay(receiver_name, config)
319
+ options = _make_mock_options(receiver_name)
320
+ text = f"ptu-test-{uuid.uuid4().hex[:8]}"
321
+
322
+ try:
323
+ on_relay(options, receiver, name=receiver_name)
324
+ await sender.agents()
325
+ await receiver.agents()
326
+ await receiver.inbox()
327
+
328
+ await sender.send(receiver_name, text)
329
+
330
+ deadline = asyncio.get_event_loop().time() + 15.0
331
+ found = False
332
+ while asyncio.get_event_loop().time() < deadline:
333
+ msgs = await receiver.peek()
334
+ for m in msgs:
335
+ if m.sender == sender_name and m.text == text:
336
+ found = True
337
+ break
338
+ if found:
339
+ break
340
+ await asyncio.sleep(0.5)
341
+
342
+ assert found, f"DM from {sender_name} not received within timeout"
343
+
344
+ result = await options.hooks.post_tool_use()
345
+ assert result is not None
346
+ assert sender_name in result.system_message
347
+ assert text in result.system_message
348
+ finally:
349
+ await asyncio.gather(sender.close(), receiver.close())
350
+
351
+ @pytest.mark.asyncio
352
+ async def test_hooks_chain_with_original_hooks(self, monkeypatch):
353
+ """on_relay preserves and chains original hooks."""
354
+ _install_claude_sdk_module(monkeypatch)
355
+
356
+ from agent_relay.communicate.adapters.claude_sdk import on_relay
357
+
358
+ config = _live_config()
359
+ agent_name = _unique_name("e2e-claude-chain")
360
+ relay = Relay(agent_name, config)
361
+
362
+ orig_called = {"post": False, "stop": False}
363
+
364
+ async def orig_post_tool(*a, **kw):
365
+ orig_called["post"] = True
366
+ return None
367
+
368
+ async def orig_stop(*a, **kw):
369
+ orig_called["stop"] = True
370
+ return None
371
+
372
+ options = SimpleNamespace(
373
+ name=agent_name,
374
+ hooks=SimpleNamespace(post_tool_use=orig_post_tool, stop=orig_stop),
375
+ mcp_servers=[],
376
+ )
377
+
378
+ try:
379
+ on_relay(agent_name, options, relay)
380
+ await relay.agents()
381
+ await relay.inbox()
382
+
383
+ await options.hooks.post_tool_use()
384
+ await options.hooks.stop()
385
+
386
+ assert orig_called["post"], "Original post_tool_use was not called"
387
+ assert orig_called["stop"], "Original stop was not called"
388
+ finally:
389
+ await relay.close()
390
+
391
+
392
+ class TestCleanupE2E:
393
+ """Verify cleanup against live API."""
394
+
395
+ @pytest.mark.xfail(reason="Agent removal propagation depends on server-side TTL", strict=False)
396
+ @pytest.mark.asyncio
397
+ async def test_relay_close_disconnects(self):
398
+ """After relay.close(), the agent eventually disappears from the agent list."""
399
+ config = _live_config()
400
+ agent_name = _unique_name("e2e-claude-cleanup")
401
+ relay = Relay(agent_name, config)
402
+ probe = Relay(_unique_name("e2e-claude-probe"), config)
403
+
404
+ try:
405
+ agents = await relay.agents()
406
+ assert agent_name in agents
407
+
408
+ await relay.close()
409
+
410
+ # Allow rate-limit budget to recover before polling
411
+ await asyncio.sleep(5)
412
+
413
+ deadline = asyncio.get_event_loop().time() + 25.0
414
+ absent = False
415
+ while asyncio.get_event_loop().time() < deadline:
416
+ try:
417
+ current = await probe.agents()
418
+ except Exception:
419
+ await asyncio.sleep(2.0)
420
+ continue
421
+ if agent_name not in current:
422
+ absent = True
423
+ break
424
+ await asyncio.sleep(2.0)
425
+
426
+ assert absent, f"Agent {agent_name} still present after close"
427
+ finally:
428
+ await probe.close()