agent-relay 3.2.14 → 3.2.16

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 (137) 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 +3859 -17164
  6. package/package.json +8 -8
  7. package/packages/acp-bridge/package.json +2 -2
  8. package/packages/config/package.json +1 -1
  9. package/packages/hooks/package.json +4 -4
  10. package/packages/memory/package.json +2 -2
  11. package/packages/openclaw/package.json +2 -2
  12. package/packages/policy/package.json +2 -2
  13. package/packages/sdk/dist/broker-path.d.ts +19 -0
  14. package/packages/sdk/dist/broker-path.d.ts.map +1 -0
  15. package/packages/sdk/dist/broker-path.js +71 -0
  16. package/packages/sdk/dist/broker-path.js.map +1 -0
  17. package/packages/sdk/dist/cli-registry.d.ts.map +1 -1
  18. package/packages/sdk/dist/cli-registry.js +10 -1
  19. package/packages/sdk/dist/cli-registry.js.map +1 -1
  20. package/packages/sdk/dist/client.d.ts +6 -1
  21. package/packages/sdk/dist/client.d.ts.map +1 -1
  22. package/packages/sdk/dist/client.js +18 -0
  23. package/packages/sdk/dist/client.js.map +1 -1
  24. package/packages/sdk/dist/communicate/adapters/index.d.ts +0 -5
  25. package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -1
  26. package/packages/sdk/dist/communicate/adapters/index.js +0 -5
  27. package/packages/sdk/dist/communicate/adapters/index.js.map +1 -1
  28. package/packages/sdk/dist/communicate/adapters/pi.d.ts +0 -1
  29. package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -1
  30. package/packages/sdk/dist/communicate/adapters/pi.js +0 -4
  31. package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -1
  32. package/packages/sdk/dist/communicate/core.d.ts.map +1 -1
  33. package/packages/sdk/dist/communicate/core.js +2 -3
  34. package/packages/sdk/dist/communicate/core.js.map +1 -1
  35. package/packages/sdk/dist/communicate/index.d.ts +17 -1
  36. package/packages/sdk/dist/communicate/index.d.ts.map +1 -1
  37. package/packages/sdk/dist/communicate/index.js +40 -1
  38. package/packages/sdk/dist/communicate/index.js.map +1 -1
  39. package/packages/sdk/dist/communicate/transport.d.ts +0 -1
  40. package/packages/sdk/dist/communicate/transport.d.ts.map +1 -1
  41. package/packages/sdk/dist/communicate/transport.js +42 -134
  42. package/packages/sdk/dist/communicate/transport.js.map +1 -1
  43. package/packages/sdk/dist/http.d.ts +38 -0
  44. package/packages/sdk/dist/http.d.ts.map +1 -0
  45. package/packages/sdk/dist/http.js +60 -0
  46. package/packages/sdk/dist/http.js.map +1 -0
  47. package/packages/sdk/dist/protocol.d.ts +25 -0
  48. package/packages/sdk/dist/protocol.d.ts.map +1 -1
  49. package/packages/sdk/dist/relay.d.ts +26 -3
  50. package/packages/sdk/dist/relay.d.ts.map +1 -1
  51. package/packages/sdk/dist/relay.js +62 -4
  52. package/packages/sdk/dist/relay.js.map +1 -1
  53. package/packages/sdk/dist/workflows/api-executor.d.ts +16 -0
  54. package/packages/sdk/dist/workflows/api-executor.d.ts.map +1 -0
  55. package/packages/sdk/dist/workflows/api-executor.js +94 -0
  56. package/packages/sdk/dist/workflows/api-executor.js.map +1 -0
  57. package/packages/sdk/dist/workflows/builder.d.ts +14 -0
  58. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  59. package/packages/sdk/dist/workflows/builder.js +26 -0
  60. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  61. package/packages/sdk/dist/workflows/cloud-runner.d.ts +15 -0
  62. package/packages/sdk/dist/workflows/cloud-runner.d.ts.map +1 -0
  63. package/packages/sdk/dist/workflows/cloud-runner.js +41 -0
  64. package/packages/sdk/dist/workflows/cloud-runner.js.map +1 -0
  65. package/packages/sdk/dist/workflows/index.d.ts +2 -0
  66. package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
  67. package/packages/sdk/dist/workflows/index.js +1 -0
  68. package/packages/sdk/dist/workflows/index.js.map +1 -1
  69. package/packages/sdk/dist/workflows/run.d.ts.map +1 -1
  70. package/packages/sdk/dist/workflows/run.js +4 -0
  71. package/packages/sdk/dist/workflows/run.js.map +1 -1
  72. package/packages/sdk/dist/workflows/runner.d.ts +14 -0
  73. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  74. package/packages/sdk/dist/workflows/runner.js +154 -10
  75. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  76. package/packages/sdk/dist/workflows/types.d.ts +13 -3
  77. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  78. package/packages/sdk/dist/workflows/types.js +5 -1
  79. package/packages/sdk/dist/workflows/types.js.map +1 -1
  80. package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
  81. package/packages/sdk/dist/workflows/validator.js +12 -0
  82. package/packages/sdk/dist/workflows/validator.js.map +1 -1
  83. package/packages/sdk/package.json +13 -3
  84. package/packages/sdk/src/__tests__/channel-management.test.ts +131 -0
  85. package/packages/sdk/src/__tests__/communicate/core.test.ts +36 -88
  86. package/packages/sdk/src/__tests__/communicate/transport.test.ts +41 -80
  87. package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +120 -0
  88. package/packages/sdk/src/__tests__/relay-channel-ops.test.ts +121 -0
  89. package/packages/sdk/src/__tests__/workflow-runner.test.ts +2 -2
  90. package/packages/sdk/src/broker-path.ts +74 -0
  91. package/packages/sdk/src/cli-registry.ts +10 -1
  92. package/packages/sdk/src/client.ts +28 -0
  93. package/packages/sdk/src/communicate/adapters/index.ts +0 -5
  94. package/packages/sdk/src/communicate/adapters/pi.ts +1 -5
  95. package/packages/sdk/src/communicate/core.ts +6 -10
  96. package/packages/sdk/src/communicate/index.ts +57 -1
  97. package/packages/sdk/src/communicate/transport.ts +46 -177
  98. package/packages/sdk/src/http.ts +96 -0
  99. package/packages/sdk/src/protocol.ts +24 -0
  100. package/packages/sdk/src/relay.ts +93 -8
  101. package/packages/sdk/src/workflows/README.md +5 -2
  102. package/packages/sdk/src/workflows/api-executor.ts +108 -0
  103. package/packages/sdk/src/workflows/builder.ts +40 -0
  104. package/packages/sdk/src/workflows/cloud-runner.ts +56 -0
  105. package/packages/sdk/src/workflows/index.ts +2 -0
  106. package/packages/sdk/src/workflows/run.ts +5 -0
  107. package/packages/sdk/src/workflows/runner.ts +181 -11
  108. package/packages/sdk/src/workflows/types.ts +19 -4
  109. package/packages/sdk/src/workflows/validator.ts +15 -0
  110. package/packages/sdk-py/README.md +7 -0
  111. package/packages/sdk-py/pyproject.toml +1 -1
  112. package/packages/sdk-py/src/agent_relay/__init__.py +2 -0
  113. package/packages/sdk-py/src/agent_relay/client.py +4 -0
  114. package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +0 -9
  115. package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +5 -9
  116. package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +5 -7
  117. package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +3 -13
  118. package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +5 -2
  119. package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +5 -9
  120. package/packages/sdk-py/src/agent_relay/communicate/core.py +7 -24
  121. package/packages/sdk-py/src/agent_relay/communicate/transport.py +35 -212
  122. package/packages/sdk-py/src/agent_relay/communicate/types.py +1 -1
  123. package/packages/sdk-py/src/agent_relay/protocol.py +1 -0
  124. package/packages/sdk-py/src/agent_relay/relay.py +9 -1
  125. package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +6 -6
  126. package/packages/sdk-py/tests/communicate/conftest.py +86 -233
  127. package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +2 -2
  128. package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +14 -24
  129. package/packages/sdk-py/tests/communicate/test_transport.py +65 -54
  130. package/packages/sdk-py/tests/test_send_message_mode.py +91 -0
  131. package/packages/sdk-swift/Sources/AgentRelaySDK/RelayObserver.swift +323 -0
  132. package/packages/sdk-swift/Sources/AgentRelaySDK/RelayObserverTypes.swift +143 -0
  133. package/packages/sdk-swift/Tests/AgentRelaySDKTests/RelayObserverTests.swift +526 -0
  134. package/packages/telemetry/package.json +1 -1
  135. package/packages/trajectory/package.json +2 -2
  136. package/packages/user-directory/package.json +2 -2
  137. package/packages/utils/package.json +2 -2
