agent-relay 3.2.15 → 3.2.17
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 +3865 -17179
- package/dist/src/cli/commands/setup.d.ts.map +1 -1
- package/dist/src/cli/commands/setup.js +2 -0
- package/dist/src/cli/commands/setup.js.map +1 -1
- 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 +169 -28
- 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 +197 -30
- 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/builder.py +64 -7
- 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/src/agent_relay/types.py +1 -0
- 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_builder.py +58 -0
- package/packages/sdk-py/tests/test_dry_run.py +215 -0
- 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
|
@@ -9,10 +9,10 @@ if TYPE_CHECKING:
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _format_instructions_with_inbox(messages: list[Any], base_instructions: str) -> str:
|
|
12
|
-
content = "
|
|
12
|
+
content = "New messages from other agents:\n"
|
|
13
13
|
for message in messages:
|
|
14
14
|
content += f" {message.sender}: {message.text}\n"
|
|
15
|
-
return f"{
|
|
15
|
+
return f"{base_instructions}\n\n{content}" if base_instructions else content
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
@@ -45,11 +45,8 @@ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
|
45
45
|
|
|
46
46
|
agent.tools.extend([relay_send, relay_inbox, relay_post, relay_agents])
|
|
47
47
|
|
|
48
|
-
# 2. Wrap instructions
|
|
48
|
+
# 2. Wrap instructions
|
|
49
49
|
orig_instructions = agent.instructions
|
|
50
|
-
pending_messages: list[Any] = []
|
|
51
|
-
|
|
52
|
-
relay.on_message(lambda msg: pending_messages.append(msg))
|
|
53
50
|
|
|
54
51
|
async def instructions_wrapper(*args: Any, **kwargs: Any) -> str:
|
|
55
52
|
if callable(orig_instructions):
|
|
@@ -63,11 +60,10 @@ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
|
63
60
|
base = orig_instructions
|
|
64
61
|
|
|
65
62
|
base = base or ""
|
|
66
|
-
|
|
63
|
+
messages = await relay.inbox()
|
|
64
|
+
if not messages:
|
|
67
65
|
return base
|
|
68
66
|
|
|
69
|
-
messages = list(pending_messages)
|
|
70
|
-
pending_messages.clear()
|
|
71
67
|
return _format_instructions_with_inbox(messages, base)
|
|
72
68
|
|
|
73
69
|
agent.instructions = instructions_wrapper
|
|
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
|
|
9
9
|
from ..core import Relay
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def on_relay(
|
|
12
|
+
def on_relay(options: Any, relay: "Relay | None" = None, *, name: str | None = None) -> Any:
|
|
13
13
|
"""Wrap Claude Agent SDK query options to connect them to the relay."""
|
|
14
14
|
try:
|
|
15
15
|
from claude_agent_sdk.types import HookResult
|
|
@@ -24,10 +24,8 @@ def on_relay(name: str, options: Any, relay: "Relay | None" = None) -> Any:
|
|
|
24
24
|
from ..core import Relay
|
|
25
25
|
relay = Relay(agent_name)
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
hooks = SimpleNamespace(post_tool_use=None, stop=None)
|
|
30
|
-
options.hooks = hooks
|
|
27
|
+
if getattr(options, "hooks", None) is None:
|
|
28
|
+
options.hooks = SimpleNamespace(post_tool_use=None, stop=None)
|
|
31
29
|
|
|
32
30
|
# 1. Inject Relaycast MCP server
|
|
33
31
|
mcp_config = {"name": "relaycast", "command": "agent-relay", "args": ["mcp"]}
|
|
@@ -46,8 +44,8 @@ def on_relay(name: str, options: Any, relay: "Relay | None" = None) -> Any:
|
|
|
46
44
|
return content
|
|
47
45
|
|
|
48
46
|
# 3. Hook wrappers
|
|
49
|
-
orig_post =
|
|
50
|
-
orig_stop =
|
|
47
|
+
orig_post = options.hooks.post_tool_use if hasattr(options.hooks, "post_tool_use") else None
|
|
48
|
+
orig_stop = options.hooks.stop if hasattr(options.hooks, "stop") else None
|
|
51
49
|
|
|
52
50
|
async def post_tool_use_hook(*args, **kwargs):
|
|
53
51
|
res = None
|
|
@@ -47,18 +47,9 @@ class _RelayBackstory:
|
|
|
47
47
|
def _resolve_sync(self) -> str:
|
|
48
48
|
messages = self._drain_buffer()
|
|
49
49
|
try:
|
|
50
|
-
loop = asyncio.get_running_loop()
|
|
51
|
-
except RuntimeError:
|
|
52
|
-
loop = None
|
|
53
|
-
|
|
54
|
-
if loop is None:
|
|
55
50
|
messages.extend(self._relay.inbox_sync())
|
|
56
|
-
|
|
57
|
-
#
|
|
58
|
-
import concurrent.futures
|
|
59
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
60
|
-
polled = pool.submit(asyncio.run, self._relay.inbox()).result()
|
|
61
|
-
messages.extend(polled)
|
|
51
|
+
except Exception:
|
|
52
|
+
pass # Buffer messages are still available
|
|
62
53
|
return _format_backstory(self._dedupe(messages), self._base_backstory)
|
|
63
54
|
|
|
64
55
|
async def _resolve_async(self) -> str:
|
|
@@ -136,8 +127,7 @@ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
|
136
127
|
def _buffer_message(message: Any) -> None:
|
|
137
128
|
backstory_buffer.append(message)
|
|
138
129
|
|
|
139
|
-
|
|
130
|
+
relay.on_message(_buffer_message)
|
|
140
131
|
agent.backstory = _RelayBackstory(relay, agent.backstory or "", backstory_buffer)
|
|
141
|
-
agent._relay_unsubscribe = unsubscribe # type: ignore[attr-defined]
|
|
142
132
|
|
|
143
133
|
return agent
|
|
@@ -47,7 +47,10 @@ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
|
47
47
|
if inspect.isawaitable(orig_result):
|
|
48
48
|
orig_result = await orig_result
|
|
49
49
|
|
|
50
|
-
#
|
|
50
|
+
# If original callback short-circuited (returned Content), respect that
|
|
51
|
+
if orig_result is not None:
|
|
52
|
+
return orig_result
|
|
53
|
+
|
|
51
54
|
messages = await relay.inbox()
|
|
52
55
|
if messages:
|
|
53
56
|
from google.genai.types import Content, Part
|
|
@@ -63,7 +66,7 @@ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
|
63
66
|
)
|
|
64
67
|
)
|
|
65
68
|
|
|
66
|
-
return
|
|
69
|
+
return None
|
|
67
70
|
|
|
68
71
|
agent.before_model_callback = relay_callback
|
|
69
72
|
return agent
|
|
@@ -10,10 +10,10 @@ if TYPE_CHECKING:
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def _format_instructions_with_inbox(messages: list[Any], base_instructions: str) -> str:
|
|
13
|
-
content = "
|
|
13
|
+
content = "New messages from other agents:\n"
|
|
14
14
|
for message in messages:
|
|
15
15
|
content += f" {message.sender}: {message.text}\n"
|
|
16
|
-
return f"{
|
|
16
|
+
return f"{base_instructions}\n\n{content}" if base_instructions else content
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
@@ -57,11 +57,8 @@ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
|
57
57
|
function_tool(relay_agents)
|
|
58
58
|
])
|
|
59
59
|
|
|
60
|
-
# 2. Wrap instructions
|
|
60
|
+
# 2. Wrap instructions
|
|
61
61
|
orig_instructions = agent.instructions
|
|
62
|
-
pending_messages: list[Any] = []
|
|
63
|
-
|
|
64
|
-
relay.on_message(lambda msg: pending_messages.append(msg))
|
|
65
62
|
|
|
66
63
|
async def instructions_wrapper(*args: Any, **kwargs: Any) -> str:
|
|
67
64
|
if callable(orig_instructions):
|
|
@@ -75,11 +72,10 @@ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
|
75
72
|
base = orig_instructions
|
|
76
73
|
|
|
77
74
|
base = base or ""
|
|
78
|
-
|
|
75
|
+
messages = await relay.inbox()
|
|
76
|
+
if not messages:
|
|
79
77
|
return base
|
|
80
78
|
|
|
81
|
-
messages = list(pending_messages)
|
|
82
|
-
pending_messages.clear()
|
|
83
79
|
return _format_instructions_with_inbox(messages, base)
|
|
84
80
|
|
|
85
81
|
agent.instructions = instructions_wrapper
|
|
@@ -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)
|
|
@@ -562,6 +562,7 @@ class RunOptions:
|
|
|
562
562
|
vars: dict[str, str | int | bool] | None = None
|
|
563
563
|
trajectories: TrajectoryConfig | Literal[False] | dict[str, Any] | bool | None = None
|
|
564
564
|
on_event: WorkflowEventCallback | None = None
|
|
565
|
+
dry_run: bool | None = None
|
|
565
566
|
|
|
566
567
|
|
|
567
568
|
@dataclass
|