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.
- 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 +3859 -17164
- 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 +10 -1
- 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/__tests__/workflow-runner.test.ts +2 -2
- package/packages/sdk/src/broker-path.ts +74 -0
- package/packages/sdk/src/cli-registry.ts +10 -1
- 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/sdk-swift/Sources/AgentRelaySDK/RelayObserver.swift +323 -0
- package/packages/sdk-swift/Sources/AgentRelaySDK/RelayObserverTypes.swift +143 -0
- package/packages/sdk-swift/Tests/AgentRelaySDKTests/RelayObserverTests.swift +526 -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
|
@@ -16,20 +16,7 @@ from agent_relay.communicate.types import RelayConfig
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class MockRelayServer:
|
|
19
|
-
"""Minimal in-process Relaycast mock
|
|
20
|
-
|
|
21
|
-
Endpoints:
|
|
22
|
-
POST /v1/agents — register (workspace key auth)
|
|
23
|
-
POST /v1/agents/disconnect — unregister (agent token auth)
|
|
24
|
-
POST /v1/agents/heartbeat — presence heartbeat (agent token)
|
|
25
|
-
GET /v1/agents — list agents (workspace key auth)
|
|
26
|
-
POST /v1/dm — send DM (agent token auth)
|
|
27
|
-
POST /v1/channels/{name}/messages — channel message (agent token auth)
|
|
28
|
-
POST /v1/messages/{id}/replies — reply (agent token auth)
|
|
29
|
-
GET /v1/inbox — inbox summary (agent token auth)
|
|
30
|
-
GET /v1/dm/{conv_id}/messages — DM conversation messages (agent token)
|
|
31
|
-
GET /v1/ws — websocket (agent token via query param)
|
|
32
|
-
"""
|
|
19
|
+
"""Minimal in-process Relaycast mock for transport and integration tests."""
|
|
33
20
|
|
|
34
21
|
def __init__(self, *, api_key: str = "test-key", workspace: str = "test-workspace") -> None:
|
|
35
22
|
self.api_key = api_key
|
|
@@ -44,27 +31,20 @@ class MockRelayServer:
|
|
|
44
31
|
self.received_ws_messages: list[dict[str, Any]] = []
|
|
45
32
|
self.ws_connection_counts: dict[str, int] = defaultdict(int)
|
|
46
33
|
|
|
47
|
-
# Map agent tokens → agent IDs for auth
|
|
48
|
-
self._token_to_agent_id: dict[str, str] = {}
|
|
49
|
-
# DM conversations: conv_id → list of messages
|
|
50
|
-
self._dm_conversations: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
|
51
|
-
|
|
52
34
|
self._active_websockets: dict[str, web.WebSocketResponse] = {}
|
|
53
35
|
self._queued_errors: dict[str, deque[tuple[int, dict[str, Any]]]] = defaultdict(deque)
|
|
54
36
|
self._agent_ids = count(1)
|
|
55
37
|
self._message_ids = count(1)
|
|
56
38
|
|
|
57
39
|
self._app = web.Application()
|
|
58
|
-
self._app.router.add_post("/v1/agents", self._handle_register)
|
|
40
|
+
self._app.router.add_post("/v1/agents/register", self._handle_register)
|
|
41
|
+
self._app.router.add_delete("/v1/agents/{agent_id}", self._handle_unregister)
|
|
42
|
+
self._app.router.add_post("/v1/messages/dm", self._handle_dm)
|
|
43
|
+
self._app.router.add_post("/v1/messages/channel", self._handle_channel)
|
|
44
|
+
self._app.router.add_post("/v1/messages/reply", self._handle_reply)
|
|
45
|
+
self._app.router.add_get("/v1/inbox/{agent_id}", self._handle_inbox)
|
|
59
46
|
self._app.router.add_get("/v1/agents", self._handle_agents)
|
|
60
|
-
self._app.router.
|
|
61
|
-
self._app.router.add_post("/v1/agents/heartbeat", self._handle_heartbeat)
|
|
62
|
-
self._app.router.add_post("/v1/dm", self._handle_dm)
|
|
63
|
-
self._app.router.add_get("/v1/dm/{conv_id}/messages", self._handle_dm_messages)
|
|
64
|
-
self._app.router.add_post("/v1/channels/{channel}/messages", self._handle_channel)
|
|
65
|
-
self._app.router.add_post("/v1/messages/{message_id}/replies", self._handle_reply)
|
|
66
|
-
self._app.router.add_get("/v1/inbox", self._handle_inbox)
|
|
67
|
-
self._app.router.add_get("/v1/ws", self._handle_ws)
|
|
47
|
+
self._app.router.add_get("/v1/ws/{agent_id}", self._handle_ws)
|
|
68
48
|
|
|
69
49
|
self._runner: web.AppRunner | None = None
|
|
70
50
|
self._site: web.TCPSite | None = None
|
|
@@ -89,7 +69,7 @@ class MockRelayServer:
|
|
|
89
69
|
message: str,
|
|
90
70
|
repeat: int = 1,
|
|
91
71
|
) -> None:
|
|
92
|
-
body = {"
|
|
72
|
+
body = {"message": message}
|
|
93
73
|
for _ in range(repeat):
|
|
94
74
|
self._queued_errors[operation].append((status, body))
|
|
95
75
|
|
|
@@ -121,30 +101,15 @@ class MockRelayServer:
|
|
|
121
101
|
message_id: str | None = None,
|
|
122
102
|
timestamp: float | None = None,
|
|
123
103
|
) -> dict[str, Any]:
|
|
124
|
-
msg_id = message_id or f"message-{next(self._message_ids)}"
|
|
125
104
|
payload = {
|
|
126
105
|
"sender": sender,
|
|
127
106
|
"text": text,
|
|
128
107
|
"channel": channel,
|
|
129
108
|
"thread_id": thread_id,
|
|
130
|
-
"message_id":
|
|
109
|
+
"message_id": message_id or f"message-{next(self._message_ids)}",
|
|
131
110
|
"timestamp": timestamp,
|
|
132
111
|
}
|
|
133
|
-
|
|
134
|
-
conv_id = f"dm_{agent_id}_{sender}"
|
|
135
|
-
dm_msg = {
|
|
136
|
-
"id": msg_id,
|
|
137
|
-
"agent_name": sender,
|
|
138
|
-
"text": text,
|
|
139
|
-
"created_at": str(timestamp) if timestamp else None,
|
|
140
|
-
}
|
|
141
|
-
self._dm_conversations[conv_id].append(dm_msg)
|
|
142
|
-
self.inboxes[agent_id].append({
|
|
143
|
-
"conversation_id": conv_id,
|
|
144
|
-
"from": sender,
|
|
145
|
-
"unread_count": 1,
|
|
146
|
-
"last_message": dm_msg,
|
|
147
|
-
})
|
|
112
|
+
self.inboxes[agent_id].append(payload)
|
|
148
113
|
return payload
|
|
149
114
|
|
|
150
115
|
async def push_ws_message(
|
|
@@ -162,19 +127,15 @@ class MockRelayServer:
|
|
|
162
127
|
if ws is None or ws.closed:
|
|
163
128
|
raise AssertionError(f"No active websocket for agent {agent_id!r}")
|
|
164
129
|
|
|
165
|
-
payload
|
|
166
|
-
"type": "
|
|
167
|
-
"
|
|
130
|
+
payload = {
|
|
131
|
+
"type": "message",
|
|
132
|
+
"sender": sender,
|
|
168
133
|
"text": text,
|
|
169
|
-
"
|
|
134
|
+
"channel": channel,
|
|
135
|
+
"thread_id": thread_id,
|
|
136
|
+
"message_id": message_id or f"message-{next(self._message_ids)}",
|
|
137
|
+
"timestamp": timestamp,
|
|
170
138
|
}
|
|
171
|
-
if channel is not None:
|
|
172
|
-
payload["channel_name"] = channel
|
|
173
|
-
if thread_id is not None:
|
|
174
|
-
payload["thread_id"] = thread_id
|
|
175
|
-
if timestamp is not None:
|
|
176
|
-
payload["created_at"] = str(timestamp)
|
|
177
|
-
|
|
178
139
|
await ws.send_json(payload)
|
|
179
140
|
return payload
|
|
180
141
|
|
|
@@ -228,195 +189,130 @@ class MockRelayServer:
|
|
|
228
189
|
if self._runner is not None:
|
|
229
190
|
await self._runner.cleanup()
|
|
230
191
|
|
|
231
|
-
# --- Handlers ---
|
|
232
|
-
|
|
233
192
|
async def _handle_register(self, request: web.Request) -> web.StreamResponse:
|
|
234
193
|
payload = await request.json()
|
|
235
194
|
self._record_request("register_agent", request, payload)
|
|
236
195
|
|
|
237
196
|
if error := self._pop_error("register_agent"):
|
|
238
197
|
return error
|
|
239
|
-
if error := self.
|
|
198
|
+
if error := self._require_auth(request):
|
|
240
199
|
return error
|
|
241
200
|
|
|
242
201
|
agent_id = f"agent-{next(self._agent_ids)}"
|
|
243
202
|
token = f"token-{agent_id}"
|
|
244
203
|
self.registered_agents[agent_id] = {"name": payload["name"], "token": token}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
async def _handle_disconnect(self, request: web.Request) -> web.StreamResponse:
|
|
252
|
-
agent_id = self._resolve_agent_from_token(request)
|
|
204
|
+
return web.json_response({"agent_id": agent_id, "token": token})
|
|
205
|
+
|
|
206
|
+
async def _handle_unregister(self, request: web.Request) -> web.StreamResponse:
|
|
207
|
+
agent_id = request.match_info["agent_id"]
|
|
253
208
|
self._record_request("unregister_agent", request, {"agent_id": agent_id})
|
|
254
209
|
|
|
255
210
|
if error := self._pop_error("unregister_agent"):
|
|
256
211
|
return error
|
|
257
|
-
if
|
|
258
|
-
return
|
|
212
|
+
if error := self._require_auth(request):
|
|
213
|
+
return error
|
|
259
214
|
|
|
260
215
|
self.registered_agents.pop(agent_id, None)
|
|
261
216
|
self.inboxes.pop(agent_id, None)
|
|
262
217
|
ws = self._active_websockets.pop(agent_id, None)
|
|
263
218
|
if ws is not None and not ws.closed:
|
|
264
219
|
await ws.close()
|
|
265
|
-
|
|
266
|
-
token = self._extract_token(request)
|
|
267
|
-
if token:
|
|
268
|
-
self._token_to_agent_id.pop(token, None)
|
|
269
|
-
return web.json_response({"ok": True})
|
|
270
|
-
|
|
271
|
-
async def _handle_heartbeat(self, request: web.Request) -> web.StreamResponse:
|
|
272
|
-
return web.json_response({"ok": True})
|
|
220
|
+
return web.Response(status=204)
|
|
273
221
|
|
|
274
222
|
async def _handle_dm(self, request: web.Request) -> web.StreamResponse:
|
|
275
223
|
payload = await request.json()
|
|
276
|
-
agent_id = self._resolve_agent_from_token(request)
|
|
277
|
-
sender_name = self.registered_agents.get(agent_id, {}).get("name", "unknown") if agent_id else "unknown"
|
|
278
224
|
self._record_request("send_dm", request, payload)
|
|
279
225
|
|
|
280
226
|
if error := self._pop_error("send_dm"):
|
|
281
227
|
return error
|
|
282
|
-
if
|
|
283
|
-
return
|
|
228
|
+
if error := self._require_auth(request):
|
|
229
|
+
return error
|
|
284
230
|
|
|
285
231
|
message_id = f"message-{next(self._message_ids)}"
|
|
286
|
-
|
|
287
|
-
self.messages.append({"kind": "dm", "message_id": message_id, **payload, "from": sender_name})
|
|
288
|
-
|
|
289
|
-
# Deliver to recipient
|
|
290
|
-
recipient_ids = self.find_agent_ids(payload["to"])
|
|
291
|
-
dm_msg = {
|
|
292
|
-
"id": message_id,
|
|
293
|
-
"agent_name": sender_name,
|
|
294
|
-
"text": payload["text"],
|
|
295
|
-
"created_at": None,
|
|
296
|
-
}
|
|
297
|
-
self._dm_conversations[conv_id].append(dm_msg)
|
|
298
|
-
for rid in recipient_ids:
|
|
299
|
-
self.inboxes[rid].append({
|
|
300
|
-
"conversation_id": conv_id,
|
|
301
|
-
"from": sender_name,
|
|
302
|
-
"unread_count": 1,
|
|
303
|
-
"last_message": dm_msg,
|
|
304
|
-
})
|
|
305
|
-
|
|
232
|
+
self.messages.append({"kind": "dm", "message_id": message_id, **payload})
|
|
306
233
|
await self._deliver_to_agents(
|
|
307
|
-
|
|
308
|
-
sender=
|
|
234
|
+
self.find_agent_ids(payload["to"]),
|
|
235
|
+
sender=payload["from"],
|
|
309
236
|
text=payload["text"],
|
|
310
237
|
message_id=message_id,
|
|
311
238
|
)
|
|
312
|
-
|
|
313
|
-
return web.json_response({
|
|
314
|
-
"ok": True,
|
|
315
|
-
"data": {"id": message_id, "conversation_id": conv_id, "text": payload["text"]},
|
|
316
|
-
}, status=201)
|
|
317
|
-
|
|
318
|
-
async def _handle_dm_messages(self, request: web.Request) -> web.StreamResponse:
|
|
319
|
-
conv_id = request.match_info["conv_id"]
|
|
320
|
-
messages = list(self._dm_conversations.get(conv_id, []))
|
|
321
|
-
return web.json_response({"ok": True, "data": messages})
|
|
239
|
+
return web.json_response({"message_id": message_id})
|
|
322
240
|
|
|
323
241
|
async def _handle_channel(self, request: web.Request) -> web.StreamResponse:
|
|
324
|
-
channel = request.match_info["channel"]
|
|
325
242
|
payload = await request.json()
|
|
326
|
-
|
|
327
|
-
sender_name = self.registered_agents.get(agent_id, {}).get("name", "unknown") if agent_id else "unknown"
|
|
328
|
-
self._record_request("post_message", request, {**payload, "channel": channel})
|
|
243
|
+
self._record_request("post_message", request, payload)
|
|
329
244
|
|
|
330
245
|
if error := self._pop_error("post_message"):
|
|
331
246
|
return error
|
|
332
|
-
if
|
|
333
|
-
return
|
|
247
|
+
if error := self._require_auth(request):
|
|
248
|
+
return error
|
|
334
249
|
|
|
335
250
|
message_id = f"message-{next(self._message_ids)}"
|
|
336
|
-
self.messages.append({"kind": "channel", "message_id": message_id,
|
|
251
|
+
self.messages.append({"kind": "channel", "message_id": message_id, **payload})
|
|
337
252
|
await self._deliver_to_agents(
|
|
338
253
|
[
|
|
339
|
-
|
|
340
|
-
for
|
|
341
|
-
if
|
|
254
|
+
agent_id
|
|
255
|
+
for agent_id, registration in self.registered_agents.items()
|
|
256
|
+
if registration["name"] != payload["from"]
|
|
342
257
|
],
|
|
343
|
-
sender=
|
|
258
|
+
sender=payload["from"],
|
|
344
259
|
text=payload["text"],
|
|
345
|
-
channel=channel,
|
|
260
|
+
channel=payload["channel"],
|
|
346
261
|
message_id=message_id,
|
|
347
262
|
)
|
|
348
|
-
return web.json_response({
|
|
349
|
-
"ok": True,
|
|
350
|
-
"data": {"id": message_id, "channel_name": channel, "text": payload["text"]},
|
|
351
|
-
}, status=201)
|
|
263
|
+
return web.json_response({"message_id": message_id})
|
|
352
264
|
|
|
353
265
|
async def _handle_reply(self, request: web.Request) -> web.StreamResponse:
|
|
354
|
-
parent_id = request.match_info["message_id"]
|
|
355
266
|
payload = await request.json()
|
|
356
|
-
|
|
357
|
-
sender_name = self.registered_agents.get(agent_id, {}).get("name", "unknown") if agent_id else "unknown"
|
|
358
|
-
self._record_request("reply", request, {**payload, "message_id": parent_id})
|
|
267
|
+
self._record_request("reply", request, payload)
|
|
359
268
|
|
|
360
269
|
if error := self._pop_error("reply"):
|
|
361
270
|
return error
|
|
362
|
-
if
|
|
363
|
-
return
|
|
271
|
+
if error := self._require_auth(request):
|
|
272
|
+
return error
|
|
364
273
|
|
|
365
274
|
reply_id = f"message-{next(self._message_ids)}"
|
|
366
|
-
self.messages.append(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
"data": {"id": reply_id, "text": payload["text"]},
|
|
376
|
-
}, status=201)
|
|
275
|
+
self.messages.append(
|
|
276
|
+
{
|
|
277
|
+
"kind": "reply",
|
|
278
|
+
"reply_id": reply_id,
|
|
279
|
+
"thread_id": payload.get("thread_id") or payload.get("message_id"),
|
|
280
|
+
**payload,
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
return web.json_response({"message_id": reply_id})
|
|
377
284
|
|
|
378
285
|
async def _handle_inbox(self, request: web.Request) -> web.StreamResponse:
|
|
379
|
-
agent_id =
|
|
286
|
+
agent_id = request.match_info["agent_id"]
|
|
380
287
|
self._record_request("check_inbox", request, {"agent_id": agent_id})
|
|
381
288
|
|
|
382
289
|
if error := self._pop_error("check_inbox"):
|
|
383
290
|
return error
|
|
384
|
-
if
|
|
385
|
-
return
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
self.inboxes[agent_id]
|
|
389
|
-
return web.json_response({
|
|
390
|
-
"ok": True,
|
|
391
|
-
"data": {
|
|
392
|
-
"unread_channels": [],
|
|
393
|
-
"mentions": [],
|
|
394
|
-
"unread_dms": unread_dms,
|
|
395
|
-
"recent_reactions": [],
|
|
396
|
-
},
|
|
397
|
-
})
|
|
291
|
+
if error := self._require_auth(request):
|
|
292
|
+
return error
|
|
293
|
+
|
|
294
|
+
messages = list(self.inboxes[agent_id])
|
|
295
|
+
self.inboxes[agent_id].clear()
|
|
296
|
+
return web.json_response({"messages": messages})
|
|
398
297
|
|
|
399
298
|
async def _handle_agents(self, request: web.Request) -> web.StreamResponse:
|
|
400
299
|
self._record_request("list_agents", request, None)
|
|
401
300
|
|
|
402
301
|
if error := self._pop_error("list_agents"):
|
|
403
302
|
return error
|
|
404
|
-
if error := self.
|
|
303
|
+
if error := self._require_auth(request):
|
|
405
304
|
return error
|
|
406
305
|
|
|
407
|
-
|
|
408
|
-
{
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
for name in self.extra_agents:
|
|
412
|
-
agent_list.append({"name": name, "id": f"extra-{name}", "status": "online"})
|
|
413
|
-
|
|
414
|
-
return web.json_response({"ok": True, "data": agent_list})
|
|
306
|
+
agents = sorted(
|
|
307
|
+
{agent["name"] for agent in self.registered_agents.values()} | self.extra_agents
|
|
308
|
+
)
|
|
309
|
+
return web.json_response({"agents": agents})
|
|
415
310
|
|
|
416
311
|
async def _handle_ws(self, request: web.Request) -> web.StreamResponse:
|
|
312
|
+
agent_id = request.match_info["agent_id"]
|
|
417
313
|
token = request.query.get("token")
|
|
418
|
-
|
|
419
|
-
if
|
|
314
|
+
registration = self.registered_agents.get(agent_id)
|
|
315
|
+
if registration is None or token != registration["token"]:
|
|
420
316
|
raise web.HTTPUnauthorized(text="Invalid websocket token")
|
|
421
317
|
|
|
422
318
|
ws = web.WebSocketResponse()
|
|
@@ -437,8 +333,6 @@ class MockRelayServer:
|
|
|
437
333
|
|
|
438
334
|
return ws
|
|
439
335
|
|
|
440
|
-
# --- Helpers ---
|
|
441
|
-
|
|
442
336
|
def _record_request(
|
|
443
337
|
self,
|
|
444
338
|
operation: str,
|
|
@@ -460,23 +354,11 @@ class MockRelayServer:
|
|
|
460
354
|
status, body = self._queued_errors[operation].popleft()
|
|
461
355
|
return web.json_response(body, status=status)
|
|
462
356
|
|
|
463
|
-
def
|
|
464
|
-
|
|
465
|
-
if
|
|
466
|
-
return auth[7:]
|
|
467
|
-
return None
|
|
468
|
-
|
|
469
|
-
def _require_workspace_auth(self, request: web.Request) -> web.StreamResponse | None:
|
|
470
|
-
token = self._extract_token(request)
|
|
471
|
-
if token == self.api_key:
|
|
357
|
+
def _require_auth(self, request: web.Request) -> web.StreamResponse | None:
|
|
358
|
+
auth_header = request.headers.get("Authorization")
|
|
359
|
+
if auth_header == f"Bearer {self.api_key}":
|
|
472
360
|
return None
|
|
473
|
-
return web.json_response({"
|
|
474
|
-
|
|
475
|
-
def _resolve_agent_from_token(self, request: web.Request) -> str | None:
|
|
476
|
-
token = self._extract_token(request)
|
|
477
|
-
if token is None:
|
|
478
|
-
return None
|
|
479
|
-
return self._token_to_agent_id.get(token)
|
|
361
|
+
return web.json_response({"message": "Unauthorized"}, status=401)
|
|
480
362
|
|
|
481
363
|
async def _deliver_to_agents(
|
|
482
364
|
self,
|
|
@@ -492,57 +374,28 @@ class MockRelayServer:
|
|
|
492
374
|
if not agent_ids:
|
|
493
375
|
return
|
|
494
376
|
|
|
377
|
+
payload = {
|
|
378
|
+
"sender": sender,
|
|
379
|
+
"text": text,
|
|
380
|
+
"channel": channel,
|
|
381
|
+
"thread_id": thread_id,
|
|
382
|
+
"message_id": message_id or f"message-{next(self._message_ids)}",
|
|
383
|
+
"timestamp": timestamp,
|
|
384
|
+
}
|
|
385
|
+
|
|
495
386
|
for agent_id in agent_ids:
|
|
496
|
-
await self._deliver_message(agent_id,
|
|
497
|
-
channel=channel, thread_id=thread_id,
|
|
498
|
-
message_id=message_id, timestamp=timestamp)
|
|
387
|
+
await self._deliver_message(agent_id, payload)
|
|
499
388
|
|
|
500
|
-
async def _deliver_message(
|
|
501
|
-
self,
|
|
502
|
-
agent_id: str,
|
|
503
|
-
*,
|
|
504
|
-
sender: str,
|
|
505
|
-
text: str,
|
|
506
|
-
channel: str | None = None,
|
|
507
|
-
thread_id: str | None = None,
|
|
508
|
-
message_id: str | None = None,
|
|
509
|
-
timestamp: float | None = None,
|
|
510
|
-
) -> None:
|
|
389
|
+
async def _deliver_message(self, agent_id: str, payload: dict[str, Any]) -> None:
|
|
511
390
|
ws = self._active_websockets.get(agent_id)
|
|
512
391
|
if ws is not None and not ws.closed:
|
|
513
|
-
payload: dict[str, Any] = {
|
|
514
|
-
"type": "dm.received" if channel is None else "message.created",
|
|
515
|
-
"agent_name": sender,
|
|
516
|
-
"text": text,
|
|
517
|
-
"id": message_id or f"message-{next(self._message_ids)}",
|
|
518
|
-
}
|
|
519
|
-
if channel is not None:
|
|
520
|
-
payload["channel_name"] = channel
|
|
521
|
-
if thread_id is not None:
|
|
522
|
-
payload["thread_id"] = thread_id
|
|
523
|
-
if timestamp is not None:
|
|
524
|
-
payload["created_at"] = str(timestamp)
|
|
525
392
|
try:
|
|
526
|
-
await ws.send_json(payload)
|
|
393
|
+
await ws.send_json({"type": "message", **payload})
|
|
527
394
|
return
|
|
528
395
|
except Exception:
|
|
529
396
|
self._active_websockets.pop(agent_id, None)
|
|
530
397
|
|
|
531
|
-
|
|
532
|
-
conv_id = f"dm_{sender}_{agent_id}"
|
|
533
|
-
dm_msg = {
|
|
534
|
-
"id": message_id or f"message-{next(self._message_ids)}",
|
|
535
|
-
"agent_name": sender,
|
|
536
|
-
"text": text,
|
|
537
|
-
"created_at": str(timestamp) if timestamp else None,
|
|
538
|
-
}
|
|
539
|
-
self._dm_conversations[conv_id].append(dm_msg)
|
|
540
|
-
self.inboxes[agent_id].append({
|
|
541
|
-
"conversation_id": conv_id,
|
|
542
|
-
"from": sender,
|
|
543
|
-
"unread_count": 1,
|
|
544
|
-
"last_message": dm_msg,
|
|
545
|
-
})
|
|
398
|
+
self.inboxes[agent_id].append(dict(payload))
|
|
546
399
|
|
|
547
400
|
|
|
548
401
|
@pytest_asyncio.fixture
|
|
@@ -218,7 +218,7 @@ async def test_swarms_sender_reaches_claude_sdk_hook_system_message(relay_server
|
|
|
218
218
|
receiver_options = SimpleNamespace(mcp_servers=[], hooks=Hooks())
|
|
219
219
|
|
|
220
220
|
swarms_adapter.on_relay(sender_agent, sender_relay)
|
|
221
|
-
claude_adapter.on_relay(
|
|
221
|
+
claude_adapter.on_relay(receiver_options, relay=receiver_relay, name="ClaudeReceiver")
|
|
222
222
|
|
|
223
223
|
try:
|
|
224
224
|
await _prime_claude_receiver(
|
|
@@ -266,7 +266,7 @@ async def test_multiple_framework_agents_post_to_the_same_channel(relay_server,
|
|
|
266
266
|
openai_adapter.on_relay(openai_agent, openai_relay)
|
|
267
267
|
google_adapter.on_relay(google_agent, google_relay)
|
|
268
268
|
swarms_adapter.on_relay(swarms_agent, swarms_relay)
|
|
269
|
-
claude_adapter.on_relay(
|
|
269
|
+
claude_adapter.on_relay(receiver_options, relay=receiver_relay, name="ClaudeChannelReader")
|
|
270
270
|
|
|
271
271
|
try:
|
|
272
272
|
await _prime_claude_receiver(
|
|
@@ -64,30 +64,16 @@ async def _wait_for_agent_absent(
|
|
|
64
64
|
transport: RelayTransport,
|
|
65
65
|
agent_name: str,
|
|
66
66
|
*,
|
|
67
|
-
timeout: float =
|
|
67
|
+
timeout: float = 15.0,
|
|
68
68
|
) -> None:
|
|
69
|
-
"""Wait for an agent to disappear from list_agents or go offline.
|
|
70
|
-
|
|
71
|
-
Relaycast presence may take a few seconds to propagate after disconnect.
|
|
72
|
-
We check both absence from the list and offline status.
|
|
73
|
-
"""
|
|
74
69
|
deadline = asyncio.get_running_loop().time() + timeout
|
|
75
70
|
|
|
76
71
|
while asyncio.get_running_loop().time() < deadline:
|
|
77
|
-
|
|
78
|
-
data = agents_payload.get("data", agents_payload)
|
|
79
|
-
if isinstance(data, list):
|
|
80
|
-
matching = [a for a in data if isinstance(a, dict) and a.get("name") == agent_name]
|
|
81
|
-
if not matching:
|
|
82
|
-
return
|
|
83
|
-
# Also accept "offline" status
|
|
84
|
-
if all(a.get("status") == "offline" for a in matching):
|
|
85
|
-
return
|
|
86
|
-
elif agent_name not in await transport.list_agents():
|
|
72
|
+
if agent_name not in await transport.list_agents():
|
|
87
73
|
return
|
|
88
|
-
await asyncio.sleep(0.
|
|
74
|
+
await asyncio.sleep(0.25)
|
|
89
75
|
|
|
90
|
-
raise AssertionError(f"Timed out waiting for {agent_name!r} to
|
|
76
|
+
raise AssertionError(f"Timed out waiting for {agent_name!r} to disappear from list_agents().")
|
|
91
77
|
|
|
92
78
|
|
|
93
79
|
@pytest.mark.asyncio
|
|
@@ -110,14 +96,9 @@ async def test_register_send_receive_inbox_and_unregister_round_trip():
|
|
|
110
96
|
assert message.text == text
|
|
111
97
|
assert message.sender == sender.agent_name
|
|
112
98
|
|
|
113
|
-
# Verify disconnect completes without error
|
|
114
99
|
receiver_name = receiver.agent_name
|
|
115
100
|
await receiver.disconnect()
|
|
116
|
-
|
|
117
|
-
# Note: Relaycast presence updates are eventually consistent —
|
|
118
|
-
# agents may remain "online" in list_agents for a heartbeat window
|
|
119
|
-
# after disconnect. We verify the disconnect call succeeds rather
|
|
120
|
-
# than waiting for presence propagation.
|
|
101
|
+
await _wait_for_agent_absent(probe, receiver_name)
|
|
121
102
|
finally:
|
|
122
103
|
await asyncio.gather(
|
|
123
104
|
_safe_disconnect(sender),
|
|
@@ -125,6 +106,15 @@ async def test_register_send_receive_inbox_and_unregister_round_trip():
|
|
|
125
106
|
_safe_disconnect(probe),
|
|
126
107
|
)
|
|
127
108
|
|
|
109
|
+
cleanup_probe = RelayTransport(_unique_name("sdk-py-e2e-cleanup"), config)
|
|
110
|
+
await cleanup_probe.connect()
|
|
111
|
+
try:
|
|
112
|
+
agents = await cleanup_probe.list_agents()
|
|
113
|
+
assert sender.agent_name not in agents
|
|
114
|
+
assert receiver.agent_name not in agents
|
|
115
|
+
finally:
|
|
116
|
+
await _safe_disconnect(cleanup_probe)
|
|
117
|
+
|
|
128
118
|
|
|
129
119
|
@pytest.mark.asyncio
|
|
130
120
|
async def test_two_agents_can_exchange_bidirectional_messages():
|