@@ -10,7 +10,7 @@ from inspect import isawaitable
10
10
  from typing import Any, Callable
11
11
 
12
12
  from .transport import RelayTransport
13
- from .types import Message, MessageCallback, RelayConfig, RelayConfigError, RelayAuthError
13
+ from .types import Message, MessageCallback, RelayAuthError, RelayConfig, RelayConfigError
14
14
 
15
15
  MAX_PENDING_MESSAGES = 10_000
16
16
 
@@ -85,11 +85,6 @@ class Relay:
85
85
 
86
86
  return unsubscribe
87
87
 
88
-
89
- async def join(self, channel: str) -> None:
90
- await self._ensure_connected()
91
- await self.transport.join_channel(channel)
92
-
93
88
  async def agents(self) -> list[str]:
94
89
  await self._ensure_connected()
95
90
  return await self.transport.list_agents()
@@ -160,25 +155,17 @@ class Relay:
160
155
  try:
161
156
  await self.transport.connect()
162
157
  self._ws_connected = True
158
+ except (RelayConfigError, RelayAuthError):
159
+ raise
163
160
  except Exception:
164
161
  # WebSocket failed — register agent via HTTP and fall back to polling
165
162
  await self.transport.register_agent()
166
163
  self._ws_connected = False
167
164
  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
