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,88 @@
1
+ """E2E test: OpenAI Agents Python adapter against live Relaycast."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import sys
7
+ import uuid
8
+ import time
9
+ from types import ModuleType
10
+ from unittest.mock import MagicMock
11
+
12
+ import pytest
13
+
14
+ from agent_relay.communicate.core import Relay
15
+ from agent_relay.communicate.types import RelayConfig
16
+
17
+
18
+ def _install_agents_module(monkeypatch):
19
+ """Inject a fake 'agents' module so the adapter can import function_tool."""
20
+ agents_mod = ModuleType("agents")
21
+ agents_mod.Agent = type("Agent", (), {})
22
+ agents_mod.function_tool = MagicMock(side_effect=lambda func: func)
23
+ monkeypatch.setitem(sys.modules, "agents", agents_mod)
24
+ return agents_mod
25
+
26
+
27
+ def _make_mock_agent(name: str):
28
+ agent = MagicMock()
29
+ agent.name = name
30
+ agent.tools = []
31
+ agent.instructions = "You are a helpful agent."
32
+ type(agent).__module__ = "agents"
33
+ return agent
34
+
35
+
36
+ def _unique_name(prefix: str) -> str:
37
+ return f"{prefix}-{int(time.time() * 1000)}-{uuid.uuid4().hex[:6]}"
38
+
39
+
40
+ @pytest.mark.asyncio
41
+ async def test_openai_agents_e2e(monkeypatch):
42
+ """Full round-trip: real Relay, mock Agent, live Relaycast API."""
43
+ agents_mod = _install_agents_module(monkeypatch)
44
+
45
+ from agent_relay.communicate.adapters.openai_agents import on_relay
46
+
47
+ agent_name = _unique_name("oai-py-e2e")
48
+ config = RelayConfig.resolve(channels=["general"], auto_cleanup=False)
49
+
50
+ relay = Relay(agent_name, config)
51
+ mock_agent = _make_mock_agent(agent_name)
52
+
53
+ try:
54
+ # Step 1: wrap agent — tools should be injected
55
+ wrapped = on_relay(mock_agent, relay)
56
+ assert wrapped is mock_agent
57
+ assert agents_mod.function_tool.call_count == 4
58
+ tool_names = [c.args[0].__name__ for c in agents_mod.function_tool.call_args_list]
59
+ assert set(tool_names) == {"relay_send", "relay_inbox", "relay_post", "relay_agents"}
60
+ assert len(mock_agent.tools) == 4
61
+
62
+ # Step 2: list_agents against real API
63
+ agents_list = await relay.agents()
64
+ assert isinstance(agents_list, list)
65
+ assert agent_name in agents_list, f"{agent_name} not in {agents_list}"
66
+
67
+ # Step 3: post a message to the general channel
68
+ test_text = f"e2e-openai-py-{uuid.uuid4().hex[:8]}"
69
+ await relay.post("general", test_text)
70
+
71
+ # Step 4: invoke the relay_agents tool closure
72
+ relay_agents_fn = mock_agent.tools[3]
73
+ result = await relay_agents_fn()
74
+ assert isinstance(result, str)
75
+ assert agent_name in result
76
+
77
+ # Step 5: invoke the relay_post tool closure
78
+ relay_post_fn = mock_agent.tools[2]
79
+ post_result = await relay_post_fn("general", f"tool-post-{uuid.uuid4().hex[:6]}")
80
+ assert post_result == "Message posted"
81
+
82
+ # Step 6: instructions wrapper returns base instructions when no pending msgs
83
+ assert callable(mock_agent.instructions)
84
+ instr = await mock_agent.instructions()
85
+ assert "You are a helpful agent." in instr
86
+
87
+ finally:
88
+ await relay.close()
@@ -0,0 +1,156 @@
1
+ """End-to-end tests for the Pi RPC Python adapter against live Relaycast.
2
+
3
+ Reuses a small number of Relay connections across tests to stay within
4
+ the 60 req/min rate limit of the free plan.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ import os
12
+ import time
13
+ import uuid
14
+ from unittest.mock import MagicMock, patch
15
+
16
+ import pytest
17
+
18
+ from agent_relay.communicate import Relay
19
+ from agent_relay.communicate.adapters.pi import RELAY_TOOL_PREAMBLE, PiRpcSession, on_relay
20
+ from agent_relay.communicate.types import Message, RelayConfig
21
+
22
+
23
+ def _live_config() -> RelayConfig:
24
+ return RelayConfig.resolve(
25
+ workspace=os.environ.get("RELAY_WORKSPACE"),
26
+ api_key=os.environ.get("RELAY_API_KEY"),
27
+ base_url=os.environ.get("RELAY_BASE_URL"),
28
+ channels=[],
29
+ auto_cleanup=False,
30
+ )
31
+
32
+
33
+ def _unique_name(prefix: str = "e2e-pi-py") -> str:
34
+ return f"{prefix}-{int(time.time() * 1000)}-{uuid.uuid4().hex[:8]}"
35
+
36
+
37
+ def _make_mock_proc():
38
+ proc = MagicMock()
39
+ proc.stdin = MagicMock()
40
+ proc.stdout = iter([])
41
+ proc.stderr = MagicMock()
42
+ proc.poll = MagicMock(return_value=None)
43
+ proc.terminate = MagicMock()
44
+ proc.wait = MagicMock()
45
+ return proc
46
+
47
+
48
+ @pytest.mark.asyncio
49
+ async def test_pi_adapter_e2e():
50
+ """Comprehensive e2e test for the Pi adapter against live Relaycast.
51
+
52
+ Uses only 2 Relay connections to minimize API calls and stay within rate limits.
53
+ Tests cover:
54
+ 1. PiRpcSession creation via on_relay() with a real Relay
55
+ 2. RELAY_TOOL_PREAMBLE contents
56
+ 3. Relay registration against live API (unique agent name with uuid suffix)
57
+ 4. relay.agents() - live agent list
58
+ 5. relay.send() + relay.inbox() - DM round-trip
59
+ 6. relay.post() - channel posting
60
+ 7. PiRpcSession relay message callback routing
61
+ 8. Cleanup / disconnect
62
+ """
63
+ config = _live_config()
64
+ sender_name = _unique_name("e2e-pi-sender")
65
+ receiver_name = _unique_name("e2e-pi-recv")
66
+
67
+ sender = Relay(sender_name, config)
68
+ receiver = Relay(receiver_name, config)
69
+
70
+ try:
71
+ # --- 1. Registration: both agents connect and appear in agent list ---
72
+ agents = await sender.agents()
73
+ assert sender_name in agents, f"{sender_name} not in agents after connect"
74
+
75
+ await asyncio.sleep(1)
76
+ agents2 = await receiver.agents()
77
+ assert receiver_name in agents2, f"{receiver_name} not in agents after connect"
78
+ assert sender_name in agents2, f"{sender_name} not visible to receiver"
79
+
80
+ # --- 2. RELAY_TOOL_PREAMBLE has expected relay tool descriptions ---
81
+ assert "relay_send" in RELAY_TOOL_PREAMBLE
82
+ assert "relay_inbox" in RELAY_TOOL_PREAMBLE
83
+ assert "relay_agents" in RELAY_TOOL_PREAMBLE
84
+ assert "relay_post" in RELAY_TOOL_PREAMBLE
85
+
86
+ # --- 3. on_relay() creates a PiRpcSession backed by a real Relay ---
87
+ with patch("subprocess.Popen") as mock_popen:
88
+ proc = _make_mock_proc()
89
+ mock_popen.return_value = proc
90
+
91
+ session = on_relay(sender_name, relay=sender)
92
+
93
+ assert isinstance(session, PiRpcSession)
94
+ assert session._relay is sender
95
+ assert session._relay.agent_name == sender_name
96
+
97
+ # --- 4. Relay message callback routes to Pi subprocess ---
98
+ msg = Message(sender="test-lead", text="status update request")
99
+ callback = sender._callbacks[0]
100
+ callback(msg)
101
+
102
+ written = proc.stdin.write.call_args[0][0]
103
+ parsed = json.loads(written.strip())
104
+ assert "test-lead" in parsed["message"]
105
+ assert "status update request" in parsed["message"]
106
+ assert parsed["streamingBehavior"] == "followUp"
107
+
108
+ # Clean up session (subprocess mock only)
109
+ session.close()
110
+ proc.terminate.assert_called_once()
111
+ assert session._closed
112
+
113
+ await asyncio.sleep(1)
114
+
115
+ # --- 5. DM round-trip: send() + inbox() ---
116
+ text = f"pi-e2e-{uuid.uuid4().hex[:8]}"
117
+ await sender.send(receiver_name, text)
118
+
119
+ deadline = asyncio.get_event_loop().time() + 15.0
120
+ found = False
121
+ while asyncio.get_event_loop().time() < deadline:
122
+ messages = await receiver.inbox()
123
+ for m in messages:
124
+ if m.sender == sender_name and m.text == text:
125
+ found = True
126
+ break
127
+ if found:
128
+ break
129
+ await asyncio.sleep(1.0)
130
+
131
+ assert found, f"DM from {sender_name} not received within timeout"
132
+
133
+ await asyncio.sleep(1)
134
+
135
+ # --- 6. Post to channel ---
136
+ await sender.join("general")
137
+ await sender.post("general", f"pi-e2e-test-{uuid.uuid4().hex[:8]}")
138
+
139
+ # --- 7. Cleanup: close both relays ---
140
+ # Relaycast presence is eventually consistent -- agents may remain
141
+ # "online" in list_agents for a heartbeat window after disconnect.
142
+ # We verify that close() completes without error (same approach as
143
+ # the existing transport e2e tests).
144
+ await sender.close()
145
+ assert not sender._connected, "sender._connected should be False after close"
146
+ await receiver.close()
147
+ assert not receiver._connected, "receiver._connected should be False after close"
148
+
149
+ except Exception:
150
+ # Best-effort cleanup on failure
151
+ for r in (sender, receiver):
152
+ try:
153
+ await r.close()
154
+ except Exception:
155
+ pass
156
+ raise
@@ -0,0 +1,239 @@
1
+ """E2E test: Swarms Python adapter against live Relaycast."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ import uuid
8
+ from unittest.mock import MagicMock
9
+
10
+ import pytest
11
+
12
+ from agent_relay.communicate.core import Relay
13
+ from agent_relay.communicate.types import Message, RelayConfig, RelayConnectionError
14
+
15
+
16
+ def _live_config() -> RelayConfig:
17
+ return RelayConfig.resolve(channels=[], auto_cleanup=False)
18
+
19
+
20
+ def _unique_name(prefix: str = "e2e-swarms-py") -> str:
21
+ return f"{prefix}-{int(time.time() * 1000)}-{uuid.uuid4().hex[:8]}"
22
+
23
+
24
+ def _make_mock_agent(name: str):
25
+ agent = MagicMock()
26
+ agent.name = name
27
+ agent.tools = []
28
+ agent.receive_message = MagicMock()
29
+ type(agent).__module__ = "swarms"
30
+ return agent
31
+
32
+
33
+ async def _retry_on_429(coro_fn, max_retries=5, base_delay=15.0):
34
+ """Call an async function, retrying on 429 rate limit errors."""
35
+ for attempt in range(max_retries):
36
+ try:
37
+ return await coro_fn()
38
+ except RelayConnectionError as e:
39
+ if e.status_code == 429 and attempt < max_retries - 1:
40
+ delay = base_delay * (attempt + 1)
41
+ await asyncio.sleep(delay)
42
+ continue
43
+ raise
44
+
45
+
46
+ async def _make_connected_relay(name: str, config: RelayConfig) -> Relay:
47
+ """Create a Relay and connect with 429 retry."""
48
+ relay = Relay(name, config)
49
+ await _retry_on_429(relay.agents)
50
+ return relay
51
+
52
+
53
+ class TestSwarmsToolInjection:
54
+ """Verify on_relay() injects tools (no API calls needed)."""
55
+
56
+ def test_on_relay_injects_four_tools(self):
57
+ from agent_relay.communicate.adapters.swarms import on_relay
58
+
59
+ mock_agent = _make_mock_agent("local-agent")
60
+ mock_relay = MagicMock()
61
+ mock_relay.on_message = MagicMock(return_value=lambda: None)
62
+
63
+ wrapped = on_relay(mock_agent, mock_relay)
64
+ assert wrapped is mock_agent
65
+ tool_names = [t.__name__ for t in mock_agent.tools]
66
+ assert set(tool_names) == {"relay_send", "relay_inbox", "relay_post", "relay_agents"}
67
+
68
+ def test_on_relay_registers_callback(self):
69
+ from agent_relay.communicate.adapters.swarms import on_relay
70
+
71
+ mock_agent = _make_mock_agent("cb-agent")
72
+ mock_relay = MagicMock()
73
+ mock_relay.on_message = MagicMock(return_value=lambda: None)
74
+
75
+ on_relay(mock_agent, mock_relay)
76
+ assert mock_relay.on_message.called
77
+
78
+ callback = mock_relay.on_message.call_args[0][0]
79
+ msg = Message(sender="test-lead", text="status check")
80
+ callback(msg)
81
+ mock_agent.receive_message.assert_called_once_with("test-lead", "status check")
82
+
83
+
84
+ class TestSwarmsLiveAPI:
85
+ """Live API tests — consolidated to minimize requests."""
86
+
87
+ @pytest.mark.asyncio
88
+ async def test_registration_and_agents_tool(self):
89
+ """Agent registers and relay_agents tool returns its name."""
90
+ from agent_relay.communicate.adapters.swarms import on_relay
91
+
92
+ agent_name = _unique_name("swarms-reg")
93
+ config = _live_config()
94
+ relay = Relay(agent_name, config)
95
+ mock_agent = _make_mock_agent(agent_name)
96
+
97
+ try:
98
+ on_relay(mock_agent, relay)
99
+ agents = await _retry_on_429(relay.agents)
100
+ assert agent_name in agents
101
+
102
+ tools = {t.__name__: t for t in mock_agent.tools}
103
+ result = await tools["relay_agents"]()
104
+ assert isinstance(result, str)
105
+ assert agent_name in result
106
+ finally:
107
+ await relay.close()
108
+
109
+ @pytest.mark.asyncio
110
+ async def test_post_tool_and_channel(self):
111
+ """The relay_post tool posts to a channel without errors."""
112
+ from agent_relay.communicate.adapters.swarms import on_relay
113
+
114
+ agent_name = _unique_name("swarms-post")
115
+ config = _live_config()
116
+ relay = Relay(agent_name, config)
117
+ mock_agent = _make_mock_agent(agent_name)
118
+
119
+ try:
120
+ on_relay(mock_agent, relay)
121
+ await _retry_on_429(relay.agents)
122
+ await _retry_on_429(lambda: relay.join("general"))
123
+
124
+ tools = {t.__name__: t for t in mock_agent.tools}
125
+ result = await tools["relay_post"]("general", f"swarms-e2e-{uuid.uuid4().hex[:8]}")
126
+ assert result == "Message posted"
127
+ finally:
128
+ await relay.close()
129
+
130
+ @pytest.mark.asyncio
131
+ async def test_send_and_inbox_dm(self):
132
+ """relay_send delivers a DM that relay_inbox retrieves."""
133
+ from agent_relay.communicate.adapters.swarms import on_relay
134
+
135
+ config = _live_config()
136
+ sender_name = _unique_name("swarms-s")
137
+ receiver_name = _unique_name("swarms-r")
138
+ sender_relay = Relay(sender_name, config)
139
+ receiver_relay = Relay(receiver_name, config)
140
+ sender_agent = _make_mock_agent(sender_name)
141
+ receiver_agent = _make_mock_agent(receiver_name)
142
+ text = f"swarms-dm-{uuid.uuid4().hex[:8]}"
143
+
144
+ try:
145
+ on_relay(sender_agent, sender_relay)
146
+ on_relay(receiver_agent, receiver_relay)
147
+
148
+ await _retry_on_429(sender_relay.agents)
149
+ await _retry_on_429(receiver_relay.agents)
150
+
151
+ sender_tools = {t.__name__: t for t in sender_agent.tools}
152
+ result = await sender_tools["relay_send"](receiver_name, text)
153
+ assert result == "Message sent"
154
+
155
+ deadline = asyncio.get_event_loop().time() + 20.0
156
+ found = False
157
+ while asyncio.get_event_loop().time() < deadline:
158
+ try:
159
+ messages = await receiver_relay.inbox()
160
+ except RelayConnectionError as e:
161
+ if e.status_code == 429:
162
+ await asyncio.sleep(15)
163
+ continue
164
+ raise
165
+ for msg in messages:
166
+ if msg.sender == sender_name and msg.text == text:
167
+ found = True
168
+ break
169
+ if found:
170
+ break
171
+ await asyncio.sleep(2.0)
172
+
173
+ assert found, f"DM from {sender_name} not received within timeout"
174
+ finally:
175
+ await asyncio.gather(sender_relay.close(), receiver_relay.close())
176
+
177
+ @pytest.mark.asyncio
178
+ async def test_callback_with_live_relay(self):
179
+ """on_message callback routes messages to agent.receive_message with a live relay."""
180
+ from agent_relay.communicate.adapters.swarms import on_relay
181
+
182
+ agent_name = _unique_name("swarms-cb")
183
+ config = _live_config()
184
+ relay = Relay(agent_name, config)
185
+ mock_agent = _make_mock_agent(agent_name)
186
+
187
+ try:
188
+ on_relay(mock_agent, relay)
189
+ await _retry_on_429(relay.agents)
190
+
191
+ assert len(relay._callbacks) >= 1
192
+ callback = relay._callbacks[0]
193
+ msg = Message(sender="test-lead", text="status check")
194
+ callback(msg)
195
+
196
+ mock_agent.receive_message.assert_called_once_with("test-lead", "status check")
197
+ finally:
198
+ await relay.close()
199
+
200
+ @pytest.mark.asyncio
201
+ async def test_cleanup_removes_agent(self):
202
+ """After relay.close(), agent disappears from the agent list."""
203
+ config = _live_config()
204
+ agent_name = _unique_name("swarms-cl")
205
+ relay = Relay(agent_name, config)
206
+ probe = Relay(_unique_name("swarms-pr"), config)
207
+
208
+ try:
209
+ agents = await _retry_on_429(relay.agents)
210
+ assert agent_name in agents
211
+
212
+ await relay.close()
213
+
214
+ # If close() silently failed to unregister (e.g. 429), retry manually
215
+ if relay.transport.agent_id is not None:
216
+ await asyncio.sleep(15)
217
+ try:
218
+ await relay.transport.unregister_agent()
219
+ except Exception:
220
+ pass
221
+
222
+ deadline = asyncio.get_event_loop().time() + 35.0
223
+ absent = False
224
+ while asyncio.get_event_loop().time() < deadline:
225
+ try:
226
+ current = await probe.agents()
227
+ except RelayConnectionError as e:
228
+ if e.status_code == 429:
229
+ await asyncio.sleep(15)
230
+ continue
231
+ raise
232
+ if agent_name not in current:
233
+ absent = True
234
+ break
235
+ await asyncio.sleep(3.0)
236
+
237
+ assert absent, f"Agent {agent_name} still present after close"
238
+ finally:
239
+ await probe.close()
@@ -0,0 +1,140 @@
1
+ """Tests for the Agno Python adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import sys
7
+ from unittest.mock import AsyncMock, MagicMock
8
+
9
+ import pytest
10
+
11
+ # Mock agno before it's imported in the adapter
12
+ agno_mock = MagicMock()
13
+ sys.modules["agno"] = agno_mock
14
+ sys.modules["agno.agent"] = agno_mock.agent
15
+
16
+ def _adapter_module():
17
+ import importlib
18
+ if "agent_relay.communicate.adapters.agno" in sys.modules:
19
+ importlib.reload(sys.modules["agent_relay.communicate.adapters.agno"])
20
+ return importlib.import_module("agent_relay.communicate.adapters.agno")
21
+
22
+ @pytest.fixture
23
+ def mock_relay():
24
+ relay = MagicMock()
25
+ relay.agent_name = "TestAgent"
26
+ relay.inbox = AsyncMock(return_value=[])
27
+ relay.peek = AsyncMock(return_value=[])
28
+ relay.send = AsyncMock()
29
+ relay.post = AsyncMock()
30
+ relay.agents = AsyncMock(return_value=[])
31
+ return relay
32
+
33
+ @pytest.fixture
34
+ def mock_agent():
35
+ agent = MagicMock()
36
+ agent.tools = []
37
+ agent.instructions = "Agno base instructions."
38
+ return agent
39
+
40
+ def test_on_relay_adds_tools(mock_relay, mock_agent):
41
+ adapter = _adapter_module()
42
+
43
+ adapter.on_relay(mock_agent, mock_relay)
44
+
45
+ tools = {t.__name__: t for t in mock_agent.tools}
46
+ assert "relay_send" in tools
47
+ assert "relay_inbox" in tools
48
+ assert "relay_post" in tools
49
+ assert "relay_agents" in tools
50
+
51
+ @pytest.mark.asyncio
52
+ async def test_tool_execution(mock_relay, mock_agent):
53
+ adapter = _adapter_module()
54
+ adapter.on_relay(mock_agent, mock_relay)
55
+ tools = {t.__name__: t for t in mock_agent.tools}
56
+
57
+ # Test relay_send
58
+ await tools["relay_send"]("Alice", "Hi")
59
+ mock_relay.send.assert_called_with("Alice", "Hi")
60
+
61
+ # Test relay_inbox
62
+ from agent_relay.communicate.types import Message
63
+ mock_relay.inbox.return_value = [Message(sender="Bob", text="Hey")]
64
+ inbox_res = await tools["relay_inbox"]()
65
+ assert "From Bob: Hey" in inbox_res
66
+
67
+ # Test relay_post
68
+ await tools["relay_post"]("general", "Update")
69
+ mock_relay.post.assert_called_with("general", "Update")
70
+
71
+ # Test relay_agents
72
+ mock_relay.agents.return_value = ["Alice", "Bob"]
73
+ agents_res = await tools["relay_agents"]()
74
+ assert "Alice, Bob" in agents_res
75
+
76
+ @pytest.mark.asyncio
77
+ async def test_instructions_wrapping(mock_relay, mock_agent):
78
+ adapter = _adapter_module()
79
+ from agent_relay.communicate.types import Message
80
+
81
+ mock_relay.peek.return_value = [
82
+ Message(sender="Other", text="Agno message", message_id="1")
83
+ ]
84
+
85
+ adapter.on_relay(mock_agent, mock_relay)
86
+
87
+ assert callable(mock_agent.instructions)
88
+
89
+ result = await mock_agent.instructions()
90
+ assert "Agno base instructions." in result
91
+ assert "Other: Agno message" in result
92
+
93
+ @pytest.mark.asyncio
94
+ async def test_instructions_wrapping_callable(mock_relay, mock_agent):
95
+ """Chaining: existing callable instructions are preserved."""
96
+ adapter = _adapter_module()
97
+
98
+ original_instructions = MagicMock(return_value="Dynamic context.")
99
+ mock_agent.instructions = original_instructions
100
+
101
+ adapter.on_relay(mock_agent, mock_relay)
102
+
103
+ result = await mock_agent.instructions()
104
+ assert "Dynamic context." in result
105
+ assert original_instructions.called
106
+
107
+
108
+ @pytest.mark.asyncio
109
+ async def test_instructions_wrapping_async_callable(mock_relay, mock_agent):
110
+ adapter = _adapter_module()
111
+ from agent_relay.communicate.types import Message
112
+
113
+ async def original_instructions():
114
+ return "Async context."
115
+
116
+ mock_agent.instructions = original_instructions
117
+ mock_relay.peek.return_value = [Message(sender="Other", text="Agno message", message_id="1")]
118
+
119
+ adapter.on_relay(mock_agent, mock_relay)
120
+
121
+ result = await mock_agent.instructions()
122
+ assert result.startswith("\n\nNew messages from other agents:\n Other: Agno message\n")
123
+ assert result.endswith("\n\nAsync context.")
124
+
125
+ @pytest.mark.asyncio
126
+ async def test_instructions_no_messages(mock_relay, mock_agent):
127
+ """When inbox is empty, only base instructions returned."""
128
+ adapter = _adapter_module()
129
+ mock_relay.peek = AsyncMock(return_value=[])
130
+
131
+ adapter.on_relay(mock_agent, mock_relay)
132
+
133
+ result = await mock_agent.instructions()
134
+ assert result == "Agno base instructions."
135
+
136
+ def test_on_relay_returns_agent(mock_relay, mock_agent):
137
+ """on_relay() returns the modified agent."""
138
+ adapter = _adapter_module()
139
+ result = adapter.on_relay(mock_agent, mock_relay)
140
+ assert result is mock_agent