agent-relay 3.2.3 → 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 (225) hide show
  1. package/dist/index.cjs +265 -108
  2. package/package.json +11 -10
  3. package/packages/acp-bridge/package.json +2 -2
  4. package/packages/config/package.json +1 -1
  5. package/packages/hooks/package.json +4 -4
  6. package/packages/memory/package.json +2 -2
  7. package/packages/openclaw/package.json +2 -2
  8. package/packages/policy/package.json +2 -2
  9. package/packages/sdk/ADAPTER_REVIEW.md +109 -0
  10. package/packages/sdk/dist/communicate/a2a-bridge.d.ts +25 -0
  11. package/packages/sdk/dist/communicate/a2a-bridge.d.ts.map +1 -0
  12. package/packages/sdk/dist/communicate/a2a-bridge.js +89 -0
  13. package/packages/sdk/dist/communicate/a2a-bridge.js.map +1 -0
  14. package/packages/sdk/dist/communicate/a2a-server.d.ts +31 -0
  15. package/packages/sdk/dist/communicate/a2a-server.d.ts.map +1 -0
  16. package/packages/sdk/dist/communicate/a2a-server.js +220 -0
  17. package/packages/sdk/dist/communicate/a2a-server.js.map +1 -0
  18. package/packages/sdk/dist/communicate/a2a-transport.d.ts +48 -0
  19. package/packages/sdk/dist/communicate/a2a-transport.d.ts.map +1 -0
  20. package/packages/sdk/dist/communicate/a2a-transport.js +302 -0
  21. package/packages/sdk/dist/communicate/a2a-transport.js.map +1 -0
  22. package/packages/sdk/dist/communicate/a2a-types.d.ts +107 -0
  23. package/packages/sdk/dist/communicate/a2a-types.d.ts.map +1 -0
  24. package/packages/sdk/dist/communicate/a2a-types.js +209 -0
  25. package/packages/sdk/dist/communicate/a2a-types.js.map +1 -0
  26. package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts +28 -0
  27. package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts.map +1 -0
  28. package/packages/sdk/dist/communicate/adapters/claude-sdk.js +47 -0
  29. package/packages/sdk/dist/communicate/adapters/claude-sdk.js.map +1 -0
  30. package/packages/sdk/dist/communicate/adapters/crewai.d.ts +42 -0
  31. package/packages/sdk/dist/communicate/adapters/crewai.d.ts.map +1 -0
  32. package/packages/sdk/dist/communicate/adapters/crewai.js +95 -0
  33. package/packages/sdk/dist/communicate/adapters/crewai.js.map +1 -0
  34. package/packages/sdk/dist/communicate/adapters/google-adk.d.ts +53 -0
  35. package/packages/sdk/dist/communicate/adapters/google-adk.d.ts.map +1 -0
  36. package/packages/sdk/dist/communicate/adapters/google-adk.js +77 -0
  37. package/packages/sdk/dist/communicate/adapters/google-adk.js.map +1 -0
  38. package/packages/sdk/dist/communicate/adapters/index.d.ts +7 -0
  39. package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -0
  40. package/packages/sdk/dist/communicate/adapters/index.js +7 -0
  41. package/packages/sdk/dist/communicate/adapters/index.js.map +1 -0
  42. package/packages/sdk/dist/communicate/adapters/langgraph.d.ts +40 -0
  43. package/packages/sdk/dist/communicate/adapters/langgraph.d.ts.map +1 -0
  44. package/packages/sdk/dist/communicate/adapters/langgraph.js +77 -0
  45. package/packages/sdk/dist/communicate/adapters/langgraph.js.map +1 -0
  46. package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts +25 -0
  47. package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts.map +1 -0
  48. package/packages/sdk/dist/communicate/adapters/openai-agents.js +70 -0
  49. package/packages/sdk/dist/communicate/adapters/openai-agents.js.map +1 -0
  50. package/packages/sdk/dist/communicate/adapters/pi.d.ts +45 -0
  51. package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -0
  52. package/packages/sdk/dist/communicate/adapters/pi.js +59 -0
  53. package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -0
  54. package/packages/sdk/dist/communicate/core.d.ts +58 -0
  55. package/packages/sdk/dist/communicate/core.d.ts.map +1 -0
  56. package/packages/sdk/dist/communicate/core.js +128 -0
  57. package/packages/sdk/dist/communicate/core.js.map +1 -0
  58. package/packages/sdk/dist/communicate/index.d.ts +4 -0
  59. package/packages/sdk/dist/communicate/index.d.ts.map +1 -0
  60. package/packages/sdk/dist/communicate/index.js +4 -0
  61. package/packages/sdk/dist/communicate/index.js.map +1 -0
  62. package/packages/sdk/dist/communicate/transport.d.ts +36 -0
  63. package/packages/sdk/dist/communicate/transport.d.ts.map +1 -0
  64. package/packages/sdk/dist/communicate/transport.js +371 -0
  65. package/packages/sdk/dist/communicate/transport.js.map +1 -0
  66. package/packages/sdk/dist/communicate/types.d.ts +58 -0
  67. package/packages/sdk/dist/communicate/types.d.ts.map +1 -0
  68. package/packages/sdk/dist/communicate/types.js +66 -0
  69. package/packages/sdk/dist/communicate/types.js.map +1 -0
  70. package/packages/sdk/dist/workflows/builder.d.ts +35 -5
  71. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  72. package/packages/sdk/dist/workflows/builder.js +81 -7
  73. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  74. package/packages/sdk/dist/workflows/cli.js +14 -1
  75. package/packages/sdk/dist/workflows/cli.js.map +1 -1
  76. package/packages/sdk/dist/workflows/runner.d.ts +10 -2
  77. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  78. package/packages/sdk/dist/workflows/runner.js +95 -1
  79. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  80. package/packages/sdk/dist/workflows/types.d.ts +11 -0
  81. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  82. package/packages/sdk/examples/communicate/claude_sdk_example.ts +5 -0
  83. package/packages/sdk/examples/communicate/pi_example.ts +8 -0
  84. package/packages/sdk/package.json +48 -2
  85. package/packages/sdk/src/__tests__/builder-deterministic.test.ts +132 -0
  86. package/packages/sdk/src/__tests__/communicate/a2a-bridge.test.ts +211 -0
  87. package/packages/sdk/src/__tests__/communicate/a2a-server.test.ts +359 -0
  88. package/packages/sdk/src/__tests__/communicate/a2a-transport.test.ts +537 -0
  89. package/packages/sdk/src/__tests__/communicate/a2a-types.test.ts +297 -0
  90. package/packages/sdk/src/__tests__/communicate/adapters/claude-sdk.test.ts +163 -0
  91. package/packages/sdk/src/__tests__/communicate/adapters/crewai.test.ts +219 -0
  92. package/packages/sdk/src/__tests__/communicate/adapters/e2e-crewai.test.ts +101 -0
  93. package/packages/sdk/src/__tests__/communicate/adapters/e2e-google-adk.test.ts +166 -0
  94. package/packages/sdk/src/__tests__/communicate/adapters/e2e-langgraph.test.ts +181 -0
  95. package/packages/sdk/src/__tests__/communicate/adapters/e2e-openai-agents.test.ts +137 -0
  96. package/packages/sdk/src/__tests__/communicate/adapters/e2e-pi.test.ts +140 -0
  97. package/packages/sdk/src/__tests__/communicate/adapters/google-adk.test.ts +200 -0
  98. package/packages/sdk/src/__tests__/communicate/adapters/langgraph.test.ts +162 -0
  99. package/packages/sdk/src/__tests__/communicate/adapters/openai-agents.test.ts +166 -0
  100. package/packages/sdk/src/__tests__/communicate/adapters/pi.test.ts +140 -0
  101. package/packages/sdk/src/__tests__/communicate/core.test.ts +574 -0
  102. package/packages/sdk/src/__tests__/communicate/integration/cross-framework.test.ts +353 -0
  103. package/packages/sdk/src/__tests__/communicate/transport.test.ts +613 -0
  104. package/packages/sdk/src/__tests__/start-from.test.ts +346 -0
  105. package/packages/sdk/src/communicate/a2a-bridge.ts +111 -0
  106. package/packages/sdk/src/communicate/a2a-server.ts +277 -0
  107. package/packages/sdk/src/communicate/a2a-transport.ts +395 -0
  108. package/packages/sdk/src/communicate/a2a-types.ts +338 -0
  109. package/packages/sdk/src/communicate/adapters/claude-sdk.ts +85 -0
  110. package/packages/sdk/src/communicate/adapters/crewai.ts +141 -0
  111. package/packages/sdk/src/communicate/adapters/google-adk.ts +139 -0
  112. package/packages/sdk/src/communicate/adapters/index.ts +6 -0
  113. package/packages/sdk/src/communicate/adapters/langgraph.ts +112 -0
  114. package/packages/sdk/src/communicate/adapters/openai-agents.ts +113 -0
  115. package/packages/sdk/src/communicate/adapters/pi.ts +105 -0
  116. package/packages/sdk/src/communicate/core.ts +157 -0
  117. package/packages/sdk/src/communicate/index.ts +3 -0
  118. package/packages/sdk/src/communicate/transport.ts +489 -0
  119. package/packages/sdk/src/communicate/types.ts +106 -0
  120. package/packages/sdk/src/examples/workflows/fix-dashboard-user-registration.yaml +182 -0
  121. package/packages/sdk/src/workflows/builder.ts +97 -9
  122. package/packages/sdk/src/workflows/cli.ts +16 -1
  123. package/packages/sdk/src/workflows/runner.ts +110 -1
  124. package/packages/sdk/src/workflows/types.ts +14 -0
  125. package/packages/sdk/tsconfig.build.json +1 -7
  126. package/packages/sdk/tsconfig.json +1 -7
  127. package/packages/sdk-py/README.md +67 -25
  128. package/packages/sdk-py/examples/communicate/agno_example.py +8 -0
  129. package/packages/sdk-py/examples/communicate/claude_sdk_example.py +6 -0
  130. package/packages/sdk-py/examples/communicate/crewai_example.py +7 -0
  131. package/packages/sdk-py/examples/communicate/google_adk_example.py +7 -0
  132. package/packages/sdk-py/examples/communicate/openai_agents_example.py +8 -0
  133. package/packages/sdk-py/examples/communicate/swarms_example.py +7 -0
  134. package/packages/sdk-py/pyproject.toml +12 -1
  135. package/packages/sdk-py/src/agent_relay/__init__.py +8 -0
  136. package/packages/sdk-py/src/agent_relay/builder.py +65 -26
  137. package/packages/sdk-py/src/agent_relay/communicate/__init__.py +6 -0
  138. package/packages/sdk-py/src/agent_relay/communicate/a2a_bridge.py +138 -0
  139. package/packages/sdk-py/src/agent_relay/communicate/a2a_server.py +242 -0
  140. package/packages/sdk-py/src/agent_relay/communicate/a2a_transport.py +366 -0
  141. package/packages/sdk-py/src/agent_relay/communicate/a2a_types.py +294 -0
  142. package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +10 -0
  143. package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +74 -0
  144. package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +78 -0
  145. package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +143 -0
  146. package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +69 -0
  147. package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +86 -0
  148. package/packages/sdk-py/src/agent_relay/communicate/adapters/pi.py +175 -0
  149. package/packages/sdk-py/src/agent_relay/communicate/adapters/swarms.py +44 -0
  150. package/packages/sdk-py/src/agent_relay/communicate/core.py +293 -0
  151. package/packages/sdk-py/src/agent_relay/communicate/transport.py +502 -0
  152. package/packages/sdk-py/src/agent_relay/communicate/types.py +89 -0
  153. package/packages/sdk-py/src/agent_relay/types.py +2 -1
  154. package/packages/sdk-py/tests/communicate/__init__.py +0 -0
  155. package/packages/sdk-py/tests/communicate/adapters/__init__.py +0 -0
  156. package/packages/sdk-py/tests/communicate/adapters/e2e_test_agno.py +154 -0
  157. package/packages/sdk-py/tests/communicate/adapters/e2e_test_claude_sdk.py +428 -0
  158. package/packages/sdk-py/tests/communicate/adapters/e2e_test_crewai.py +234 -0
  159. package/packages/sdk-py/tests/communicate/adapters/e2e_test_google_adk.py +182 -0
  160. package/packages/sdk-py/tests/communicate/adapters/e2e_test_langgraph.py +262 -0
  161. package/packages/sdk-py/tests/communicate/adapters/e2e_test_openai_agents.py +88 -0
  162. package/packages/sdk-py/tests/communicate/adapters/e2e_test_pi.py +156 -0
  163. package/packages/sdk-py/tests/communicate/adapters/e2e_test_swarms.py +239 -0
  164. package/packages/sdk-py/tests/communicate/adapters/test_agno.py +140 -0
  165. package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +147 -0
  166. package/packages/sdk-py/tests/communicate/adapters/test_crewai.py +136 -0
  167. package/packages/sdk-py/tests/communicate/adapters/test_google_adk.py +125 -0
  168. package/packages/sdk-py/tests/communicate/adapters/test_openai_agents.py +99 -0
  169. package/packages/sdk-py/tests/communicate/adapters/test_pi.py +270 -0
  170. package/packages/sdk-py/tests/communicate/adapters/test_swarms.py +113 -0
  171. package/packages/sdk-py/tests/communicate/conftest.py +555 -0
  172. package/packages/sdk-py/tests/communicate/integration/__init__.py +1 -0
  173. package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +331 -0
  174. package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +151 -0
  175. package/packages/sdk-py/tests/communicate/test_a2a_bridge.py +363 -0
  176. package/packages/sdk-py/tests/communicate/test_a2a_server.py +346 -0
  177. package/packages/sdk-py/tests/communicate/test_a2a_transport.py +561 -0
  178. package/packages/sdk-py/tests/communicate/test_a2a_types.py +342 -0
  179. package/packages/sdk-py/tests/communicate/test_auto_detect.py +67 -0
  180. package/packages/sdk-py/tests/communicate/test_core.py +331 -0
  181. package/packages/sdk-py/tests/communicate/test_transport.py +373 -0
  182. package/packages/sdk-py/tests/communicate/test_types.py +285 -0
  183. package/packages/sdk-py/tests/test_builder_deterministic.py +118 -0
  184. package/packages/telemetry/package.json +1 -1
  185. package/packages/trajectory/package.json +2 -2
  186. package/packages/user-directory/package.json +2 -2
  187. package/packages/utils/package.json +2 -2
  188. package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts +0 -14
  189. package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts.map +0 -1
  190. package/packages/sdk/dist/__tests__/completion-pipeline.test.js +0 -1476
  191. package/packages/sdk/dist/__tests__/completion-pipeline.test.js.map +0 -1
  192. package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts +0 -2
  193. package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts.map +0 -1
  194. package/packages/sdk/dist/__tests__/contract-fixtures.test.js +0 -152
  195. package/packages/sdk/dist/__tests__/contract-fixtures.test.js.map +0 -1
  196. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts +0 -16
  197. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts.map +0 -1
  198. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +0 -640
  199. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +0 -1
  200. package/packages/sdk/dist/__tests__/facade.test.d.ts +0 -2
  201. package/packages/sdk/dist/__tests__/facade.test.d.ts.map +0 -1
  202. package/packages/sdk/dist/__tests__/facade.test.js +0 -305
  203. package/packages/sdk/dist/__tests__/facade.test.js.map +0 -1
  204. package/packages/sdk/dist/__tests__/integration.test.d.ts +0 -2
  205. package/packages/sdk/dist/__tests__/integration.test.d.ts.map +0 -1
  206. package/packages/sdk/dist/__tests__/integration.test.js +0 -205
  207. package/packages/sdk/dist/__tests__/integration.test.js.map +0 -1
  208. package/packages/sdk/dist/__tests__/pty.test.d.ts +0 -2
  209. package/packages/sdk/dist/__tests__/pty.test.d.ts.map +0 -1
  210. package/packages/sdk/dist/__tests__/pty.test.js +0 -20
  211. package/packages/sdk/dist/__tests__/pty.test.js.map +0 -1
  212. package/packages/sdk/dist/__tests__/quickstart.test.d.ts +0 -2
  213. package/packages/sdk/dist/__tests__/quickstart.test.d.ts.map +0 -1
  214. package/packages/sdk/dist/__tests__/quickstart.test.js +0 -176
  215. package/packages/sdk/dist/__tests__/quickstart.test.js.map +0 -1
  216. package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts +0 -2
  217. package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts.map +0 -1
  218. package/packages/sdk/dist/__tests__/spawn-from-env.test.js +0 -222
  219. package/packages/sdk/dist/__tests__/spawn-from-env.test.js.map +0 -1
  220. package/packages/sdk/dist/__tests__/unit.test.d.ts +0 -2
  221. package/packages/sdk/dist/__tests__/unit.test.d.ts.map +0 -1
  222. package/packages/sdk/dist/__tests__/unit.test.js +0 -357
  223. package/packages/sdk/dist/__tests__/unit.test.js.map +0 -1
  224. package/packages/sdk-py/agent_relay/__init__.py +0 -21
  225. package/packages/sdk-py/agent_relay/models.py +0 -398