165
  self._connected = True
175
166
  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
167
+ except BaseException as exc:
168
+ self._connect_future.set_exception(exc)
182
169
  raise
183
170
 
184
171
  def _schedule_connect(self) -> None:
@@ -262,10 +249,6 @@ def on_relay(agent: Any, relay: Relay | None = None) -> Any:
262
249
  relay = Relay(getattr(agent, "name", "Agent"))
263
250
 
264
251
  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
252
  if cls_module.startswith("agents"):
270
253
  from .adapters.openai_agents import on_relay as _adapt
271
254
  return _adapt(agent, relay)
@@ -284,8 +267,8 @@ def on_relay(agent: Any, relay: Relay | None = None) -> Any:
284
267
 
285
268
  raise TypeError(
286
269
  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: "
270
+ "Supported frameworks: OpenAI Agents, Google ADK, Agno, Swarms, CrewAI (Python). "
271
+ "For Claude Agent SDK, import the adapter directly: "
289
272
  "from agent_relay.communicate.adapters.claude_sdk import on_relay"
290
273
  )
291
274
 
@@ -136,211 +136,66 @@ class RelayTransport:
136
136
  self._message_callback = callback
137
137
 
138
138
  async def register_agent(self) -> str:
139
- """Register agent, or rotate token if it already exists (registerOrRotate pattern)."""
140
139
  self._require_config(require_workspace=True)
141
140
 
142
141
  if self.agent_id is not None and self.token is not None:
143
142
  return self.agent_id
144
143
 
