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.
Files changed (140) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/dist/index.cjs +3865 -17179
  6. package/dist/src/cli/commands/setup.d.ts.map +1 -1
  7. package/dist/src/cli/commands/setup.js +2 -0
  8. package/dist/src/cli/commands/setup.js.map +1 -1
  9. package/package.json +8 -8
  10. package/packages/acp-bridge/package.json +2 -2
  11. package/packages/config/package.json +1 -1
  12. package/packages/hooks/package.json +4 -4
  13. package/packages/memory/package.json +2 -2
  14. package/packages/openclaw/package.json +2 -2
  15. package/packages/policy/package.json +2 -2
  16. package/packages/sdk/dist/broker-path.d.ts +19 -0
  17. package/packages/sdk/dist/broker-path.d.ts.map +1 -0
  18. package/packages/sdk/dist/broker-path.js +71 -0
  19. package/packages/sdk/dist/broker-path.js.map +1 -0
  20. package/packages/sdk/dist/cli-registry.d.ts.map +1 -1
  21. package/packages/sdk/dist/cli-registry.js +4 -0
  22. package/packages/sdk/dist/cli-registry.js.map +1 -1
  23. package/packages/sdk/dist/client.d.ts +6 -1
  24. package/packages/sdk/dist/client.d.ts.map +1 -1
  25. package/packages/sdk/dist/client.js +18 -0
  26. package/packages/sdk/dist/client.js.map +1 -1
  27. package/packages/sdk/dist/communicate/adapters/index.d.ts +0 -5
  28. package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -1
  29. package/packages/sdk/dist/communicate/adapters/index.js +0 -5
  30. package/packages/sdk/dist/communicate/adapters/index.js.map +1 -1
  31. package/packages/sdk/dist/communicate/adapters/pi.d.ts +0 -1
  32. package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -1
  33. package/packages/sdk/dist/communicate/adapters/pi.js +0 -4
  34. package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -1
  35. package/packages/sdk/dist/communicate/core.d.ts.map +1 -1
  36. package/packages/sdk/dist/communicate/core.js +2 -3
  37. package/packages/sdk/dist/communicate/core.js.map +1 -1
  38. package/packages/sdk/dist/communicate/index.d.ts +17 -1
  39. package/packages/sdk/dist/communicate/index.d.ts.map +1 -1
  40. package/packages/sdk/dist/communicate/index.js +40 -1
  41. package/packages/sdk/dist/communicate/index.js.map +1 -1
  42. package/packages/sdk/dist/communicate/transport.d.ts +0 -1
  43. package/packages/sdk/dist/communicate/transport.d.ts.map +1 -1
  44. package/packages/sdk/dist/communicate/transport.js +42 -134
  45. package/packages/sdk/dist/communicate/transport.js.map +1 -1
  46. package/packages/sdk/dist/http.d.ts +38 -0
  47. package/packages/sdk/dist/http.d.ts.map +1 -0
  48. package/packages/sdk/dist/http.js +60 -0
  49. package/packages/sdk/dist/http.js.map +1 -0
  50. package/packages/sdk/dist/protocol.d.ts +25 -0
  51. package/packages/sdk/dist/protocol.d.ts.map +1 -1
  52. package/packages/sdk/dist/relay.d.ts +26 -3
  53. package/packages/sdk/dist/relay.d.ts.map +1 -1
  54. package/packages/sdk/dist/relay.js +62 -4
  55. package/packages/sdk/dist/relay.js.map +1 -1
  56. package/packages/sdk/dist/workflows/api-executor.d.ts +16 -0
  57. package/packages/sdk/dist/workflows/api-executor.d.ts.map +1 -0
  58. package/packages/sdk/dist/workflows/api-executor.js +94 -0
  59. package/packages/sdk/dist/workflows/api-executor.js.map +1 -0
  60. package/packages/sdk/dist/workflows/builder.d.ts +14 -0
  61. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  62. package/packages/sdk/dist/workflows/builder.js +26 -0
  63. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  64. package/packages/sdk/dist/workflows/cloud-runner.d.ts +15 -0
  65. package/packages/sdk/dist/workflows/cloud-runner.d.ts.map +1 -0
  66. package/packages/sdk/dist/workflows/cloud-runner.js +41 -0
  67. package/packages/sdk/dist/workflows/cloud-runner.js.map +1 -0
  68. package/packages/sdk/dist/workflows/index.d.ts +2 -0
  69. package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
  70. package/packages/sdk/dist/workflows/index.js +1 -0
  71. package/packages/sdk/dist/workflows/index.js.map +1 -1
  72. package/packages/sdk/dist/workflows/run.d.ts.map +1 -1
  73. package/packages/sdk/dist/workflows/run.js +4 -0
  74. package/packages/sdk/dist/workflows/run.js.map +1 -1
  75. package/packages/sdk/dist/workflows/runner.d.ts +14 -0
  76. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  77. package/packages/sdk/dist/workflows/runner.js +169 -28
  78. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  79. package/packages/sdk/dist/workflows/types.d.ts +13 -3
  80. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  81. package/packages/sdk/dist/workflows/types.js +5 -1
  82. package/packages/sdk/dist/workflows/types.js.map +1 -1
  83. package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
  84. package/packages/sdk/dist/workflows/validator.js +12 -0
  85. package/packages/sdk/dist/workflows/validator.js.map +1 -1
  86. package/packages/sdk/package.json +13 -3
  87. package/packages/sdk/src/__tests__/channel-management.test.ts +131 -0
  88. package/packages/sdk/src/__tests__/communicate/core.test.ts +36 -88
  89. package/packages/sdk/src/__tests__/communicate/transport.test.ts +41 -80
  90. package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +120 -0
  91. package/packages/sdk/src/__tests__/relay-channel-ops.test.ts +121 -0
  92. package/packages/sdk/src/broker-path.ts +74 -0
  93. package/packages/sdk/src/cli-registry.ts +4 -0
  94. package/packages/sdk/src/client.ts +28 -0
  95. package/packages/sdk/src/communicate/adapters/index.ts +0 -5
  96. package/packages/sdk/src/communicate/adapters/pi.ts +1 -5
  97. package/packages/sdk/src/communicate/core.ts +6 -10
  98. package/packages/sdk/src/communicate/index.ts +57 -1
  99. package/packages/sdk/src/communicate/transport.ts +46 -177
  100. package/packages/sdk/src/http.ts +96 -0
  101. package/packages/sdk/src/protocol.ts +24 -0
  102. package/packages/sdk/src/relay.ts +93 -8
  103. package/packages/sdk/src/workflows/README.md +5 -2
  104. package/packages/sdk/src/workflows/api-executor.ts +108 -0
  105. package/packages/sdk/src/workflows/builder.ts +40 -0
  106. package/packages/sdk/src/workflows/cloud-runner.ts +56 -0
  107. package/packages/sdk/src/workflows/index.ts +2 -0
  108. package/packages/sdk/src/workflows/run.ts +5 -0
  109. package/packages/sdk/src/workflows/runner.ts +197 -30
  110. package/packages/sdk/src/workflows/types.ts +19 -4
  111. package/packages/sdk/src/workflows/validator.ts +15 -0
  112. package/packages/sdk-py/README.md +7 -0
  113. package/packages/sdk-py/pyproject.toml +1 -1
  114. package/packages/sdk-py/src/agent_relay/__init__.py +2 -0
  115. package/packages/sdk-py/src/agent_relay/builder.py +64 -7
  116. package/packages/sdk-py/src/agent_relay/client.py +4 -0
  117. package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +0 -9
  118. package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +5 -9
  119. package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +5 -7
  120. package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +3 -13
  121. package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +5 -2
  122. package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +5 -9
  123. package/packages/sdk-py/src/agent_relay/communicate/core.py +7 -24
  124. package/packages/sdk-py/src/agent_relay/communicate/transport.py +35 -212
  125. package/packages/sdk-py/src/agent_relay/communicate/types.py +1 -1
  126. package/packages/sdk-py/src/agent_relay/protocol.py +1 -0
  127. package/packages/sdk-py/src/agent_relay/relay.py +9 -1
  128. package/packages/sdk-py/src/agent_relay/types.py +1 -0
  129. package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +6 -6
  130. package/packages/sdk-py/tests/communicate/conftest.py +86 -233
  131. package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +2 -2
  132. package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +14 -24
  133. package/packages/sdk-py/tests/communicate/test_transport.py +65 -54
  134. package/packages/sdk-py/tests/test_builder.py +58 -0
  135. package/packages/sdk-py/tests/test_dry_run.py +215 -0
  136. package/packages/sdk-py/tests/test_send_message_mode.py +91 -0
  137. package/packages/telemetry/package.json +1 -1
  138. package/packages/trajectory/package.json +2 -2
  139. package/packages/user-directory/package.json +2 -2
  140. package/packages/utils/package.json +2 -2
