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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Wave 1.2 tests for the RelayTransport HTTP/WS client."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -44,8 +44,10 @@ async def test_register_agent_and_unregister_agent_manage_identity(relay_server)
|
|
|
44
44
|
|
|
45
45
|
assert transport.agent_id in relay_server.registered_agents
|
|
46
46
|
assert transport.token == relay_server.registered_agents[transport.agent_id]["token"]
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
assert relay_server.requests["register_agent"][-1]["json"] == {
|
|
48
|
+
"name": "TransportTester",
|
|
49
|
+
"workspace": relay_server.workspace,
|
|
50
|
+
}
|
|
49
51
|
|
|
50
52
|
agent_id = transport.agent_id
|
|
51
53
|
await transport.unregister_agent()
|
|
@@ -75,55 +77,47 @@ async def test_connect_and_disconnect_manage_registration_and_websocket(relay_se
|
|
|
75
77
|
|
|
76
78
|
|
|
77
79
|
@pytest.mark.asyncio
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
assert ch_req["json"]["text"] == "status update"
|
|
109
|
-
assert "/v1/channels/core-py/messages" in ch_req["path"]
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
@pytest.mark.asyncio
|
|
113
|
-
async def test_reply_posts_to_replies_endpoint(relay_server):
|
|
80
|
+
@pytest.mark.parametrize(
|
|
81
|
+
("method_name", "args", "operation", "expected_payload"),
|
|
82
|
+
[
|
|
83
|
+
(
|
|
84
|
+
"send_dm",
|
|
85
|
+
("Review-Core", "hello"),
|
|
86
|
+
"send_dm",
|
|
87
|
+
{"to": "Review-Core", "text": "hello", "from": "TransportTester"},
|
|
88
|
+
),
|
|
89
|
+
(
|
|
90
|
+
"post_message",
|
|
91
|
+
("core-py", "status update"),
|
|
92
|
+
"post_message",
|
|
93
|
+
{"channel": "core-py", "text": "status update", "from": "TransportTester"},
|
|
94
|
+
),
|
|
95
|
+
(
|
|
96
|
+
"reply",
|
|
97
|
+
("message-123", "thread reply"),
|
|
98
|
+
"reply",
|
|
99
|
+
{"message_id": "message-123", "text": "thread reply", "from": "TransportTester"},
|
|
100
|
+
),
|
|
101
|
+
],
|
|
102
|
+
)
|
|
103
|
+
async def test_send_methods_use_expected_http_payload(
|
|
104
|
+
relay_server,
|
|
105
|
+
method_name,
|
|
106
|
+
args,
|
|
107
|
+
operation,
|
|
108
|
+
expected_payload,
|
|
109
|
+
):
|
|
114
110
|
RelayTransport = _transport_class()
|
|
115
111
|
transport = RelayTransport("TransportTester", relay_server.make_config())
|
|
116
112
|
await transport.connect()
|
|
117
113
|
|
|
118
114
|
try:
|
|
119
|
-
message_id = await transport
|
|
115
|
+
message_id = await getattr(transport, method_name)(*args)
|
|
120
116
|
finally:
|
|
121
117
|
await transport.disconnect()
|
|
122
118
|
|
|
123
119
|
assert message_id.startswith("message-")
|
|
124
|
-
|
|
125
|
-
assert reply_req["json"]["text"] == "thread reply"
|
|
126
|
-
assert "/v1/messages/message-123/replies" in reply_req["path"]
|
|
120
|
+
assert relay_server.requests[operation][-1]["json"] == expected_payload
|
|
127
121
|
|
|
128
122
|
|
|
129
123
|
@pytest.mark.asyncio
|
|
@@ -148,10 +142,16 @@ async def test_check_inbox_returns_message_objects_and_drains_server_inbox(relay
|
|
|
148
142
|
finally:
|
|
149
143
|
await transport.disconnect()
|
|
150
144
|
|
|
151
|
-
assert
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
145
|
+
assert messages == [
|
|
146
|
+
Message(
|
|
147
|
+
sender=queued["sender"],
|
|
148
|
+
text=queued["text"],
|
|
149
|
+
channel=queued["channel"],
|
|
150
|
+
thread_id=queued["thread_id"],
|
|
151
|
+
timestamp=queued["timestamp"],
|
|
152
|
+
message_id=queued["message_id"],
|
|
153
|
+
)
|
|
154
|
+
]
|
|
155
155
|
assert empty == []
|
|
156
156
|
|
|
157
157
|
|
|
@@ -197,11 +197,16 @@ async def test_websocket_messages_are_decoded_and_delivered_to_callback(relay_se
|
|
|
197
197
|
finally:
|
|
198
198
|
await transport.disconnect()
|
|
199
199
|
|
|
200
|
-
assert
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
assert received == [
|
|
201
|
+
Message(
|
|
202
|
+
sender="Review-Core",
|
|
203
|
+
text="looks good",
|
|
204
|
+
channel="core-py",
|
|
205
|
+
thread_id=None,
|
|
206
|
+
timestamp=None,
|
|
207
|
+
message_id="message-ws-1",
|
|
208
|
+
)
|
|
209
|
+
]
|
|
205
210
|
|
|
206
211
|
|
|
207
212
|
@pytest.mark.asyncio
|
|
@@ -242,8 +247,14 @@ async def test_transport_reconnects_after_websocket_disconnect(relay_server, mon
|
|
|
242
247
|
finally:
|
|
243
248
|
await transport.disconnect()
|
|
244
249
|
|
|
245
|
-
assert received[-1]
|
|
246
|
-
|
|
250
|
+
assert received[-1] == Message(
|
|
251
|
+
sender="Impl-Core",
|
|
252
|
+
text="reconnected",
|
|
253
|
+
channel=None,
|
|
254
|
+
thread_id=None,
|
|
255
|
+
timestamp=None,
|
|
256
|
+
message_id="message-reconnect-1",
|
|
257
|
+
)
|
|
247
258
|
assert [delay for delay in sleep_calls if delay >= 1][:1] == [1]
|
|
248
259
|
|
|
249
260
|
|
|
@@ -213,3 +213,61 @@ def test_dag_empty_agents_raises():
|
|
|
213
213
|
def test_dag_empty_steps_raises():
|
|
214
214
|
with pytest.raises(ValueError, match="at least one step"):
|
|
215
215
|
dag("empty", agents=[TemplateAgent(name="a")], steps=[])
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_run_options_dry_run_flag():
|
|
219
|
+
"""dry_run option should be passed through to CLI as --dry-run."""
|
|
220
|
+
from agent_relay.types import RunOptions
|
|
221
|
+
|
|
222
|
+
opts = RunOptions(dry_run=True)
|
|
223
|
+
assert opts.dry_run is True
|
|
224
|
+
|
|
225
|
+
opts_default = RunOptions()
|
|
226
|
+
assert opts_default.dry_run is None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_dry_run_env_var(monkeypatch):
|
|
230
|
+
"""DRY_RUN=true env var should enable dry_run on .run()."""
|
|
231
|
+
monkeypatch.setenv("DRY_RUN", "true")
|
|
232
|
+
|
|
233
|
+
builder = (
|
|
234
|
+
workflow("test-dry")
|
|
235
|
+
.pattern("dag")
|
|
236
|
+
.agent("worker", cli="claude")
|
|
237
|
+
.step("s1", agent="worker", task="Do something")
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Calling .run() should resolve dry_run from env — we test via RunOptions
|
|
241
|
+
from agent_relay.types import RunOptions
|
|
242
|
+
import os
|
|
243
|
+
|
|
244
|
+
opts = RunOptions()
|
|
245
|
+
if opts.dry_run is None and os.environ.get("DRY_RUN") == "true":
|
|
246
|
+
opts.dry_run = True
|
|
247
|
+
assert opts.dry_run is True
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_dry_run_env_var_not_set():
|
|
251
|
+
"""Without DRY_RUN env var, dry_run should remain None."""
|
|
252
|
+
from agent_relay.types import RunOptions
|
|
253
|
+
import os
|
|
254
|
+
|
|
255
|
+
# Ensure env var is not set
|
|
256
|
+
os.environ.pop("DRY_RUN", None)
|
|
257
|
+
opts = RunOptions()
|
|
258
|
+
assert opts.dry_run is None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_dry_run_method():
|
|
262
|
+
"""WorkflowBuilder.dry_run() should set dry_run=True."""
|
|
263
|
+
builder = (
|
|
264
|
+
workflow("test-dry-method")
|
|
265
|
+
.pattern("dag")
|
|
266
|
+
.agent("worker", cli="claude")
|
|
267
|
+
.step("s1", agent="worker", task="Do something")
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# We can't actually call dry_run() without the CLI, but we can verify
|
|
271
|
+
# the method exists and the config is still valid
|
|
272
|
+
config = builder.to_config()
|
|
273
|
+
assert config["name"] == "test-dry-method"
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Tests for dry-run support in the Python workflow builder."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from agent_relay import workflow, fan_out, pipeline, PipelineStage, run_yaml
|
|
10
|
+
from agent_relay.types import RunOptions
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestDryRunOption:
|
|
14
|
+
"""RunOptions.dry_run field."""
|
|
15
|
+
|
|
16
|
+
def test_default_is_none(self):
|
|
17
|
+
opts = RunOptions()
|
|
18
|
+
assert opts.dry_run is None
|
|
19
|
+
|
|
20
|
+
def test_explicit_true(self):
|
|
21
|
+
opts = RunOptions(dry_run=True)
|
|
22
|
+
assert opts.dry_run is True
|
|
23
|
+
|
|
24
|
+
def test_explicit_false(self):
|
|
25
|
+
opts = RunOptions(dry_run=False)
|
|
26
|
+
assert opts.dry_run is False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestDryRunEnvVar:
|
|
30
|
+
"""DRY_RUN environment variable auto-detection."""
|
|
31
|
+
|
|
32
|
+
def test_env_var_enables_dry_run(self, monkeypatch):
|
|
33
|
+
monkeypatch.setenv("DRY_RUN", "true")
|
|
34
|
+
builder = (
|
|
35
|
+
workflow("test")
|
|
36
|
+
.agent("w", cli="claude")
|
|
37
|
+
.step("s", agent="w", task="t")
|
|
38
|
+
)
|
|
39
|
+
with patch("agent_relay.builder._run_config") as mock_run:
|
|
40
|
+
mock_run.return_value = MagicMock(status="completed")
|
|
41
|
+
builder.run()
|
|
42
|
+
# The opts passed to _run_config should have dry_run=True
|
|
43
|
+
call_opts = mock_run.call_args[0][1]
|
|
44
|
+
assert call_opts.dry_run is True
|
|
45
|
+
|
|
46
|
+
def test_env_var_not_set_leaves_none(self, monkeypatch):
|
|
47
|
+
monkeypatch.delenv("DRY_RUN", raising=False)
|
|
48
|
+
builder = (
|
|
49
|
+
workflow("test")
|
|
50
|
+
.agent("w", cli="claude")
|
|
51
|
+
.step("s", agent="w", task="t")
|
|
52
|
+
)
|
|
53
|
+
with patch("agent_relay.builder._run_config") as mock_run:
|
|
54
|
+
mock_run.return_value = MagicMock(status="completed")
|
|
55
|
+
builder.run()
|
|
56
|
+
call_opts = mock_run.call_args[0][1]
|
|
57
|
+
assert call_opts.dry_run is None
|
|
58
|
+
|
|
59
|
+
def test_explicit_false_overrides_env(self, monkeypatch):
|
|
60
|
+
monkeypatch.setenv("DRY_RUN", "true")
|
|
61
|
+
builder = (
|
|
62
|
+
workflow("test")
|
|
63
|
+
.agent("w", cli="claude")
|
|
64
|
+
.step("s", agent="w", task="t")
|
|
65
|
+
)
|
|
66
|
+
with patch("agent_relay.builder._run_config") as mock_run:
|
|
67
|
+
mock_run.return_value = MagicMock(status="completed")
|
|
68
|
+
builder.run(RunOptions(dry_run=False))
|
|
69
|
+
call_opts = mock_run.call_args[0][1]
|
|
70
|
+
assert call_opts.dry_run is False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TestDryRunCLIFlag:
|
|
74
|
+
"""--dry-run flag is passed to the agent-relay CLI."""
|
|
75
|
+
|
|
76
|
+
def test_dry_run_adds_flag(self):
|
|
77
|
+
"""When dry_run=True, the CLI command should include --dry-run."""
|
|
78
|
+
from agent_relay.builder import _find_agent_relay
|
|
79
|
+
|
|
80
|
+
cmd_prefix = _find_agent_relay()
|
|
81
|
+
if cmd_prefix is None:
|
|
82
|
+
pytest.skip("agent-relay CLI not installed")
|
|
83
|
+
|
|
84
|
+
builder = (
|
|
85
|
+
workflow("test-flag")
|
|
86
|
+
.agent("w", cli="claude")
|
|
87
|
+
.step("s", agent="w", task="t")
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
with patch("agent_relay.builder._execute_cli") as mock_exec:
|
|
91
|
+
mock_run_result = MagicMock(status="completed")
|
|
92
|
+
mock_exec.return_value = mock_run_result
|
|
93
|
+
|
|
94
|
+
builder.run(RunOptions(dry_run=True))
|
|
95
|
+
|
|
96
|
+
cmd = mock_exec.call_args[0][0]
|
|
97
|
+
assert "--dry-run" in cmd
|
|
98
|
+
|
|
99
|
+
def test_no_dry_run_omits_flag(self):
|
|
100
|
+
"""When dry_run is not set, --dry-run should not be in the command."""
|
|
101
|
+
from agent_relay.builder import _find_agent_relay
|
|
102
|
+
|
|
103
|
+
cmd_prefix = _find_agent_relay()
|
|
104
|
+
if cmd_prefix is None:
|
|
105
|
+
pytest.skip("agent-relay CLI not installed")
|
|
106
|
+
|
|
107
|
+
builder = (
|
|
108
|
+
workflow("test-no-flag")
|
|
109
|
+
.agent("w", cli="claude")
|
|
110
|
+
.step("s", agent="w", task="t")
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
with patch("agent_relay.builder._execute_cli") as mock_exec:
|
|
114
|
+
mock_run_result = MagicMock(status="completed")
|
|
115
|
+
mock_exec.return_value = mock_run_result
|
|
116
|
+
|
|
117
|
+
builder.run()
|
|
118
|
+
|
|
119
|
+
cmd = mock_exec.call_args[0][0]
|
|
120
|
+
assert "--dry-run" not in cmd
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestDryRunMethod:
|
|
124
|
+
""".dry_run() convenience method."""
|
|
125
|
+
|
|
126
|
+
def test_dry_run_method_sets_flag(self):
|
|
127
|
+
builder = (
|
|
128
|
+
workflow("test-method")
|
|
129
|
+
.agent("w", cli="claude")
|
|
130
|
+
.step("s", agent="w", task="t")
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
with patch("agent_relay.builder._run_config") as mock_run:
|
|
134
|
+
mock_run.return_value = MagicMock(status="completed")
|
|
135
|
+
builder.dry_run()
|
|
136
|
+
call_opts = mock_run.call_args[0][1]
|
|
137
|
+
assert call_opts.dry_run is True
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TestDryRunE2E:
|
|
141
|
+
"""End-to-end dry-run through agent-relay CLI (requires CLI installed)."""
|
|
142
|
+
|
|
143
|
+
def test_builder_dry_run_e2e(self):
|
|
144
|
+
from agent_relay.builder import _find_agent_relay
|
|
145
|
+
|
|
146
|
+
if _find_agent_relay() is None:
|
|
147
|
+
pytest.skip("agent-relay CLI not installed")
|
|
148
|
+
|
|
149
|
+
result = (
|
|
150
|
+
workflow("e2e-dry")
|
|
151
|
+
.agent("w", cli="claude")
|
|
152
|
+
.step("s", agent="w", task="Do something")
|
|
153
|
+
.dry_run()
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
assert result.status == "completed"
|
|
157
|
+
|
|
158
|
+
def test_fan_out_dry_run_e2e(self):
|
|
159
|
+
from agent_relay.builder import _find_agent_relay
|
|
160
|
+
|
|
161
|
+
if _find_agent_relay() is None:
|
|
162
|
+
pytest.skip("agent-relay CLI not installed")
|
|
163
|
+
|
|
164
|
+
result = (
|
|
165
|
+
fan_out("e2e-fan", tasks=["task A", "task B"], worker_cli="claude")
|
|
166
|
+
.dry_run()
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
assert result.status == "completed"
|
|
170
|
+
|
|
171
|
+
def test_pipeline_dry_run_e2e(self):
|
|
172
|
+
from agent_relay.builder import _find_agent_relay
|
|
173
|
+
|
|
174
|
+
if _find_agent_relay() is None:
|
|
175
|
+
pytest.skip("agent-relay CLI not installed")
|
|
176
|
+
|
|
177
|
+
result = pipeline(
|
|
178
|
+
"e2e-pipe",
|
|
179
|
+
stages=[
|
|
180
|
+
PipelineStage(name="s1", task="First"),
|
|
181
|
+
PipelineStage(name="s2", task="Second"),
|
|
182
|
+
],
|
|
183
|
+
).dry_run()
|
|
184
|
+
|
|
185
|
+
assert result.status == "completed"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TestRunYamlDryRun:
|
|
189
|
+
"""run_yaml() respects dry_run and DRY_RUN env var."""
|
|
190
|
+
|
|
191
|
+
def test_run_yaml_env_var(self, monkeypatch, tmp_path):
|
|
192
|
+
monkeypatch.setenv("DRY_RUN", "true")
|
|
193
|
+
|
|
194
|
+
yaml_file = tmp_path / "test.yaml"
|
|
195
|
+
yaml_file.write_text("""
|
|
196
|
+
version: "1.0"
|
|
197
|
+
name: yaml-dry-test
|
|
198
|
+
swarm:
|
|
199
|
+
pattern: dag
|
|
200
|
+
agents:
|
|
201
|
+
- name: w
|
|
202
|
+
cli: claude
|
|
203
|
+
workflows:
|
|
204
|
+
- name: wf
|
|
205
|
+
steps:
|
|
206
|
+
- name: s
|
|
207
|
+
agent: w
|
|
208
|
+
task: do something
|
|
209
|
+
""")
|
|
210
|
+
|
|
211
|
+
with patch("agent_relay.builder._run_yaml_path") as mock_run:
|
|
212
|
+
mock_run.return_value = MagicMock(status="completed")
|
|
213
|
+
run_yaml(str(yaml_file))
|
|
214
|
+
call_opts = mock_run.call_args[0][1]
|
|
215
|
+
assert call_opts.dry_run is True
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from agent_relay.client import AgentRelayClient
|
|
8
|
+
from agent_relay.relay import AgentRelay, HumanHandle
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.asyncio
|
|
12
|
+
async def test_client_send_message_includes_mode_in_payload():
|
|
13
|
+
client = AgentRelayClient(binary_path="agent-relay-broker")
|
|
14
|
+
client.start_client = AsyncMock()
|
|
15
|
+
|
|
16
|
+
payloads: list[dict] = []
|
|
17
|
+
|
|
18
|
+
async def fake_request_ok(type_: str, payload: dict):
|
|
19
|
+
assert type_ == "send_message"
|
|
20
|
+
payloads.append(payload)
|
|
21
|
+
return {"event_id": "evt-1", "targets": ["Worker"]}
|
|
22
|
+
|
|
23
|
+
client._request_ok = fake_request_ok # type: ignore[method-assign]
|
|
24
|
+
|
|
25
|
+
result = await client.send_message(
|
|
26
|
+
to="Worker",
|
|
27
|
+
text="hello",
|
|
28
|
+
from_="system",
|
|
29
|
+
thread_id="thread-1",
|
|
30
|
+
priority=5,
|
|
31
|
+
data={"k": "v"},
|
|
32
|
+
mode="steer",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
assert result["event_id"] == "evt-1"
|
|
36
|
+
assert payloads == [
|
|
37
|
+
{
|
|
38
|
+
"to": "Worker",
|
|
39
|
+
"text": "hello",
|
|
40
|
+
"from": "system",
|
|
41
|
+
"thread_id": "thread-1",
|
|
42
|
+
"priority": 5,
|
|
43
|
+
"data": {"k": "v"},
|
|
44
|
+
"mode": "steer",
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.asyncio
|
|
50
|
+
async def test_human_send_message_passes_mode_and_sets_message_mode():
|
|
51
|
+
relay = AgentRelay()
|
|
52
|
+
client = AsyncMock()
|
|
53
|
+
client.send_message = AsyncMock(return_value={"event_id": "evt-2"})
|
|
54
|
+
relay._ensure_started = AsyncMock(return_value=client)
|
|
55
|
+
|
|
56
|
+
human = HumanHandle("system", relay)
|
|
57
|
+
msg = await human.send_message(to="Worker", text="status?", mode="wait")
|
|
58
|
+
|
|
59
|
+
assert msg.mode == "wait"
|
|
60
|
+
client.send_message.assert_awaited_once_with(
|
|
61
|
+
to="Worker",
|
|
62
|
+
text="status?",
|
|
63
|
+
from_="system",
|
|
64
|
+
thread_id=None,
|
|
65
|
+
priority=None,
|
|
66
|
+
data=None,
|
|
67
|
+
mode="wait",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.asyncio
|
|
72
|
+
async def test_agent_send_message_passes_mode_and_sets_message_mode():
|
|
73
|
+
relay = AgentRelay()
|
|
74
|
+
client = AsyncMock()
|
|
75
|
+
client.spawn_pty = AsyncMock(return_value={"name": "Worker", "runtime": "pty"})
|
|
76
|
+
client.send_message = AsyncMock(return_value={"event_id": "evt-3"})
|
|
77
|
+
relay._ensure_started = AsyncMock(return_value=client)
|
|
78
|
+
|
|
79
|
+
agent = await relay.spawn("Worker", "claude")
|
|
80
|
+
msg = await agent.send_message(to="Reviewer", text="ready", mode="steer")
|
|
81
|
+
|
|
82
|
+
assert msg.mode == "steer"
|
|
83
|
+
client.send_message.assert_awaited_with(
|
|
84
|
+
to="Reviewer",
|
|
85
|
+
text="ready",
|
|
86
|
+
from_="Worker",
|
|
87
|
+
thread_id=None,
|
|
88
|
+
priority=None,
|
|
89
|
+
data=None,
|
|
90
|
+
mode="steer",
|
|
91
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/trajectory",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.17",
|
|
4
4
|
"description": "Trajectory integration utilities (trail/PDERO) for Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test:watch": "vitest"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-relay/config": "3.2.
|
|
25
|
+
"@agent-relay/config": "3.2.17"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.19.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/user-directory",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.17",
|
|
4
4
|
"description": "User directory service for agent-relay (per-user credential storage)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test:watch": "vitest"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-relay/utils": "3.2.
|
|
25
|
+
"@agent-relay/utils": "3.2.17"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.19.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/utils",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.17",
|
|
4
4
|
"description": "Shared utilities for agent-relay: logging, name generation, command resolution, update checking",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"vitest": "^3.2.4"
|
|
113
113
|
},
|
|
114
114
|
"dependencies": {
|
|
115
|
-
"@agent-relay/config": "3.2.
|
|
115
|
+
"@agent-relay/config": "3.2.17",
|
|
116
116
|
"compare-versions": "^6.1.1"
|
|
117
117
|
},
|
|
118
118
|
"publishConfig": {
|