145
- try:
146
- payload = await self.send_http(
147
- "POST",
148
- "/v1/agents",
149
- payload={"name": self.agent_name, "type": "agent"},
150
- )
151
- except RelayConnectionError as exc:
152
- if exc.status_code == 409:
153
- # Agent already exists — get its info and rotate the token
154
- from urllib.parse import quote
155
- agent_payload = await self.send_http(
156
- "GET",
157
- f"/v1/agents/{quote(self.agent_name, safe='')}",
158
- )
159
- agent_data = agent_payload.get("data", agent_payload)
160
- self.agent_id = agent_data["id"]
161
-
162
- rotate_payload = await self.send_http(
163
- "POST",
164
- f"/v1/agents/{quote(self.agent_name, safe='')}/rotate-token",
165
- )
166
- rotate_data = rotate_payload.get("data", rotate_payload)
167
- self.token = rotate_data["token"]
168
- return self.agent_id
169
- raise
170
- # Relaycast API wraps in {ok, data: {...}}
171
- data = payload.get("data", payload)
172
- self.agent_id = data["id"]
173
- self.token = data["token"]
144
+ payload = await self.send_http(
145
+ "POST",
146
+ "/v1/agents/register",
147
+ payload={"name": self.agent_name, "workspace": self.config.workspace},
148
+ )
149
+ self.agent_id = payload["agent_id"]
150
+ self.token = payload["token"]
174
151
  return self.agent_id
175
152
 
176
153
  async def unregister_agent(self) -> None:
177
- if self.agent_id is None or self.token is None:
154
+ if self.agent_id is None:
178
155
  await self._close_session_if_idle()
179
156
  return
180
157
 
181
- await self._send_http_as_agent("POST", "/v1/agents/disconnect")
158
+ agent_id = self.agent_id
159
+ await self.send_http("DELETE", f"/v1/agents/{agent_id}")
182
160
  self.agent_id = None
183
161
  self.token = None
184
162
  await self._close_session_if_idle()
185
163
 
186
164
  async def send_dm(self, recipient: str, text: str) -> str:
187
165
  await self._ensure_registered()
