agent-relay 3.2.15 → 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.
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +3853 -17163
- package/package.json +8 -8
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/broker-path.d.ts +19 -0
- package/packages/sdk/dist/broker-path.d.ts.map +1 -0
- package/packages/sdk/dist/broker-path.js +71 -0
- package/packages/sdk/dist/broker-path.js.map +1 -0
- package/packages/sdk/dist/cli-registry.d.ts.map +1 -1
- package/packages/sdk/dist/cli-registry.js +4 -0
- package/packages/sdk/dist/cli-registry.js.map +1 -1
- package/packages/sdk/dist/client.d.ts +6 -1
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +18 -0
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/communicate/adapters/index.d.ts +0 -5
- package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/adapters/index.js +0 -5
- package/packages/sdk/dist/communicate/adapters/index.js.map +1 -1
- package/packages/sdk/dist/communicate/adapters/pi.d.ts +0 -1
- package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/adapters/pi.js +0 -4
- package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -1
- package/packages/sdk/dist/communicate/core.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/core.js +2 -3
- package/packages/sdk/dist/communicate/core.js.map +1 -1
- package/packages/sdk/dist/communicate/index.d.ts +17 -1
- package/packages/sdk/dist/communicate/index.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/index.js +40 -1
- package/packages/sdk/dist/communicate/index.js.map +1 -1
- package/packages/sdk/dist/communicate/transport.d.ts +0 -1
- package/packages/sdk/dist/communicate/transport.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/transport.js +42 -134
- package/packages/sdk/dist/communicate/transport.js.map +1 -1
- package/packages/sdk/dist/http.d.ts +38 -0
- package/packages/sdk/dist/http.d.ts.map +1 -0
- package/packages/sdk/dist/http.js +60 -0
- package/packages/sdk/dist/http.js.map +1 -0
- package/packages/sdk/dist/protocol.d.ts +25 -0
- package/packages/sdk/dist/protocol.d.ts.map +1 -1
- package/packages/sdk/dist/relay.d.ts +26 -3
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +62 -4
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/workflows/api-executor.d.ts +16 -0
- package/packages/sdk/dist/workflows/api-executor.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/api-executor.js +94 -0
- package/packages/sdk/dist/workflows/api-executor.js.map +1 -0
- package/packages/sdk/dist/workflows/builder.d.ts +14 -0
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +26 -0
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/cloud-runner.d.ts +15 -0
- package/packages/sdk/dist/workflows/cloud-runner.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/cloud-runner.js +41 -0
- package/packages/sdk/dist/workflows/cloud-runner.js.map +1 -0
- package/packages/sdk/dist/workflows/index.d.ts +2 -0
- package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/index.js +1 -0
- package/packages/sdk/dist/workflows/index.js.map +1 -1
- package/packages/sdk/dist/workflows/run.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/run.js +4 -0
- package/packages/sdk/dist/workflows/run.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +14 -0
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +154 -10
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/types.d.ts +13 -3
- package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/types.js +5 -1
- package/packages/sdk/dist/workflows/types.js.map +1 -1
- package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/validator.js +12 -0
- package/packages/sdk/dist/workflows/validator.js.map +1 -1
- package/packages/sdk/package.json +13 -3
- package/packages/sdk/src/__tests__/channel-management.test.ts +131 -0
- package/packages/sdk/src/__tests__/communicate/core.test.ts +36 -88
- package/packages/sdk/src/__tests__/communicate/transport.test.ts +41 -80
- package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +120 -0
- package/packages/sdk/src/__tests__/relay-channel-ops.test.ts +121 -0
- package/packages/sdk/src/broker-path.ts +74 -0
- package/packages/sdk/src/cli-registry.ts +4 -0
- package/packages/sdk/src/client.ts +28 -0
- package/packages/sdk/src/communicate/adapters/index.ts +0 -5
- package/packages/sdk/src/communicate/adapters/pi.ts +1 -5
- package/packages/sdk/src/communicate/core.ts +6 -10
- package/packages/sdk/src/communicate/index.ts +57 -1
- package/packages/sdk/src/communicate/transport.ts +46 -177
- package/packages/sdk/src/http.ts +96 -0
- package/packages/sdk/src/protocol.ts +24 -0
- package/packages/sdk/src/relay.ts +93 -8
- package/packages/sdk/src/workflows/README.md +5 -2
- package/packages/sdk/src/workflows/api-executor.ts +108 -0
- package/packages/sdk/src/workflows/builder.ts +40 -0
- package/packages/sdk/src/workflows/cloud-runner.ts +56 -0
- package/packages/sdk/src/workflows/index.ts +2 -0
- package/packages/sdk/src/workflows/run.ts +5 -0
- package/packages/sdk/src/workflows/runner.ts +181 -11
- package/packages/sdk/src/workflows/types.ts +19 -4
- package/packages/sdk/src/workflows/validator.ts +15 -0
- package/packages/sdk-py/README.md +7 -0
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +2 -0
- package/packages/sdk-py/src/agent_relay/client.py +4 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +0 -9
- package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +5 -9
- package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +5 -7
- package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +3 -13
- package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +5 -2
- package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +5 -9
- package/packages/sdk-py/src/agent_relay/communicate/core.py +7 -24
- package/packages/sdk-py/src/agent_relay/communicate/transport.py +35 -212
- package/packages/sdk-py/src/agent_relay/communicate/types.py +1 -1
- package/packages/sdk-py/src/agent_relay/protocol.py +1 -0
- package/packages/sdk-py/src/agent_relay/relay.py +9 -1
- package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +6 -6
- package/packages/sdk-py/tests/communicate/conftest.py +86 -233
- package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +2 -2
- package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +14 -24
- package/packages/sdk-py/tests/communicate/test_transport.py +65 -54
- package/packages/sdk-py/tests/test_send_message_mode.py +91 -0
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- 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
|
|
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
|
-
|
|
177
|
-
|
|
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:
|
|
288
|
-
"For Claude Agent SDK,
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
154
|
+
if self.agent_id is None:
|
|
178
155
|
await self._close_session_if_idle()
|
|
179
156
|
return
|
|
180
157
|
|
|
181
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
payload = await self._send_http_as_agent(
|
|
175
|
+
payload = await self.send_http(
|
|
203
176
|
"POST",
|
|
204
|
-
|
|
205
|
-
payload={"text": text},
|
|
177
|
+
"/v1/messages/channel",
|
|
178
|
+
payload={"channel": channel, "text": text, "from": self.agent_name},
|
|
206
179
|
)
|
|
207
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
payload = await self._send_http_as_agent(
|
|
184
|
+
payload = await self.send_http(
|
|
217
185
|
"POST",
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 = []
|