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,234 @@
1
+ """Real end-to-end test of the CrewAI Python adapter against live Relaycast."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import time
8
+ import uuid
9
+
10
+ import pytest
11
+
12
+ from agent_relay.communicate.core import Relay
13
+ from agent_relay.communicate.types import RelayConfig
14
+
15
+ pytestmark = pytest.mark.asyncio
16
+
17
+ RATE_LIMIT_PAUSE = 5 # seconds between tests to avoid 429
18
+
19
+
20
+ @pytest.fixture(autouse=True)
21
+ async def _rate_limit_pause():
22
+ """Pause between tests to respect the 60 req/min rate limit."""
23
+ yield
24
+ await asyncio.sleep(RATE_LIMIT_PAUSE)
25
+
26
+
27
+ def _e2e_config() -> RelayConfig:
28
+ config = RelayConfig.resolve(
29
+ workspace=os.environ.get("RELAY_WORKSPACE"),
30
+ api_key=os.environ.get("RELAY_API_KEY"),
31
+ base_url=os.environ.get("RELAY_BASE_URL"),
32
+ channels=[],
33
+ auto_cleanup=False,
34
+ )
35
+ if not config.workspace or not config.api_key:
36
+ pytest.fail("RELAY_WORKSPACE and RELAY_API_KEY must be set.")
37
+ return config
38
+
39
+
40
+ def _unique_name(prefix: str) -> str:
41
+ ts = int(time.time() * 1000)
42
+ return f"{prefix}-{ts}-{uuid.uuid4().hex[:6]}"
43
+
44
+
45
+ async def test_crewai_on_relay_adds_tools():
46
+ """on_relay() should attach four relay tools to a CrewAI Agent."""
47
+ from crewai import Agent as CrewAgent
48
+ from agent_relay.communicate.adapters.crewai import on_relay
49
+
50
+ config = _e2e_config()
51
+ agent_name = _unique_name("e2e-crewai-tools")
52
+
53
+ relay = Relay(agent_name, config)
54
+ try:
55
+ crew_agent = CrewAgent(
56
+ role="Test worker",
57
+ goal="Verify relay tools",
58
+ backstory="Test agent.",
59
+ llm="gpt-4o-mini",
60
+ )
61
+ original_tool_count = len(crew_agent.tools)
62
+
63
+ wrapped = on_relay(crew_agent, relay)
64
+ assert wrapped is crew_agent
65
+
66
+ tool_names = {t.name for t in crew_agent.tools}
67
+ expected = {"relay_send", "relay_inbox", "relay_post", "relay_agents"}
68
+ assert expected.issubset(tool_names), f"Missing tools: {expected - tool_names}"
69
+ assert len(crew_agent.tools) >= original_tool_count + 4
70
+ finally:
71
+ await relay.close()
72
+
73
+
74
+ async def test_crewai_relay_agents_lists_live_agents():
75
+ """relay_agents tool should list agents from the live workspace."""
76
+ from crewai import Agent as CrewAgent
77
+ from agent_relay.communicate.adapters.crewai import on_relay
78
+
79
+ config = _e2e_config()
80
+ agent_name = _unique_name("e2e-crewai-list")
81
+
82
+ relay = Relay(agent_name, config)
83
+ try:
84
+ crew_agent = CrewAgent(
85
+ role="Lister",
86
+ goal="List agents",
87
+ backstory="Lists agents.",
88
+ llm="gpt-4o-mini",
89
+ )
90
+ on_relay(crew_agent, relay)
91
+
92
+ tools_by_name = {t.name: t for t in crew_agent.tools}
93
+ agents_tool = tools_by_name["relay_agents"]
94
+
95
+ result = await agents_tool.func()
96
+ assert isinstance(result, str)
97
+ assert agent_name in result, f"Expected {agent_name} in agents list, got: {result}"
98
+ finally:
99
+ await relay.close()
100
+
101
+
102
+ async def test_crewai_relay_send_dm():
103
+ """relay_send tool should successfully send a DM via live API."""
104
+ from crewai import Agent as CrewAgent
105
+ from agent_relay.communicate.adapters.crewai import on_relay
106
+
107
+ config = _e2e_config()
108
+ sender_name = _unique_name("e2e-crewai-sender")
109
+ receiver_name = _unique_name("e2e-crewai-recv")
110
+
111
+ sender_relay = Relay(sender_name, config)
112
+ receiver_relay = Relay(receiver_name, config)
113
+ try:
114
+ sender_agent = CrewAgent(
115
+ role="Sender",
116
+ goal="Send messages",
117
+ backstory="Sends.",
118
+ llm="gpt-4o-mini",
119
+ )
120
+ on_relay(sender_agent, relay=sender_relay)
121
+
122
+ # Connect receiver so it exists
123
+ await receiver_relay._ensure_connected()
124
+
125
+ tools_by_name = {t.name: t for t in sender_agent.tools}
126
+ send_tool = tools_by_name["relay_send"]
127
+
128
+ result = await send_tool.func(receiver_name, f"hello-{uuid.uuid4().hex[:8]}")
129
+ assert result == "Message sent"
130
+ finally:
131
+ await asyncio.gather(sender_relay.close(), receiver_relay.close())
132
+
133
+
134
+ async def test_crewai_relay_post_channel():
135
+ """relay_post tool should successfully post to a channel via live API."""
136
+ from crewai import Agent as CrewAgent
137
+ from agent_relay.communicate.adapters.crewai import on_relay
138
+
139
+ config = _e2e_config()
140
+ agent_name = _unique_name("e2e-crewai-post")
141
+
142
+ relay = Relay(agent_name, config)
143
+ try:
144
+ crew_agent = CrewAgent(
145
+ role="Poster",
146
+ goal="Post to channel",
147
+ backstory="Posts.",
148
+ llm="gpt-4o-mini",
149
+ )
150
+ on_relay(crew_agent, relay=relay)
151
+
152
+ # Join general before posting
153
+ await relay.join("general")
154
+
155
+ tools_by_name = {t.name: t for t in crew_agent.tools}
156
+ post_tool = tools_by_name["relay_post"]
157
+
158
+ msg_text = f"e2e-crewai-post-{uuid.uuid4().hex[:8]}"
159
+ result = await post_tool.func("general", msg_text)
160
+ assert result == "Message posted"
161
+ finally:
162
+ await relay.close()
163
+
164
+
165
+ async def test_crewai_backstory_wrapping():
166
+ """on_relay() should wrap backstory so it includes relay messages."""
167
+ from crewai import Agent as CrewAgent
168
+ from agent_relay.communicate.adapters.crewai import on_relay
169
+
170
+ config = _e2e_config()
171
+ agent_name = _unique_name("e2e-crewai-backstory")
172
+
173
+ relay = Relay(agent_name, config)
174
+ try:
175
+ original_backstory = "Expert researcher."
176
+ crew_agent = CrewAgent(
177
+ role="Researcher",
178
+ goal="Research things",
179
+ backstory=original_backstory,
180
+ llm="gpt-4o-mini",
181
+ )
182
+ on_relay(crew_agent, relay=relay)
183
+
184
+ backstory_str = str(crew_agent.backstory)
185
+ assert "Expert researcher." in backstory_str
186
+ finally:
187
+ await relay.close()
188
+
189
+
190
+ async def test_crewai_full_round_trip():
191
+ """Full round-trip: register, list agents, send DM, post to channel, close."""
192
+ from crewai import Agent as CrewAgent
193
+ from agent_relay.communicate.adapters.crewai import on_relay
194
+
195
+ config = _e2e_config()
196
+ agent_name = _unique_name("e2e-crewai-full")
197
+ peer_name = _unique_name("e2e-crewai-peer")
198
+
199
+ relay = Relay(agent_name, config)
200
+ peer_relay = Relay(peer_name, config)
201
+
202
+ try:
203
+ crew_agent = CrewAgent(
204
+ role="Full test",
205
+ goal="Run full e2e",
206
+ backstory="Full round trip.",
207
+ llm="gpt-4o-mini",
208
+ )
209
+ on_relay(crew_agent, relay=relay)
210
+ await peer_relay._ensure_connected()
211
+
212
+ tools = {t.name: t for t in crew_agent.tools}
213
+
214
+ # 1. list_agents
215
+ agents_result = await tools["relay_agents"].func()
216
+ assert agent_name in agents_result
217
+ assert peer_name in agents_result
218
+
219
+ # 2. send DM
220
+ dm_text = f"e2e-roundtrip-{uuid.uuid4().hex[:8]}"
221
+ send_result = await tools["relay_send"].func(peer_name, dm_text)
222
+ assert send_result == "Message sent"
223
+
224
+ # 3. post to channel
225
+ await relay.join("general")
226
+ post_text = f"e2e-channel-{uuid.uuid4().hex[:8]}"
227
+ post_result = await tools["relay_post"].func("general", post_text)
228
+ assert post_result == "Message posted"
229
+
230
+ # 4. inbox (may or may not have messages, just verify it runs)
231
+ inbox_result = await tools["relay_inbox"].func()
232
+ assert isinstance(inbox_result, str)
233
+ finally:
234
+ await asyncio.gather(relay.close(), peer_relay.close())
@@ -0,0 +1,182 @@
1
+ """E2E test: Google ADK Python adapter against live Relaycast.
2
+
3
+ Single consolidated test to stay within the 60 req/min rate limit.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import os
10
+ import sys
11
+ import time
12
+ import uuid
13
+ from types import ModuleType
14
+ from unittest.mock import MagicMock
15
+
16
+ import pytest
17
+
18
+ from agent_relay.communicate.core import Relay
19
+ from agent_relay.communicate.types import RelayConfig
20
+
21
+
22
+ def _install_google_modules(monkeypatch):
23
+ """Inject fake google.adk / google.genai modules so the adapter can import."""
24
+ google_module = ModuleType("google")
25
+ google_adk_module = ModuleType("google.adk")
26
+ google_adk_agents_module = ModuleType("google.adk.agents")
27
+ google_genai_module = ModuleType("google.genai")
28
+ google_genai_types_module = ModuleType("google.genai.types")
29
+
30
+ class Part:
31
+ def __init__(self, text: str):
32
+ self.text = text
33
+
34
+ class Content:
35
+ def __init__(self, role: str, parts: list):
36
+ self.role = role
37
+ self.parts = parts
38
+
39
+ google_module.adk = google_adk_module
40
+ google_module.genai = google_genai_module
41
+ google_adk_module.agents = google_adk_agents_module
42
+ google_genai_module.types = google_genai_types_module
43
+ google_genai_types_module.Content = Content
44
+ google_genai_types_module.Part = Part
45
+
46
+ monkeypatch.setitem(sys.modules, "google", google_module)
47
+ monkeypatch.setitem(sys.modules, "google.adk", google_adk_module)
48
+ monkeypatch.setitem(sys.modules, "google.adk.agents", google_adk_agents_module)
49
+ monkeypatch.setitem(sys.modules, "google.genai", google_genai_module)
50
+ monkeypatch.setitem(sys.modules, "google.genai.types", google_genai_types_module)
51
+
52
+ return google_genai_types_module
53
+
54
+
55
+ def _live_config() -> RelayConfig:
56
+ return RelayConfig.resolve(
57
+ workspace=os.environ.get("RELAY_WORKSPACE"),
58
+ api_key=os.environ.get("RELAY_API_KEY"),
59
+ base_url=os.environ.get("RELAY_BASE_URL"),
60
+ channels=[],
61
+ auto_cleanup=False,
62
+ )
63
+
64
+
65
+ def _unique_name(prefix: str = "e2e-adk-py") -> str:
66
+ return f"{prefix}-{int(time.time() * 1000)}-{uuid.uuid4().hex[:8]}"
67
+
68
+
69
+ def _make_mock_agent(name: str):
70
+ agent = MagicMock()
71
+ agent.name = name
72
+ agent.tools = []
73
+ agent.before_model_callback = None
74
+ type(agent).__module__ = "google.adk.agents"
75
+ return agent
76
+
77
+
78
+ def _tool_by_name(agent, name: str):
79
+ return next(t for t in agent.tools if t.__name__ == name)
80
+
81
+
82
+ @pytest.mark.asyncio
83
+ async def test_google_adk_e2e_full_round_trip(monkeypatch):
84
+ """Full round-trip: real Relay, mock ADK Agent, live Relaycast API.
85
+
86
+ Exercises on_relay tool injection, list_agents, post, send/inbox,
87
+ before_model_callback, and cleanup -- all in one test to stay
88
+ within rate limits.
89
+ """
90
+ types_mod = _install_google_modules(monkeypatch)
91
+ from agent_relay.communicate.adapters.google_adk import on_relay
92
+
93
+ sender_name = _unique_name("adk-sender")
94
+ receiver_name = _unique_name("adk-recv")
95
+ config = _live_config()
96
+
97
+ sender_relay = Relay(sender_name, config)
98
+ receiver_relay = Relay(receiver_name, config)
99
+ sender_agent = _make_mock_agent(sender_name)
100
+ receiver_agent = _make_mock_agent(receiver_name)
101
+
102
+ try:
103
+ # -- Step 1: on_relay injects 4 tools + before_model_callback --
104
+ wrapped_s = on_relay(sender_agent, sender_relay)
105
+ wrapped_r = on_relay(receiver_agent, receiver_relay)
106
+
107
+ assert wrapped_s is sender_agent
108
+ assert wrapped_r is receiver_agent
109
+
110
+ expected_tools = {"relay_send", "relay_inbox", "relay_post", "relay_agents"}
111
+ assert set(t.__name__ for t in sender_agent.tools) == expected_tools
112
+ assert set(t.__name__ for t in receiver_agent.tools) == expected_tools
113
+ assert receiver_agent.before_model_callback is not None
114
+
115
+ # -- Step 2: list_agents via tool closure (live API) --
116
+ # Ensure both agents are registered before any cross-agent calls
117
+ agents_result = await _tool_by_name(sender_agent, "relay_agents")()
118
+ assert isinstance(agents_result, str)
119
+ assert sender_name in agents_result
120
+
121
+ receiver_agents = await _tool_by_name(receiver_agent, "relay_agents")()
122
+ assert receiver_name in receiver_agents
123
+
124
+ # -- Step 3: post to general channel via tool closure --
125
+ await sender_relay.join("general")
126
+ post_result = await _tool_by_name(sender_agent, "relay_post")(
127
+ "general", f"adk-e2e-{uuid.uuid4().hex[:8]}"
128
+ )
129
+ assert post_result == "Message posted"
130
+
131
+ # -- Step 4: send DM and verify inbox round-trip --
132
+ dm_text = f"adk-dm-{uuid.uuid4().hex[:8]}"
133
+ send_fn = _tool_by_name(sender_agent, "relay_send")
134
+ result = await send_fn(receiver_name, dm_text)
135
+ assert result == "Message sent"
136
+
137
+ inbox_fn = _tool_by_name(receiver_agent, "relay_inbox")
138
+ deadline = asyncio.get_event_loop().time() + 15.0
139
+ found = False
140
+ while asyncio.get_event_loop().time() < deadline:
141
+ inbox_result = await inbox_fn()
142
+ if dm_text in inbox_result:
143
+ found = True
144
+ break
145
+ await asyncio.sleep(0.5)
146
+ assert found, f"DM containing '{dm_text}' not received within timeout"
147
+
148
+ # -- Step 5: before_model_callback injects relay messages --
149
+ cb_text = f"adk-cb-{uuid.uuid4().hex[:8]}"
150
+ await sender_relay.send(receiver_name, cb_text)
151
+
152
+ deadline = asyncio.get_event_loop().time() + 15.0
153
+ while asyncio.get_event_loop().time() < deadline:
154
+ msgs = await receiver_relay.peek()
155
+ if any(cb_text in m.text for m in msgs):
156
+ break
157
+ await asyncio.sleep(0.5)
158
+
159
+ llm_request = MagicMock()
160
+ llm_request.contents = []
161
+ cb_result = await receiver_agent.before_model_callback(llm_request)
162
+ assert cb_result is None
163
+
164
+ injected = [p.text for c in llm_request.contents for p in c.parts]
165
+ assert any(cb_text in t for t in injected), (
166
+ f"Expected '{cb_text}' in callback-injected contents, got: {injected}"
167
+ )
168
+
169
+ # -- Step 6: cleanup -- close sender, verify internal state reset --
170
+ await sender_relay.close()
171
+ assert not sender_relay._connected
172
+ assert not sender_relay._ws_connected
173
+
174
+ finally:
175
+ try:
176
+ await sender_relay.close()
177
+ except Exception:
178
+ pass
179
+ try:
180
+ await receiver_relay.close()
181
+ except Exception:
182
+ pass
@@ -0,0 +1,262 @@
1
+ """E2E test: LangGraph-style tool wrapping with core Relay against live Relaycast.
2
+
3
+ There is no dedicated LangGraph Python adapter — this test demonstrates using
4
+ the core Relay class with LangGraph-compatible tool functions (plain async
5
+ callables that can be wrapped with @tool or used directly in a ToolNode).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import os
12
+ import time
13
+ import uuid
14
+
15
+ import pytest
16
+
17
+ from agent_relay.communicate.core import Relay
18
+ from agent_relay.communicate.types import RelayConfig
19
+
20
+ pytestmark = pytest.mark.asyncio
21
+
22
+ RATE_LIMIT_PAUSE = 5 # seconds between tests to avoid 429
23
+
24
+
25
+ @pytest.fixture(autouse=True)
26
+ async def _rate_limit_pause():
27
+ """Pause between tests to respect the 60 req/min rate limit."""
28
+ yield
29
+ await asyncio.sleep(RATE_LIMIT_PAUSE)
30
+
31
+
32
+ def _e2e_config() -> RelayConfig:
33
+ config = RelayConfig.resolve(
34
+ workspace=os.environ.get("RELAY_WORKSPACE"),
35
+ api_key=os.environ.get("RELAY_API_KEY"),
36
+ base_url=os.environ.get("RELAY_BASE_URL"),
37
+ channels=[],
38
+ auto_cleanup=False,
39
+ )
40
+ if not config.workspace or not config.api_key:
41
+ pytest.fail("RELAY_WORKSPACE and RELAY_API_KEY must be set.")
42
+ return config
43
+
44
+
45
+ def _unique_name(prefix: str = "e2e-langgraph") -> str:
46
+ ts = int(time.time() * 1000)
47
+ return f"{prefix}-{ts}-{uuid.uuid4().hex[:6]}"
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # LangGraph-style tool factories
52
+ # These mirror what a real LangGraph ToolNode would use.
53
+ # ---------------------------------------------------------------------------
54
+
55
+ def make_relay_tools(relay: Relay) -> dict[str, object]:
56
+ """Create LangGraph-compatible async tool functions backed by a Relay instance."""
57
+
58
+ async def relay_send(to: str, message: str) -> str:
59
+ """Send a DM to another agent."""
60
+ await relay.send(to, message)
61
+ return "Message sent"
62
+
63
+ async def relay_inbox() -> str:
64
+ """Check the inbox for new messages."""
65
+ messages = await relay.inbox()
66
+ if not messages:
67
+ return "No new messages"
68
+ return "\n".join(f"[{m.sender}] {m.text}" for m in messages)
69
+
70
+ async def relay_agents() -> str:
71
+ """List all connected agents."""
72
+ agents = await relay.agents()
73
+ return ", ".join(agents) if agents else "No agents online"
74
+
75
+ async def relay_post(channel: str, message: str) -> str:
76
+ """Post a message to a channel."""
77
+ await relay.post(channel, message)
78
+ return "Message posted"
79
+
80
+ return {
81
+ "relay_send": relay_send,
82
+ "relay_inbox": relay_inbox,
83
+ "relay_agents": relay_agents,
84
+ "relay_post": relay_post,
85
+ }
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Tests
90
+ # ---------------------------------------------------------------------------
91
+
92
+ async def test_langgraph_tool_creation():
93
+ """make_relay_tools() should return four async callables."""
94
+ config = _e2e_config()
95
+ agent_name = _unique_name("lg-tools")
96
+
97
+ relay = Relay(agent_name, config)
98
+ try:
99
+ tools = make_relay_tools(relay)
100
+ expected = {"relay_send", "relay_inbox", "relay_agents", "relay_post"}
101
+ assert set(tools.keys()) == expected
102
+ for fn in tools.values():
103
+ assert asyncio.iscoroutinefunction(fn)
104
+ finally:
105
+ await relay.close()
106
+
107
+
108
+ async def test_langgraph_relay_agents_live():
109
+ """relay_agents tool should list agents from the live workspace."""
110
+ config = _e2e_config()
111
+ agent_name = _unique_name("lg-list")
112
+
113
+ relay = Relay(agent_name, config)
114
+ try:
115
+ tools = make_relay_tools(relay)
116
+ result = await tools["relay_agents"]()
117
+ assert isinstance(result, str)
118
+ assert agent_name in result, f"Expected {agent_name} in agents list, got: {result}"
119
+ finally:
120
+ await relay.close()
121
+
122
+
123
+ async def test_langgraph_relay_send_dm():
124
+ """relay_send tool should successfully send a DM via live API."""
125
+ config = _e2e_config()
126
+ sender_name = _unique_name("lg-sender")
127
+ receiver_name = _unique_name("lg-recv")
128
+
129
+ sender_relay = Relay(sender_name, config)
130
+ receiver_relay = Relay(receiver_name, config)
131
+ try:
132
+ await receiver_relay._ensure_connected()
133
+
134
+ tools = make_relay_tools(sender_relay)
135
+ result = await tools["relay_send"](receiver_name, f"hello-{uuid.uuid4().hex[:8]}")
136
+ assert result == "Message sent"
137
+ finally:
138
+ await asyncio.gather(sender_relay.close(), receiver_relay.close())
139
+
140
+
141
+ async def test_langgraph_relay_post_channel():
142
+ """relay_post tool should successfully post to a channel via live API."""
143
+ config = _e2e_config()
144
+ agent_name = _unique_name("lg-post")
145
+
146
+ relay = Relay(agent_name, config)
147
+ try:
148
+ await relay.join("general")
149
+
150
+ tools = make_relay_tools(relay)
151
+ msg_text = f"e2e-lg-post-{uuid.uuid4().hex[:8]}"
152
+ result = await tools["relay_post"]("general", msg_text)
153
+ assert result == "Message posted"
154
+ finally:
155
+ await relay.close()
156
+
157
+
158
+ async def test_langgraph_inbox_round_trip():
159
+ """Send a DM and verify it appears in the receiver's inbox."""
160
+ config = _e2e_config()
161
+ sender_name = _unique_name("lg-inbox-s")
162
+ receiver_name = _unique_name("lg-inbox-r")
163
+
164
+ sender_relay = Relay(sender_name, config)
165
+ receiver_relay = Relay(receiver_name, config)
166
+ try:
167
+ sender_tools = make_relay_tools(sender_relay)
168
+ receiver_tools = make_relay_tools(receiver_relay)
169
+
170
+ dm_text = f"lg-dm-{uuid.uuid4().hex[:8]}"
171
+ await sender_tools["relay_send"](receiver_name, dm_text)
172
+
173
+ deadline = asyncio.get_event_loop().time() + 15.0
174
+ found = False
175
+ while asyncio.get_event_loop().time() < deadline:
176
+ inbox_result = await receiver_tools["relay_inbox"]()
177
+ if dm_text in inbox_result:
178
+ found = True
179
+ break
180
+ await asyncio.sleep(0.5)
181
+ assert found, f"DM containing '{dm_text}' not received within timeout"
182
+ finally:
183
+ await asyncio.gather(sender_relay.close(), receiver_relay.close())
184
+
185
+
186
+ async def test_langgraph_full_round_trip():
187
+ """Full round-trip: register, list agents, send DM, post to channel, inbox, close."""
188
+ config = _e2e_config()
189
+ agent_name = _unique_name("lg-full")
190
+ peer_name = _unique_name("lg-peer")
191
+
192
+ relay = Relay(agent_name, config)
193
+ peer_relay = Relay(peer_name, config)
194
+
195
+ try:
196
+ tools = make_relay_tools(relay)
197
+ peer_tools = make_relay_tools(peer_relay)
198
+
199
+ # Ensure peer is connected
200
+ await peer_relay._ensure_connected()
201
+
202
+ # 1. list_agents — both should appear
203
+ agents_result = await tools["relay_agents"]()
204
+ assert agent_name in agents_result
205
+ assert peer_name in agents_result
206
+
207
+ # 2. send DM
208
+ dm_text = f"lg-roundtrip-{uuid.uuid4().hex[:8]}"
209
+ send_result = await tools["relay_send"](peer_name, dm_text)
210
+ assert send_result == "Message sent"
211
+
212
+ # 3. post to channel
213
+ await relay.join("general")
214
+ post_text = f"lg-channel-{uuid.uuid4().hex[:8]}"
215
+ post_result = await tools["relay_post"]("general", post_text)
216
+ assert post_result == "Message posted"
217
+
218
+ # 4. inbox on peer — verify DM arrived
219
+ deadline = asyncio.get_event_loop().time() + 15.0
220
+ found = False
221
+ while asyncio.get_event_loop().time() < deadline:
222
+ inbox_result = await peer_tools["relay_inbox"]()
223
+ if dm_text in inbox_result:
224
+ found = True
225
+ break
226
+ await asyncio.sleep(0.5)
227
+ assert found, f"DM containing '{dm_text}' not received within timeout"
228
+
229
+ finally:
230
+ await asyncio.gather(relay.close(), peer_relay.close())
231
+
232
+
233
+ @pytest.mark.xfail(reason="Server TTL behavior — agent may linger after disconnect")
234
+ async def test_langgraph_disconnect_removes_agent():
235
+ """After close(), the agent should no longer appear in the agents list."""
236
+ config = _e2e_config()
237
+ agent_name = _unique_name("lg-disc")
238
+ observer_name = _unique_name("lg-obs")
239
+
240
+ relay = Relay(agent_name, config)
241
+ observer_relay = Relay(observer_name, config)
242
+ try:
243
+ await relay._ensure_connected()
244
+ observer_tools = make_relay_tools(observer_relay)
245
+
246
+ # Verify agent is listed
247
+ agents_before = await observer_tools["relay_agents"]()
248
+ assert agent_name in agents_before
249
+
250
+ # Disconnect
251
+ await relay.close()
252
+ await asyncio.sleep(2)
253
+
254
+ # Agent should be gone (xfail: server TTL may keep it)
255
+ agents_after = await observer_tools["relay_agents"]()
256
+ assert agent_name not in agents_after
257
+ finally:
258
+ try:
259
+ await relay.close()
260
+ except Exception:
261
+ pass
262
+ await observer_relay.close()