188
- payload = await self._send_http_as_agent(
166
+ payload = await self.send_http(
189
167
  "POST",
190
- "/v1/dm",
191
- payload={"to": recipient, "text": text},
168
+ "/v1/messages/dm",
169
+ payload={"to": recipient, "text": text, "from": self.agent_name},
192
170
  )
193
- if payload is None:
194
- return ""
195
- data = payload.get("data", payload)
196
- return data.get("id", data.get("message_id", ""))
171
+ return payload["message_id"]
197
172
 
198
173
  async def post_message(self, channel: str, text: str) -> str:
199
174
  await self._ensure_registered()
200
- from urllib.parse import quote
201
-
202
- payload = await self._send_http_as_agent(
175
+ payload = await self.send_http(
203
176
  "POST",
204
- f"/v1/channels/{quote(channel, safe='')}/messages",
205
- payload={"text": text},
177
+ "/v1/messages/channel",
178
+ payload={"channel": channel, "text": text, "from": self.agent_name},
206
179
  )
207
- if payload is None:
208
- return ""
209
- data = payload.get("data", payload)
210
- return data.get("id", data.get("message_id", ""))
180
+ return payload["message_id"]
211
181
 
212
182
  async def reply(self, message_id: str, text: str) -> str:
213
183
  await self._ensure_registered()
214
- from urllib.parse import quote
215
-
216
- payload = await self._send_http_as_agent(
184
+ payload = await self.send_http(
217
185
  "POST",
218
- f"/v1/messages/{quote(message_id, safe='')}/replies",
219
- payload={"text": text},
220
- )
221
- if payload is None:
222
- return ""
223
- data = payload.get("data", payload)
224
- return data.get("id", data.get("message_id", ""))
225
-
226
-
227
- async def join_channel(self, channel: str) -> None:
228
- await self._ensure_registered()
229
- from urllib.parse import quote
230
- await self._send_http_as_agent(
231
- 'POST',
232
- f'/v1/channels/{quote(channel, safe="")}/join',
186
+ "/v1/messages/reply",
187
+ payload={"message_id": message_id, "text": text, "from": self.agent_name},
233
188
  )
189
+ return payload["message_id"]
234
190
 
235
191
  async def check_inbox(self) -> list[Message]:
236
192
  await self._ensure_registered()
237
- from urllib.parse import quote
238
-
239
- payload = await self._send_http_as_agent("GET", "/v1/inbox")
240
- data = payload.get("data", payload)
241
- messages: list[Message] = []
242
-
243
- # Fetch unread DM conversations
244
- for dm in data.get("unread_dms", []):
245
- conv_id = dm.get("conversation_id", "")
246
- sender = dm.get("from", "unknown")
247
- # Fetch actual messages from the conversation
248
- try:
249
- conv_payload = await self._send_http_as_agent(
250
- "GET", f"/v1/dm/{quote(conv_id, safe='')}/messages"
251
- )
252
- conv_data = conv_payload.get("data", conv_payload)
253
- items = conv_data if isinstance(conv_data, list) else []
254
- for item in items:
255
- messages.append(Message(
256
- sender=item.get("agent_name", sender),
257
- text=item.get("text", ""),
258
- channel=None,
259
- thread_id=conv_id,
260
- timestamp=item.get("created_at"),
261
- message_id=item.get("id"),
262
- ))
263
- except Exception:
264
- # Fall back to the summary last_message
265
- last = dm.get("last_message", {})
266
- if last.get("text"):
267
- messages.append(Message(
268
- sender=sender,
269
- text=last["text"],
270
- channel=None,
271
- thread_id=conv_id,
272
- timestamp=last.get("created_at"),
273
- message_id=last.get("id"),
274
- ))
275
-
276
- # Also include unread channel mentions
277
- for mention in data.get("mentions", []):
278
- messages.append(Message(
279
- sender=mention.get("from", mention.get("agent_name", "unknown")),
280
- text=mention.get("text", ""),
281
- channel=mention.get("channel_name"),
282
- thread_id=mention.get("thread_id"),
283
- timestamp=mention.get("created_at"),
284
- message_id=mention.get("id"),
285
- ))
286
-
287
- return messages
193
+ payload = await self.send_http("GET", f"/v1/inbox/{self.agent_id}")
194
+ return [self._message_from_payload(item) for item in payload.get("messages", [])]
288
195
 
289
196
  async def list_agents(self) -> list[str]:
290
197
  payload = await self.send_http("GET", "/v1/agents")
291
- data = payload.get("data", payload)
292
- if isinstance(data, list):
293
- return [a.get("name", a) if isinstance(a, dict) else a for a in data]
294
- return list(data.get("agents", []))
295
-
296
- async def _send_http_as_agent(
297
- self,
298
- method: str,
299
- path: str,
300
- *,
301
- payload: dict[str, Any] | None = None,
302
- ) -> Any:
303
- """Like send_http but authenticates with the per-agent token."""
304
- await self._ensure_registered()
305
- session = await self._ensure_session()
306
- url = f"{self._base_url()}{path}"
307
- headers = {"Authorization": f"Bearer {self.token}"}
308
-
309
- for attempt in range(1, HTTP_RETRY_ATTEMPTS + 1):
310
- try:
311
- async with session.request(method, url, json=payload, headers=headers) as response:
312
- if response.status == 401:
313
- raise RelayAuthError(await self._error_message(response))
314
-
315
- if 500 <= response.status <= 599:
316
- message = await self._error_message(response)
317
- if attempt < HTTP_RETRY_ATTEMPTS:
318
- await asyncio.sleep(min(2 ** (attempt - 1), WS_RECONNECT_MAX_DELAY))
319
- continue
320
- raise RelayConnectionError(response.status, message)
321
-
322
- if response.status >= 400:
323
- raise RelayConnectionError(
324
- response.status,
325
- await self._error_message(response),
326
- )
327
-
328
- if response.status == 204:
329
- return None
330
-
331
- if response.content_type == "application/json":
332
- return await response.json()
333
-
334
- return await response.text()
335
- except (RelayAuthError, RelayConnectionError):
336
- raise
337
- except aiohttp.ClientError as exc:
338
- if attempt < HTTP_RETRY_ATTEMPTS:
339
- await asyncio.sleep(min(2 ** (attempt - 1), WS_RECONNECT_MAX_DELAY))
340
- continue
341
- raise RelayConnectionError(0, str(exc)) from exc
342
-
343
- raise RelayConnectionError(500, "Unexpected transport retry failure")
198
+ return list(payload.get("agents", []))
344
199
 
345
200
  async def _ensure_registered(self) -> None:
346
201
  if self.agent_id is None or self.token is None:
@@ -382,7 +237,7 @@ class RelayTransport:
382
237
  from urllib.parse import quote
383
238
 
384
239
  session = await self._ensure_session()
385
- ws_url = f"{self._ws_base_url()}/v1/ws?token={quote(self.token, safe='')}"
240
+ ws_url = f"{self._ws_base_url()}/v1/ws/{self.agent_id}?token={quote(self.token, safe='')}"
386
241
  self._ws = await session.ws_connect(ws_url)
387
242
 
388
243
  def _ws_base_url(self) -> str:
@@ -431,58 +286,30 @@ class RelayTransport:
431
286
 
432
287
  async def _dispatch_ws_payload(self, raw_payload: str) -> None:
433
288
  payload = json.loads(raw_payload)
434
- event_type = payload.get("type", "")
435
-
436
- if event_type == "ping":
289
+ if payload.get("type") == "ping":
437
290
  if self._ws is not None and not self._ws.closed:
438
291
  await self._ws.send_json({"type": "pong"})
439
292
  return
440
-
441
- # Accept message.created, dm.received, direct_message.received, thread.reply, and legacy "message"
442
- message_events = {"message.created", "dm.received", "direct_message.received",
443
- "thread.reply", "message", "group_dm.received"}
444
- if event_type not in message_events:
293
+ if payload.get("type") != "message":
445
294
  return
446
295
 
447
296
  callback = self._message_callback
448
297
  if callback is None:
449
298
  return
450
299
 
451
- try:
452
- msg = self._message_from_payload(payload)
453
- except (KeyError, TypeError):
454
- return
455
-
456
- result = callback(msg)
300
+ result = callback(self._message_from_payload(payload))
457
301
  if isawaitable(result):
458
302
  await result
459
303
 
460
304
  @staticmethod
461
305
  def _message_from_payload(payload: dict[str, Any]) -> Message:
462
- # Support both flat and nested message structures
463
- m = payload.get("message") if isinstance(payload.get("message"), dict) else payload
464
- sender = (
465
- m.get("sender")
466
- or m.get("agent_name")
467
- or m.get("from")
468
- or m.get("agentName")
469
- or payload.get("agent_name")
470
- or payload.get("from")
471
- or "unknown"
472
- )
473
- text = m.get("text", "")
474
- channel = m.get("channel") or m.get("channel_name") or m.get("channelName") or payload.get("channel") or payload.get("channel_name")
475
- thread_id = m.get("thread_id") or m.get("threadId") or m.get("conversation_id") or m.get("conversationId") or payload.get("thread_id")
476
- timestamp = m.get("timestamp") or m.get("created_at") or m.get("createdAt") or payload.get("timestamp")
477
- message_id = m.get("id") or m.get("message_id") or m.get("messageId") or payload.get("message_id")
478
-
479
306
  return Message(
480
- sender=sender,
481
- text=text,
482
- channel=channel,
483
- thread_id=thread_id,
484
- timestamp=timestamp,
485
- message_id=message_id,
307
+ sender=payload["sender"],
308
+ text=payload["text"],
309
+ channel=payload.get("channel"),
310
+ thread_id=payload.get("thread_id"),
311
+ timestamp=payload.get("timestamp"),
312
+ message_id=payload.get("message_id"),
486
313
  )
487
314
 
488
315
  @staticmethod
@@ -492,10 +319,6 @@ class RelayTransport:
492
319
  except Exception:
493
320
  text = await response.text()
494
321
  return text or response.reason or "Request failed"
495
- # Relaycast wraps errors as {ok: false, error: {code, message}}
496
- error = payload.get("error")
497
- if isinstance(error, dict) and error.get("message"):
498
- return str(error["message"])
499
322
  return str(payload.get("message") or response.reason or "Request failed")
500
323
 
501
324
 
@@ -15,7 +15,7 @@ class Message:
15
15
  text: str
16
16
  channel: str | None = None
17
17
  thread_id: str | None = None
18
- timestamp: str | float | None = None
18
+ timestamp: float | None = None
19
19
  message_id: str | None = None
20
20
 
21
21
 
@@ -12,6 +12,7 @@ PROTOCOL_VERSION = 1
12
12
 
13
13
  AgentRuntime = Literal["pty", "headless"]
14
14
  HeadlessProvider = Literal["claude", "opencode"]
15
+ MessageInjectionMode = Literal["wait", "steer"]
15
16
 
16
17
 
17
18
  @dataclass
@@ -16,7 +16,7 @@ from dataclasses import dataclass, field
16
16
  from typing import Any, Awaitable, Callable, Optional
17
17
 
18
18
  from .client import AgentRelayClient
19
- from .protocol import AgentRuntime, BrokerEvent
19
+ from .protocol import AgentRuntime, BrokerEvent, MessageInjectionMode
20
20
 
21
21
  # ── Public types ──────────────────────────────────────────────────────────────
22
22
 
@@ -36,6 +36,7 @@ class Message:
36
36
  text: str
37
37
  thread_id: Optional[str] = None
38
38
  data: Optional[dict[str, Any]] = None
39
+ mode: Optional[MessageInjectionMode] = None
39
40
 
40
41
 
41
42
  @dataclass
@@ -197,6 +198,7 @@ class Agent:
197
198
  thread_id: Optional[str] = None,
198
199
  priority: Optional[int] = None,
199
200
  data: Optional[dict[str, Any]] = None,
201
+ mode: Optional[MessageInjectionMode] = None,
200
202
  ) -> Message:
201
203
  client = await self._relay._ensure_started()
202
204
  result = await client.send_message(
@@ -206,6 +208,7 @@ class Agent:
206
208
  thread_id=thread_id,
207
209
  priority=priority,
208
210
  data=data,
211
+ mode=mode,
209
212
  )
210
213
 
211
214
  event_id = result.get("event_id", secrets.token_hex(8))
@@ -216,6 +219,7 @@ class Agent:
216
219
  text=text,
217
220
  thread_id=thread_id,
218
221
  data=data,
222
+ mode=mode,
219
223
  )
220
224
  # Don't fire hook for unsupported operations
221
225
  if event_id != "unsupported_operation" and self._relay.on_message_sent:
@@ -259,6 +263,7 @@ class HumanHandle:
259
263
  thread_id: Optional[str] = None,
260
264
  priority: Optional[int] = None,
261
265
  data: Optional[dict[str, Any]] = None,
266
+ mode: Optional[MessageInjectionMode] = None,
262
267
  ) -> Message:
