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,86 @@
1
+ """OpenAI Agents adapter for on_relay()."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ from ..core import Relay
10
+
11
+
12
+ def _format_instructions_with_inbox(messages: list[Any], base_instructions: str) -> str:
13
+ content = "\n\nNew messages from other agents:\n"
14
+ for message in messages:
15
+ content += f" {message.sender}: {message.text}\n"
16
+ return f"{content}\n{base_instructions}" if base_instructions else content
17
+
18
+
19
+ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
20
+ """Wrap OpenAI Agent to connect it to the relay."""
21
+ if relay is None:
22
+ from ..core import Relay
23
+ relay = Relay(getattr(agent, "name", "Agent"))
24
+ try:
25
+ from agents import function_tool
26
+ except ImportError:
27
+ raise ImportError(
28
+ "on_relay() for OpenAI Agents requires the 'openai-agents' package. "
29
+ "Install it with: pip install openai-agents"
30
+ )
31
+
32
+ async def relay_send(to: str, text: str) -> str:
33
+ """Send a private message to another agent."""
34
+ await relay.send(to, text)
35
+ return "Message sent"
36
+
37
+ async def relay_inbox() -> str:
38
+ """Check for new messages in the inbox."""
39
+ messages = await relay.inbox()
40
+ if not messages: return "No new messages"
41
+ return "\n".join([f"From {m.sender}: {m.text}" for m in messages])
42
+
43
+ async def relay_post(channel: str, text: str) -> str:
44
+ """Post a message to a shared channel."""
45
+ await relay.post(channel, text)
46
+ return "Message posted"
47
+
48
+ async def relay_agents() -> str:
49
+ """List all agents currently on the relay."""
50
+ agents = await relay.agents()
51
+ return ", ".join(agents)
52
+
53
+ agent.tools.extend([
54
+ function_tool(relay_send),
55
+ function_tool(relay_inbox),
56
+ function_tool(relay_post),
57
+ function_tool(relay_agents)
58
+ ])
59
+
60
+ # 2. Wrap instructions with a local buffer so we don't starve relay_inbox tool
61
+ orig_instructions = agent.instructions
62
+ pending_messages: list[Any] = []
63
+
64
+ relay.on_message(lambda msg: pending_messages.append(msg))
65
+
66
+ async def instructions_wrapper(*args: Any, **kwargs: Any) -> str:
67
+ if callable(orig_instructions):
68
+ if inspect.iscoroutinefunction(orig_instructions):
69
+ base = await orig_instructions(*args, **kwargs)
70
+ else:
71
+ base = orig_instructions(*args, **kwargs)
72
+ if inspect.isawaitable(base):
73
+ base = await base
74
+ else:
75
+ base = orig_instructions
76
+
77
+ base = base or ""
78
+ if not pending_messages:
79
+ return base
80
+
81
+ messages = list(pending_messages)
82
+ pending_messages.clear()
83
+ return _format_instructions_with_inbox(messages, base)
84
+
85
+ agent.instructions = instructions_wrapper
86
+ return agent
@@ -0,0 +1,175 @@
1
+ """Pi RPC adapter for on_relay().
2
+
3
+ Spawns Pi (a TypeScript coding agent) as a subprocess in RPC mode and bridges
4
+ relay communication over its stdin/stdout JSONL protocol.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import subprocess
11
+ import threading
12
+ from typing import TYPE_CHECKING, Any, Callable
13
+
14
+ if TYPE_CHECKING:
15
+ from ..core import Relay
16
+
17
+ RELAY_TOOL_PREAMBLE = (
18
+ "You have access to the following relay tools for multi-agent communication:\n"
19
+ "- relay_send(to, text): Send a direct message to another relay agent.\n"
20
+ "- relay_inbox(): Drain and inspect newly received relay messages.\n"
21
+ "- relay_post(channel, text): Post a message to a relay channel.\n"
22
+ "- relay_agents(): List currently online relay agents.\n"
23
+ )
24
+
25
+
26
+ class PiRpcSession:
27
+ """Manages a Pi subprocess in RPC mode with relay integration."""
28
+
29
+ def __init__(
30
+ self,
31
+ proc: subprocess.Popen[str],
32
+ relay: "Relay",
33
+ ) -> None:
34
+ self._proc = proc
35
+ self._relay = relay
36
+ self._is_streaming = False
37
+ self._unsubscribe: Callable[[], None] | None = None
38
+ self._reader_thread: threading.Thread | None = None
39
+ self._event_callbacks: list[Callable[[dict[str, Any]], None]] = []
40
+ self._closed = False
41
+
42
+ @property
43
+ def is_streaming(self) -> bool:
44
+ return self._is_streaming
45
+
46
+ def send_command(self, command: dict[str, Any]) -> None:
47
+ """Send a JSONL command to Pi's stdin."""
48
+ if self._proc.stdin is None:
49
+ return
50
+ line = json.dumps(command) + "\n"
51
+ self._proc.stdin.write(line)
52
+ self._proc.stdin.flush()
53
+
54
+ def prompt(self, message: str, streaming_behavior: str | None = None) -> None:
55
+ cmd: dict[str, Any] = {"type": "prompt", "message": message}
56
+ if streaming_behavior:
57
+ cmd["streamingBehavior"] = streaming_behavior
58
+ self.send_command(cmd)
59
+
60
+ def steer(self, message: str) -> None:
61
+ self.send_command({"type": "prompt", "message": message, "streamingBehavior": "steer"})
62
+
63
+ def follow_up(self, message: str) -> None:
64
+ self.send_command({"type": "prompt", "message": message, "streamingBehavior": "followUp"})
65
+
66
+ def abort(self) -> None:
67
+ self.send_command({"type": "abort"})
68
+
69
+ def on_event(self, callback: Callable[[dict[str, Any]], None]) -> Callable[[], None]:
70
+ self._event_callbacks.append(callback)
71
+
72
+ def unsubscribe() -> None:
73
+ try:
74
+ self._event_callbacks.remove(callback)
75
+ except ValueError:
76
+ pass
77
+
78
+ return unsubscribe
79
+
80
+ def close(self) -> None:
81
+ if self._closed:
82
+ return
83
+ self._closed = True
84
+ if self._unsubscribe:
85
+ self._unsubscribe()
86
+ self._unsubscribe = None
87
+ if self._proc and self._proc.poll() is None:
88
+ self._proc.terminate()
89
+ try:
90
+ self._proc.wait(timeout=5)
91
+ except subprocess.TimeoutExpired:
92
+ self._proc.kill()
93
+
94
+
95
+ def _format_relay_message(message: Any) -> str:
96
+ location = f" [#{message.channel}]" if getattr(message, "channel", None) else ""
97
+ return f"Relay message from {message.sender}{location}: {message.text}"
98
+
99
+
100
+ def _start_reader(session: PiRpcSession) -> None:
101
+ def _read_stdout() -> None:
102
+ proc = session._proc
103
+ if proc.stdout is None:
104
+ return
105
+ for line in proc.stdout:
106
+ line = line.strip()
107
+ if not line:
108
+ continue
109
+ try:
110
+ event = json.loads(line)
111
+ except json.JSONDecodeError:
112
+ continue
113
+ event_type = event.get("type", "")
114
+ if event_type in ("agent_start", "turn_start"):
115
+ session._is_streaming = True
116
+ elif event_type in ("agent_end", "turn_end"):
117
+ session._is_streaming = False
118
+ for cb in list(session._event_callbacks):
119
+ cb(event)
120
+
121
+ thread = threading.Thread(target=_read_stdout, daemon=True)
122
+ thread.start()
123
+ session._reader_thread = thread
124
+
125
+
126
+ def on_relay(
127
+ name: str,
128
+ config: dict[str, Any] | None = None,
129
+ relay: "Relay | None" = None,
130
+ ) -> PiRpcSession:
131
+ """Spawn Pi in RPC mode and bridge relay communication.
132
+
133
+ Args:
134
+ name: Agent name for relay registration.
135
+ config: Optional dict with ``model``, ``provider``, or extra Pi CLI flags.
136
+ relay: Optional pre-configured Relay instance.
137
+
138
+ Returns:
139
+ A :class:`PiRpcSession` managing the subprocess and relay bridge.
140
+ """
141
+ if config is None:
142
+ config = {}
143
+ if relay is None:
144
+ from ..core import Relay
145
+
146
+ relay = Relay(name)
147
+
148
+ cmd = ["pi", "--mode", "rpc", "--no-session"]
149
+ if "model" in config:
150
+ cmd.extend(["--model", config["model"]])
151
+ if "provider" in config:
152
+ cmd.extend(["--provider", config["provider"]])
153
+
154
+ proc = subprocess.Popen(
155
+ cmd,
156
+ stdin=subprocess.PIPE,
157
+ stdout=subprocess.PIPE,
158
+ stderr=subprocess.PIPE,
159
+ text=True,
160
+ )
161
+
162
+ session = PiRpcSession(proc, relay)
163
+
164
+ def handle_relay_message(message: Any) -> None:
165
+ formatted = _format_relay_message(message)
166
+ if session.is_streaming:
167
+ session.steer(formatted)
168
+ else:
169
+ session.follow_up(formatted)
170
+
171
+ session._unsubscribe = relay.on_message(handle_relay_message)
172
+
173
+ _start_reader(session)
174
+
175
+ return session
@@ -0,0 +1,44 @@
1
+ """Swarms adapter for on_relay()."""
2
+
3
+ from __future__ import annotations
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from ..core import Relay
8
+
9
+ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
10
+ """Wrap Swarms Agent to connect it to the relay."""
11
+ if relay is None:
12
+ from ..core import Relay
13
+ relay = Relay(getattr(agent, "name", "Agent"))
14
+
15
+ # 1. Add tools (as callables)
16
+ async def relay_send(to: str, text: str) -> str:
17
+ """Send a private message to another agent."""
18
+ await relay.send(to, text)
19
+ return "Message sent"
20
+
21
+ async def relay_inbox() -> str:
22
+ """Check for new messages in the inbox."""
23
+ messages = await relay.inbox()
24
+ if not messages: return "No new messages"
25
+ return "\n".join([f"From {m.sender}: {m.text}" for m in messages])
26
+
27
+ async def relay_post(channel: str, text: str) -> str:
28
+ """Post a message to a shared channel."""
29
+ await relay.post(channel, text)
30
+ return "Message posted"
31
+
32
+ async def relay_agents() -> str:
33
+ """List all agents currently on the relay."""
34
+ agents = await relay.agents()
35
+ return ", ".join(agents)
36
+
37
+ agent.tools.extend([relay_send, relay_inbox, relay_post, relay_agents])
38
+
39
+ # 2. Receiving: bridge relay on_message to agent.receive_message
40
+ def _handle_relay_message(message: Any) -> None:
41
+ agent.receive_message(message.sender, message.text)
42
+
43
+ relay.on_message(_handle_relay_message)
44
+ return agent
@@ -0,0 +1,293 @@
1
+ """High-level Relay facade for communicate mode."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import asyncio
7
+ import threading
8
+ import warnings
9
+ from inspect import isawaitable
10
+ from typing import Any, Callable
11
+
12
+ from .transport import RelayTransport
13
+ from .types import Message, MessageCallback, RelayConfig, RelayConfigError, RelayAuthError
14
+
15
+ MAX_PENDING_MESSAGES = 10_000
16
+
17
+
18
+ class Relay:
19
+ """Relay client with buffered inbox access and callback subscriptions."""
20
+
21
+ def __init__(self, agent_name: str, config: RelayConfig | None = None) -> None:
22
+ self.agent_name = agent_name
23
+ self.config = config if config is not None else RelayConfig.resolve()
24
+ self.transport = RelayTransport(agent_name, self.config)
25
+
26
+ self._pending: list[Message] = []
27
+ self._callbacks: list[MessageCallback] = []
28
+ self._state_lock = threading.Lock()
29
+ self._connect_task: asyncio.Task[None] | None = None
30
+ self._connect_future: asyncio.Future[None] | None = None
31
+ self._connected = False
32
+ self._ws_connected = False
33
+ self._poll_task: asyncio.Task[None] | None = None
34
+
35
+ self.transport.on_ws_message(self._handle_transport_message)
36
+
37
+ if self.config.auto_cleanup:
38
+ atexit.register(self.close_sync)
39
+
40
+ async def send(self, to: str, text: str) -> None:
41
+ await self._ensure_connected()
42
+ await self.transport.send_dm(to, text)
43
+
44
+ async def post(self, channel: str, text: str) -> None:
45
+ await self._ensure_connected()
46
+ await self.transport.post_message(channel, text)
47
+
48
+ async def reply(self, message_id: str, text: str) -> None:
49
+ await self._ensure_connected()
50
+ await self.transport.reply(message_id, text)
51
+
52
+ async def inbox(self) -> list[Message]:
53
+ await self._ensure_connected()
54
+ if not self._ws_connected:
55
+ polled = await self.transport.check_inbox()
56
+ for msg in polled:
57
+ await self._handle_transport_message(msg)
58
+ with self._state_lock:
59
+ messages = list(self._pending)
60
+ self._pending.clear()
61
+ return messages
62
+
63
+ async def peek(self) -> list[Message]:
64
+ """Return buffered messages without draining them."""
65
+ await self._ensure_connected()
66
+ if not self._ws_connected:
67
+ polled = await self.transport.check_inbox()
68
+ for msg in polled:
69
+ await self._handle_transport_message(msg)
70
+ with self._state_lock:
71
+ return list(self._pending)
72
+
73
+ def on_message(self, callback: MessageCallback) -> Callable[[], None]:
74
+ with self._state_lock:
75
+ self._callbacks.append(callback)
76
+
77
+ self._schedule_connect()
78
+
79
+ def unsubscribe() -> None:
80
+ with self._state_lock:
81
+ try:
82
+ self._callbacks.remove(callback)
83
+ except ValueError:
84
+ pass
85
+
86
+ return unsubscribe
87
+
88
+
89
+ async def join(self, channel: str) -> None:
90
+ await self._ensure_connected()
91
+ await self.transport.join_channel(channel)
92
+
93
+ async def agents(self) -> list[str]:
94
+ await self._ensure_connected()
95
+ return await self.transport.list_agents()
96
+
97
+ async def close(self) -> None:
98
+ connect_task = self._connect_task
99
+ self._connect_task = None
100
+
101
+ if connect_task is not None and not connect_task.done():
102
+ connect_task.cancel()
103
+ try:
104
+ await connect_task
105
+ except asyncio.CancelledError:
106
+ pass
107
+
108
+ poll_task = self._poll_task
109
+ self._poll_task = None
110
+ if poll_task is not None and not poll_task.done():
111
+ poll_task.cancel()
112
+ try:
113
+ await poll_task
114
+ except asyncio.CancelledError:
115
+ pass
116
+
117
+ await self.transport.disconnect()
118
+ self._connected = False
119
+ self._ws_connected = False
120
+
121
+ def send_sync(self, to: str, text: str) -> None:
122
+ return self._run_sync(self.send(to, text))
123
+
124
+ def post_sync(self, channel: str, text: str) -> None:
125
+ return self._run_sync(self.post(channel, text))
126
+
127
+ def inbox_sync(self) -> list[Message]:
128
+ return self._run_sync(self.inbox())
129
+
130
+ def agents_sync(self) -> list[str]:
131
+ return self._run_sync(self.agents())
132
+
133
+ def close_sync(self) -> None:
134
+ return self._run_sync(self.close())
135
+
136
+ async def __aenter__(self) -> "Relay":
137
+ return self
138
+
139
+ async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
140
+ await self.close()
141
+
142
+ async def _ensure_connected(self) -> None:
143
+ if self._connected:
144
+ return
145
+
146
+ # Deduplicate concurrent callers — reuse in-flight connect
147
+ if self._connect_future is not None and not self._connect_future.done():
148
+ await self._connect_future
149
+ return
150
+
151
+ current_task = asyncio.current_task()
152
+ connect_task = self._connect_task
153
+ if connect_task is not None and connect_task is not current_task and not connect_task.done():
154
+ await connect_task
155
+ return
156
+
157
+ loop = asyncio.get_running_loop()
158
+ self._connect_future = loop.create_future()
159
+ try:
160
+ try:
161
+ await self.transport.connect()
162
+ self._ws_connected = True
163
+ except Exception:
164
+ # WebSocket failed — register agent via HTTP and fall back to polling
165
+ await self.transport.register_agent()
166
+ self._ws_connected = False
167
+ self._start_poll_loop()
168
+
169
+ from contextlib import suppress
170
+ for ch in self.config.channels:
171
+ with suppress(Exception):
172
+ await self.transport.join_channel(ch)
173
+
174
+ self._connected = True
175
+ self._connect_future.set_result(None)
176
+ self._connect_future = None
177
+ except Exception as exc:
178
+ # Ensure future is always resolved so waiters don't hang
179
+ if not self._connect_future.done():
180
+ self._connect_future.set_exception(exc)
181
+ self._connect_future = None
182
+ raise
183
+
184
+ def _schedule_connect(self) -> None:
185
+ if self._connected:
186
+ return
187
+
188
+ if self._connect_task is not None and not self._connect_task.done():
189
+ return
190
+
191
+ try:
192
+ loop = asyncio.get_running_loop()
193
+ except RuntimeError:
194
+ self._run_sync(self._ensure_connected())
195
+ return
196
+
197
+ task = loop.create_task(self._ensure_connected())
198
+ self._connect_task = task
199
+ task.add_done_callback(self._clear_connect_task)
200
+
201
+ def _clear_connect_task(self, task: asyncio.Task[None]) -> None:
202
+ if self._connect_task is task:
203
+ self._connect_task = None
204
+
205
+ def _start_poll_loop(self) -> None:
206
+ if self._poll_task is not None and not self._poll_task.done():
207
+ return
208
+ try:
209
+ loop = asyncio.get_running_loop()
210
+ except RuntimeError:
211
+ return
212
+ self._poll_task = loop.create_task(self._poll_loop())
213
+
214
+ async def _poll_loop(self) -> None:
215
+ interval = self.config.poll_interval_ms / 1000.0
216
+ while self._connected and not self._ws_connected:
217
+ try:
218
+ messages = await self.transport.check_inbox()
219
+ for msg in messages:
220
+ await self._handle_transport_message(msg)
221
+ except asyncio.CancelledError:
222
+ raise
223
+ except Exception:
224
+ pass
225
+ await asyncio.sleep(interval)
226
+
227
+ async def _handle_transport_message(self, message: Message) -> None:
228
+ with self._state_lock:
229
+ callbacks = list(self._callbacks)
230
+ # Always buffer the message (spec: "both" case — callbacks AND inbox)
231
+ if len(self._pending) >= MAX_PENDING_MESSAGES:
232
+ self._pending.pop(0)
233
+ warnings.warn(
234
+ "Relay pending buffer exceeded 10,000 messages; dropping oldest message.",
235
+ UserWarning,
236
+ stacklevel=2,
237
+ )
238
+ self._pending.append(message)
239
+
240
+ for callback in callbacks:
241
+ result = callback(message)
242
+ if isawaitable(result):
243
+ await result
244
+
245
+ @staticmethod
246
+ def _run_sync(awaitable: Any) -> Any:
247
+ try:
248
+ asyncio.get_running_loop()
249
+ except RuntimeError:
250
+ return asyncio.run(awaitable)
251
+ # Running inside an active event loop — execute in a separate thread
252
+ import concurrent.futures
253
+
254
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
255
+ return pool.submit(asyncio.run, awaitable).result()
256
+
257
+
258
+ def on_relay(agent: Any, relay: Relay | None = None) -> Any:
259
+ """Auto-detect and apply the correct relay adapter for the given agent."""
260
+ if relay is None:
261
+ # Resolve a default relay if none provided
262
+ relay = Relay(getattr(agent, "name", "Agent"))
263
+
264
+ cls_module = type(agent).__module__
265
+ if cls_module.startswith("claude_agent_sdk"):
266
+ agent_name = getattr(agent, "name", "Agent")
267
+ from .adapters.claude_sdk import on_relay as _adapt
268
+ return _adapt(agent_name, agent, relay)
269
+ if cls_module.startswith("agents"):
270
+ from .adapters.openai_agents import on_relay as _adapt
271
+ return _adapt(agent, relay)
272
+ if cls_module.startswith("google.adk"):
273
+ from .adapters.google_adk import on_relay as _adapt
274
+ return _adapt(agent, relay)
275
+ if cls_module.startswith("agno"):
276
+ from .adapters.agno import on_relay as _adapt
277
+ return _adapt(agent, relay)
278
+ if cls_module.startswith("swarms"):
279
+ from .adapters.swarms import on_relay as _adapt
280
+ return _adapt(agent, relay)
281
+ if cls_module.startswith("crewai"):
282
+ from .adapters.crewai import on_relay as _adapt
283
+ return _adapt(agent, relay)
284
+
285
+ raise TypeError(
286
+ f"on_relay() doesn't recognize {type(agent).__name__} from {cls_module}. "
287
+ "Supported frameworks: Claude Agent SDK, OpenAI Agents, Google ADK, Agno, Swarms, CrewAI (Python). "
288
+ "For Claude Agent SDK, you can also import directly: "
289
+ "from agent_relay.communicate.adapters.claude_sdk import on_relay"
290
+ )
291
+
292
+
293
+ __all__ = ["Relay", "on_relay"]