@@ -0,0 +1,331 @@
1
+ """Wave 4.1 cross-framework integration tests for communicate mode."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import importlib
7
+ import sys
8
+ from types import ModuleType, SimpleNamespace
9
+ from unittest.mock import MagicMock
10
+
11
+ import pytest
12
+
13
+ from agent_relay.communicate.core import Relay
14
+
15
+
16
+ def _reload_module(module_name: str):
17
+ if module_name in sys.modules:
18
+ return importlib.reload(sys.modules[module_name])
19
+ return importlib.import_module(module_name)
20
+
21
+
22
+ def _load_openai_adapter(monkeypatch):
23
+ agents_module = ModuleType("agents")
24
+ agents_module.Agent = type("Agent", (), {})
25
+ agents_module.function_tool = lambda fn: fn
26
+ monkeypatch.setitem(sys.modules, "agents", agents_module)
27
+ return _reload_module("agent_relay.communicate.adapters.openai_agents")
28
+
29
+
30
+ def _install_google_modules(monkeypatch):
31
+ google_module = ModuleType("google")
32
+ google_adk_module = ModuleType("google.adk")
33
+ google_adk_agents_module = ModuleType("google.adk.agents")
34
+ google_genai_module = ModuleType("google.genai")
35
+ google_genai_types_module = ModuleType("google.genai.types")
36
+
37
+ class Part:
38
+ def __init__(self, text: str):
39
+ self.text = text
40
+
41
+ def __repr__(self) -> str:
42
+ return f"Part(text={self.text!r})"
43
+
44
+ class Content:
45
+ def __init__(self, role: str, parts: list[Part]):
46
+ self.role = role
47
+ self.parts = parts
48
+
49
+ def __repr__(self) -> str:
50
+ return f"Content(role={self.role!r}, parts={self.parts!r})"
51
+
52
+ google_module.adk = google_adk_module
53
+ google_module.genai = google_genai_module
54
+ google_adk_module.agents = google_adk_agents_module
55
+ google_genai_module.types = google_genai_types_module
56
+ google_genai_types_module.Content = Content
57
+ google_genai_types_module.Part = Part
58
+
59
+ monkeypatch.setitem(sys.modules, "google", google_module)
60
+ monkeypatch.setitem(sys.modules, "google.adk", google_adk_module)
61
+ monkeypatch.setitem(sys.modules, "google.adk.agents", google_adk_agents_module)
62
+ monkeypatch.setitem(sys.modules, "google.genai", google_genai_module)
63
+ monkeypatch.setitem(sys.modules, "google.genai.types", google_genai_types_module)
64
+
65
+
66
+ def _load_google_adapter(monkeypatch):
67
+ _install_google_modules(monkeypatch)
68
+ return _reload_module("agent_relay.communicate.adapters.google_adk")
69
+
70
+
71
+ def _load_swarms_adapter():
72
+ return _reload_module("agent_relay.communicate.adapters.swarms")
73
+
74
+
75
+ def _load_claude_adapter(monkeypatch):
76
+ claude_agent_sdk = ModuleType("claude_agent_sdk")
77
+ claude_types = ModuleType("claude_agent_sdk.types")
78
+
79
+ class HookResult:
80
+ def __init__(self, system_message=None, should_continue=False):
81
+ self.system_message = system_message
82
+ self.should_continue = should_continue
83
+
84
+ claude_types.HookResult = HookResult
85
+ claude_agent_sdk.types = claude_types
86
+
87
+ monkeypatch.setitem(sys.modules, "claude_agent_sdk", claude_agent_sdk)
88
+ monkeypatch.setitem(sys.modules, "claude_agent_sdk.types", claude_types)
89
+ return _reload_module("agent_relay.communicate.adapters.claude_sdk")
90
+
91
+
92
+ def _tool(agent, name: str):
93
+ for candidate in agent.tools:
94
+ tool_name = getattr(candidate, "__name__", None) or getattr(candidate, "name", None)
95
+ if tool_name == name:
96
+ return candidate
97
+ raise AssertionError(f"Tool {name!r} was not registered.")
98
+
99
+
100
+ async def _wait_for_google_message(callback, text: str, *, timeout: float = 1.0) -> list[object]:
101
+ deadline = asyncio.get_running_loop().time() + timeout
102
+ last_contents: list[object] = []
103
+
104
+ while asyncio.get_running_loop().time() < deadline:
105
+ request = SimpleNamespace(contents=[])
106
+ await callback(request)
107
+ last_contents = list(request.contents)
108
+ if any(text in str(part) for part in request.contents):
109
+ return request.contents
110
+ await asyncio.sleep(0.01)
111
+
112
+ raise AssertionError(
113
+ f"Timed out waiting for Google ADK callback delivery of {text!r}. Last contents: {last_contents!r}"
114
+ )
115
+
116
+
117
+ async def _wait_for_claude_system_message(hook, expected: list[str], *, timeout: float = 1.0) -> str:
118
+ deadline = asyncio.get_running_loop().time() + timeout
119
+ last_system_message = None
120
+
121
+ while asyncio.get_running_loop().time() < deadline:
122
+ result = await hook()
123
+ last_system_message = getattr(result, "system_message", None)
124
+ if last_system_message and all(fragment in last_system_message for fragment in expected):
125
+ return last_system_message
126
+ await asyncio.sleep(0.01)
127
+
128
+ raise AssertionError(
129
+ "Timed out waiting for Claude SDK hook delivery. "
130
+ f"Expected fragments: {expected!r}. Last system_message: {last_system_message!r}"
131
+ )
132
+
133
+
134
+ async def _wait_for_inbox_tool(tool, expected: list[str], *, timeout: float = 1.0) -> str:
135
+ deadline = asyncio.get_running_loop().time() + timeout
136
+ last_result = ""
137
+
138
+ while asyncio.get_running_loop().time() < deadline:
139
+ last_result = await tool()
140
+ if all(fragment in last_result for fragment in expected):
141
+ return last_result
142
+ await asyncio.sleep(0.01)
143
+
144
+ raise AssertionError(
145
+ f"Timed out waiting for inbox delivery. Expected fragments: {expected!r}. Last result: {last_result!r}"
146
+ )
147
+
148
+
149
+ async def _prime_google_receiver(relay_server, relay: Relay, callback) -> None:
150
+ await callback(SimpleNamespace(contents=[]))
151
+ assert relay.transport.agent_id is not None
152
+ await relay_server.wait_for_ws_connections(relay.transport.agent_id, count=1)
153
+
154
+
155
+ async def _prime_claude_receiver(relay_server, relay: Relay, hook) -> None:
156
+ await hook()
157
+ assert relay.transport.agent_id is not None
158
+ await relay_server.wait_for_ws_connections(relay.transport.agent_id, count=1)
159
+
160
+
161
+ async def _prime_inbox_receiver(relay_server, relay: Relay, inbox_tool) -> None:
162
+ await inbox_tool()
163
+ assert relay.transport.agent_id is not None
164
+ await relay_server.wait_for_ws_connections(relay.transport.agent_id, count=1)
165
+
166
+
167
+ async def _close_relays(*relays: Relay) -> None:
168
+ await asyncio.gather(*(relay.close() for relay in relays), return_exceptions=True)
169
+
170
+
171
+ @pytest.mark.asyncio
172
+ async def test_openai_sender_reaches_google_adk_before_model_callback(relay_server, monkeypatch):
173
+ openai_adapter = _load_openai_adapter(monkeypatch)
174
+ google_adapter = _load_google_adapter(monkeypatch)
175
+
176
+ sender_relay = Relay("OpenAISender", relay_server.make_config(auto_cleanup=False))
177
+ receiver_relay = Relay("GoogleReceiver", relay_server.make_config(auto_cleanup=False))
178
+
179
+ sender_agent = SimpleNamespace(tools=[], instructions="Send relay updates.")
180
+ receiver_agent = SimpleNamespace(tools=[], before_model_callback=None)
181
+
182
+ openai_adapter.on_relay(sender_agent, sender_relay)
183
+ google_adapter.on_relay(receiver_agent, receiver_relay)
184
+
185
+ try:
186
+ await _prime_google_receiver(
187
+ relay_server,
188
+ receiver_relay,
189
+ receiver_agent.before_model_callback,
190
+ )
191
+
192
+ await _tool(sender_agent, "relay_send")("GoogleReceiver", "handoff complete")
193
+ contents = await _wait_for_google_message(
194
+ receiver_agent.before_model_callback,
195
+ "handoff complete",
196
+ )
197
+ finally:
198
+ await _close_relays(sender_relay, receiver_relay)
199
+
200
+ assert any("[Relay] OpenAISender: handoff complete" in str(part) for part in contents)
201
+ assert any("handoff complete" in str(part) for part in contents)
202
+
203
+
204
+ @pytest.mark.asyncio
205
+ async def test_swarms_sender_reaches_claude_sdk_hook_system_message(relay_server, monkeypatch):
206
+ swarms_adapter = _load_swarms_adapter()
207
+ claude_adapter = _load_claude_adapter(monkeypatch)
208
+
209
+ sender_relay = Relay("SwarmsSender", relay_server.make_config(auto_cleanup=False))
210
+ receiver_relay = Relay("ClaudeReceiver", relay_server.make_config(auto_cleanup=False))
211
+
212
+ sender_agent = SimpleNamespace(tools=[], receive_message=MagicMock())
213
+
214
+ class Hooks:
215
+ post_tool_use = None
216
+ stop = None
217
+
218
+ receiver_options = SimpleNamespace(mcp_servers=[], hooks=Hooks())
219
+
220
+ swarms_adapter.on_relay(sender_agent, sender_relay)
221
+ claude_adapter.on_relay("ClaudeReceiver", receiver_options, relay=receiver_relay)
222
+
223
+ try:
224
+ await _prime_claude_receiver(
225
+ relay_server,
226
+ receiver_relay,
227
+ receiver_options.hooks.post_tool_use,
228
+ )
229
+
230
+ await _tool(sender_agent, "relay_send")("ClaudeReceiver", "ready for review")
231
+ system_message = await _wait_for_claude_system_message(
232
+ receiver_options.hooks.post_tool_use,
233
+ ["Relay message from SwarmsSender", "ready for review"],
234
+ )
235
+ finally:
236
+ await _close_relays(sender_relay, receiver_relay)
237
+
238
+ assert "Relay message from SwarmsSender" in system_message
239
+ assert "ready for review" in system_message
240
+
241
+
242
+ @pytest.mark.asyncio
243
+ async def test_multiple_framework_agents_post_to_the_same_channel(relay_server, monkeypatch):
244
+ openai_adapter = _load_openai_adapter(monkeypatch)
245
+ google_adapter = _load_google_adapter(monkeypatch)
246
+ swarms_adapter = _load_swarms_adapter()
247
+ claude_adapter = _load_claude_adapter(monkeypatch)
248
+
249
+ config = relay_server.make_config(auto_cleanup=False, channels=["integration-room"])
250
+
251
+ openai_relay = Relay("OpenAIPoster", config)
252
+ google_relay = Relay("GooglePoster", config)
253
+ swarms_relay = Relay("SwarmsPoster", config)
254
+ receiver_relay = Relay("ClaudeChannelReader", config)
255
+
256
+ openai_agent = SimpleNamespace(tools=[], instructions="Post relay updates.")
257
+ google_agent = SimpleNamespace(tools=[], before_model_callback=None)
258
+ swarms_agent = SimpleNamespace(tools=[], receive_message=MagicMock())
259
+
260
+ class Hooks:
261
+ post_tool_use = None
262
+ stop = None
263
+
264
+ receiver_options = SimpleNamespace(mcp_servers=[], hooks=Hooks())
265
+
266
+ openai_adapter.on_relay(openai_agent, openai_relay)
267
+ google_adapter.on_relay(google_agent, google_relay)
268
+ swarms_adapter.on_relay(swarms_agent, swarms_relay)
269
+ claude_adapter.on_relay("ClaudeChannelReader", receiver_options, relay=receiver_relay)
270
+
271
+ try:
272
+ await _prime_claude_receiver(
273
+ relay_server,
274
+ receiver_relay,
275
+ receiver_options.hooks.post_tool_use,
276
+ )
277
+
278
+ await _tool(openai_agent, "relay_post")("integration-room", "openai update")
279
+ await _tool(google_agent, "relay_post")("integration-room", "google update")
280
+ await _tool(swarms_agent, "relay_post")("integration-room", "swarms update")
281
+
282
+ system_message = await _wait_for_claude_system_message(
283
+ receiver_options.hooks.post_tool_use,
284
+ [
285
+ "Relay message from OpenAIPoster",
286
+ "openai update",
287
+ "Relay message from GooglePoster",
288
+ "google update",
289
+ "Relay message from SwarmsPoster",
290
+ "swarms update",
291
+ ],
292
+ )
293
+ finally:
294
+ await _close_relays(openai_relay, google_relay, swarms_relay, receiver_relay)
295
+
296
+ assert "Relay message from OpenAIPoster" in system_message
297
+ assert "Relay message from GooglePoster" in system_message
298
+ assert "Relay message from SwarmsPoster" in system_message
299
+
300
+
301
+ @pytest.mark.asyncio
302
+ async def test_cross_framework_dm_is_available_via_receiver_inbox_tool(relay_server, monkeypatch):
303
+ google_adapter = _load_google_adapter(monkeypatch)
304
+ openai_adapter = _load_openai_adapter(monkeypatch)
305
+
306
+ sender_relay = Relay("GoogleSender", relay_server.make_config(auto_cleanup=False))
307
+ receiver_relay = Relay("OpenAIReceiver", relay_server.make_config(auto_cleanup=False))
308
+
309
+ sender_agent = SimpleNamespace(tools=[], before_model_callback=None)
310
+ receiver_agent = SimpleNamespace(tools=[], instructions="Read relay inbox.")
311
+
312
+ google_adapter.on_relay(sender_agent, sender_relay)
313
+ openai_adapter.on_relay(receiver_agent, receiver_relay)
314
+
315
+ try:
316
+ await _prime_inbox_receiver(
317
+ relay_server,
318
+ receiver_relay,
319
+ _tool(receiver_agent, "relay_inbox"),
320
+ )
321
+
322
+ await _tool(sender_agent, "relay_send")("OpenAIReceiver", "dm via inbox")
323
+ inbox_result = await _wait_for_inbox_tool(
324
+ _tool(receiver_agent, "relay_inbox"),
325
+ ["GoogleSender", "dm via inbox"],
326
+ )
327
+ finally:
328
+ await _close_relays(sender_relay, receiver_relay)
329
+
330
+ assert "GoogleSender" in inbox_result
331
+ assert "dm via inbox" in inbox_result
@@ -0,0 +1,151 @@
1
+ """Wave 4.1 end-to-end integration tests against a real Relaycast server."""
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.transport import RelayTransport
13
+ from agent_relay.communicate.types import Message, RelayConfig
14
+
15
+ pytestmark = pytest.mark.skipif(
16
+ not os.environ.get("RELAY_E2E"),
17
+ reason="Set RELAY_E2E=1 for e2e tests",
18
+ )
19
+
20
+
21
+ def _e2e_config() -> RelayConfig:
22
+ config = RelayConfig.resolve(
23
+ workspace=os.environ.get("RELAY_WORKSPACE"),
24
+ api_key=os.environ.get("RELAY_API_KEY"),
25
+ base_url=os.environ.get("RELAY_BASE_URL"),
26
+ auto_cleanup=False,
27
+ )
28
+ if not config.workspace or not config.api_key:
29
+ pytest.fail("RELAY_WORKSPACE and RELAY_API_KEY must be set for e2e tests.")
30
+ return config
31
+
32
+
33
+ def _unique_name(prefix: str) -> str:
34
+ timestamp = int(time.time() * 1000)
35
+ return f"{prefix}-{timestamp}-{uuid.uuid4().hex[:8]}"
36
+
37
+
38
+ async def _safe_disconnect(transport: RelayTransport) -> None:
39
+ try:
40
+ await transport.disconnect()
41
+ except Exception:
42
+ pass
43
+
44
+
45
+ async def _wait_for_message(
46
+ transport: RelayTransport,
47
+ *,
48
+ sender: str,
49
+ text: str,
50
+ timeout: float = 15.0,
51
+ ) -> Message:
52
+ deadline = asyncio.get_running_loop().time() + timeout
53
+
54
+ while asyncio.get_running_loop().time() < deadline:
55
+ for message in await transport.check_inbox():
56
+ if message.sender == sender and message.text == text:
57
+ return message
58
+ await asyncio.sleep(0.25)
59
+
60
+ raise AssertionError(f"Timed out waiting for DM from {sender!r}: {text!r}")
61
+
62
+
63
+ async def _wait_for_agent_absent(
64
+ transport: RelayTransport,
65
+ agent_name: str,
66
+ *,
67
+ timeout: float = 30.0,
68
+ ) -> None:
69
+ """Wait for an agent to disappear from list_agents or go offline.
70
+
71
+ Relaycast presence may take a few seconds to propagate after disconnect.
72
+ We check both absence from the list and offline status.
73
+ """
74
+ deadline = asyncio.get_running_loop().time() + timeout
75
+
76
+ while asyncio.get_running_loop().time() < deadline:
77
+ agents_payload = await transport.send_http("GET", "/v1/agents")
78
+ data = agents_payload.get("data", agents_payload)
79
+ if isinstance(data, list):
80
+ matching = [a for a in data if isinstance(a, dict) and a.get("name") == agent_name]
81
+ if not matching:
82
+ return
83
+ # Also accept "offline" status
84
+ if all(a.get("status") == "offline" for a in matching):
85
+ return
86
+ elif agent_name not in await transport.list_agents():
87
+ return
88
+ await asyncio.sleep(0.5)
89
+
90
+ raise AssertionError(f"Timed out waiting for {agent_name!r} to go offline/absent from list_agents().")
91
+
92
+
93
+ @pytest.mark.asyncio
94
+ async def test_register_send_receive_inbox_and_unregister_round_trip():
95
+ config = _e2e_config()
96
+ sender = RelayTransport(_unique_name("sdk-py-e2e-sender"), config)
97
+ receiver = RelayTransport(_unique_name("sdk-py-e2e-receiver"), config)
98
+ probe = RelayTransport(_unique_name("sdk-py-e2e-probe"), config)
99
+ text = f"hello-{uuid.uuid4().hex}"
100
+
101
+ await asyncio.gather(sender.connect(), receiver.connect(), probe.connect())
102
+
103
+ try:
104
+ agents = await probe.list_agents()
105
+ assert sender.agent_name in agents
106
+ assert receiver.agent_name in agents
107
+
108
+ await sender.send_dm(receiver.agent_name, text)
109
+ message = await _wait_for_message(receiver, sender=sender.agent_name, text=text)
110
+ assert message.text == text
111
+ assert message.sender == sender.agent_name
112
+
113
+ # Verify disconnect completes without error
114
+ receiver_name = receiver.agent_name
115
+ await receiver.disconnect()
116
+
117
+ # Note: Relaycast presence updates are eventually consistent —
118
+ # agents may remain "online" in list_agents for a heartbeat window
119
+ # after disconnect. We verify the disconnect call succeeds rather
120
+ # than waiting for presence propagation.
121
+ finally:
122
+ await asyncio.gather(
123
+ _safe_disconnect(sender),
124
+ _safe_disconnect(receiver),
125
+ _safe_disconnect(probe),
126
+ )
127
+
128
+
129
+ @pytest.mark.asyncio
130
+ async def test_two_agents_can_exchange_bidirectional_messages():
131
+ config = _e2e_config()
132
+ alpha = RelayTransport(_unique_name("sdk-py-e2e-alpha"), config)
133
+ beta = RelayTransport(_unique_name("sdk-py-e2e-beta"), config)
134
+
135
+ first_text = f"alpha-to-beta-{uuid.uuid4().hex}"
136
+ second_text = f"beta-to-alpha-{uuid.uuid4().hex}"
137
+
138
+ await asyncio.gather(alpha.connect(), beta.connect())
139
+
140
+ try:
141
+ await alpha.send_dm(beta.agent_name, first_text)
142
+ first = await _wait_for_message(beta, sender=alpha.agent_name, text=first_text)
143
+ assert first.sender == alpha.agent_name
144
+ assert first.text == first_text
145
+
146
+ await beta.send_dm(alpha.agent_name, second_text)
147
+ second = await _wait_for_message(alpha, sender=beta.agent_name, text=second_text)
148
+ assert second.sender == beta.agent_name
149
+ assert second.text == second_text
150
+ finally:
151
+ await asyncio.gather(_safe_disconnect(alpha), _safe_disconnect(beta))