263
268
  client = await self._relay._ensure_started()
264
269
  result = await client.send_message(
@@ -268,6 +273,7 @@ class HumanHandle:
268
273
  thread_id=thread_id,
269
274
  priority=priority,
270
275
  data=data,
276
+ mode=mode,
271
277
  )
272
278
 
273
279
  event_id = result.get("event_id", secrets.token_hex(8))
@@ -278,6 +284,7 @@ class HumanHandle:
278
284
  text=text,
279
285
  thread_id=thread_id,
280
286
  data=data,
287
+ mode=mode,
281
288
  )
282
289
  # Don't fire hook for unsupported operations
283
290
  if event_id != "unsupported_operation" and self._relay.on_message_sent:
@@ -772,6 +779,7 @@ class AgentRelay:
772
779
  to=event.get("target", ""),
773
780
  text=event.get("body", ""),
774
781
  thread_id=event.get("thread_id"),
782
+ mode=event.get("injection_mode") or event.get("mode"),
775
783
  )
776
784
  if self.on_message_received:
777
785
  self.on_message_received(msg)
@@ -48,7 +48,7 @@ def mock_options():
48
48
  def test_on_relay_injects_mcp_server(mock_relay, mock_options):
49
49
  adapter = _adapter_module()
50
50
 
51
- result = adapter.on_relay("TestAgent", mock_options, relay=mock_relay)
51
+ result = adapter.on_relay(mock_options, relay=mock_relay, name="TestAgent")
52
52
 