@@ -1,4 +1,4 @@
1
- """Tests for the RelayTransport HTTP/WS client against real Relaycast API surface."""
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
- register_payload = relay_server.requests["register_agent"][-1]["json"]
48
- assert register_payload["name"] == "TransportTester"
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
- async def test_send_dm_posts_to_correct_endpoint(relay_server):
79
- RelayTransport = _transport_class()
80
- transport = RelayTransport("TransportTester", relay_server.make_config())
81
- await transport.connect()
82
-
83
- try:
84
- message_id = await transport.send_dm("Review-Core", "hello")
85
- finally:
86
- await transport.disconnect()
87
-
88
- assert message_id.startswith("message-")
89
- dm_req = relay_server.requests["send_dm"][-1]
90
- assert dm_req["json"]["to"] == "Review-Core"
91
- assert dm_req["json"]["text"] == "hello"
92
- assert dm_req["path"] == "/v1/dm"
93
-
94
-
95
- @pytest.mark.asyncio
96
- async def test_post_message_posts_to_channel_endpoint(relay_server):
97
- RelayTransport = _transport_class()
98
- transport = RelayTransport("TransportTester", relay_server.make_config())
99
- await transport.connect()
100
-
101
- try:
102
- message_id = await transport.post_message("core-py", "status update")
103
- finally:
104
- await transport.disconnect()
105
-
106
- assert message_id.startswith("message-")
107
- ch_req = relay_server.requests["post_message"][-1]
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.reply("message-123", "thread reply")
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
- reply_req = relay_server.requests["reply"][-1]
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 len(messages) == 1
152
- assert messages[0].sender == "Impl-Core"
153
- assert messages[0].text == "transport ready"
154
- assert messages[0].message_id == "message-inbox-1"
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 len(received) == 1
201
- assert received[0].sender == "Review-Core"
202
- assert received[0].text == "looks good"
203
- assert received[0].channel == "core-py"
204
- assert received[0].message_id == "message-ws-1"
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].sender == "Impl-Core"
246
- assert received[-1].text == "reconnected"
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/telemetry",
3
- "version": "3.2.15",
3
+ "version": "3.2.17",
4
4
  "description": "Anonymous telemetry for Agent Relay usage analytics",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/trajectory",
3
- "version": "3.2.15",
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.15"
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.15",
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.15"
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.15",
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.15",
115
+ "@agent-relay/config": "3.2.17",
116
116
  "compare-versions": "^6.1.1"
117
117
  },
118
118
  "publishConfig": {