53
53
  assert result is mock_options
54
54
  assert len(mock_options.mcp_servers) == 1
@@ -66,7 +66,7 @@ async def test_post_tool_use_hook_drains_inbox(mock_relay, mock_options):
66
66
  Message(sender="Other", text="Hello", message_id="1")
67
67
  ]
68
68
 
69
- adapter.on_relay("TestAgent", mock_options, relay=mock_relay)
69
+ adapter.on_relay(mock_options, relay=mock_relay, name="TestAgent")
70
70
  post_tool_use = mock_options.hooks.post_tool_use
71
71
 
72
72
  assert post_tool_use is not None
@@ -84,7 +84,7 @@ async def test_post_tool_use_hook_returns_none_if_inbox_empty(mock_relay, mock_o
84
84
  adapter = _adapter_module()
85
85
  mock_relay.inbox.return_value = []
86
86
 
87
- adapter.on_relay("TestAgent", mock_options, relay=mock_relay)
87
+ adapter.on_relay(mock_options, relay=mock_relay, name="TestAgent")
88
88
  hook_result = await mock_options.hooks.post_tool_use()
89
89
 
90
90
  assert hook_result is None
@@ -98,7 +98,7 @@ async def test_stop_hook_drains_inbox_and_continues(mock_relay, mock_options):
98
98
  Message(sender="Other", text="Wait!", message_id="2")
99
99
  ]
100
100
 
101
- adapter.on_relay("TestAgent", mock_options, relay=mock_relay)
101
+ adapter.on_relay(mock_options, relay=mock_relay, name="TestAgent")
102
102
  stop_hook = mock_options.hooks.stop
103
103
 
104
104
  assert stop_hook is not None
@@ -114,7 +114,7 @@ async def test_stop_hook_returns_none_if_inbox_empty(mock_relay, mock_options):
114
114
  adapter = _adapter_module()
115
115
  mock_relay.inbox.return_value = []
116
116
 
117
- adapter.on_relay("TestAgent", mock_options, relay=mock_relay)
117
+ adapter.on_relay(mock_options, relay=mock_relay, name="TestAgent")
118
118
  hook_result = await mock_options.hooks.stop()
119
119
 
120
120
  assert hook_result is None
@@ -126,7 +126,7 @@ async def test_hooks_chaining(mock_relay, mock_options):
126
126
  original_post_tool_use = AsyncMock(return_value=MagicMock(system_message="Original"))
127
127
  mock_options.hooks.post_tool_use = original_post_tool_use
128
128
 
129
- adapter.on_relay("TestAgent", mock_options, relay=mock_relay)
129
+ adapter.on_relay(mock_options, relay=mock_relay, name="TestAgent")
130
130
 
131
131
  # When inbox is empty, it should return original result
132
132
  mock_relay.inbox.return_value = []