agent-relay 3.2.3 → 3.2.4

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 (225) hide show
  1. package/dist/index.cjs +265 -108
  2. package/package.json +11 -10
  3. package/packages/acp-bridge/package.json +2 -2
  4. package/packages/config/package.json +1 -1
  5. package/packages/hooks/package.json +4 -4
  6. package/packages/memory/package.json +2 -2
  7. package/packages/openclaw/package.json +2 -2
  8. package/packages/policy/package.json +2 -2
  9. package/packages/sdk/ADAPTER_REVIEW.md +109 -0
  10. package/packages/sdk/dist/communicate/a2a-bridge.d.ts +25 -0
  11. package/packages/sdk/dist/communicate/a2a-bridge.d.ts.map +1 -0
  12. package/packages/sdk/dist/communicate/a2a-bridge.js +89 -0
  13. package/packages/sdk/dist/communicate/a2a-bridge.js.map +1 -0
  14. package/packages/sdk/dist/communicate/a2a-server.d.ts +31 -0
  15. package/packages/sdk/dist/communicate/a2a-server.d.ts.map +1 -0
  16. package/packages/sdk/dist/communicate/a2a-server.js +220 -0
  17. package/packages/sdk/dist/communicate/a2a-server.js.map +1 -0
  18. package/packages/sdk/dist/communicate/a2a-transport.d.ts +48 -0
  19. package/packages/sdk/dist/communicate/a2a-transport.d.ts.map +1 -0
  20. package/packages/sdk/dist/communicate/a2a-transport.js +302 -0
  21. package/packages/sdk/dist/communicate/a2a-transport.js.map +1 -0
  22. package/packages/sdk/dist/communicate/a2a-types.d.ts +107 -0
  23. package/packages/sdk/dist/communicate/a2a-types.d.ts.map +1 -0
  24. package/packages/sdk/dist/communicate/a2a-types.js +209 -0
  25. package/packages/sdk/dist/communicate/a2a-types.js.map +1 -0
  26. package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts +28 -0
  27. package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts.map +1 -0
  28. package/packages/sdk/dist/communicate/adapters/claude-sdk.js +47 -0
  29. package/packages/sdk/dist/communicate/adapters/claude-sdk.js.map +1 -0
  30. package/packages/sdk/dist/communicate/adapters/crewai.d.ts +42 -0
  31. package/packages/sdk/dist/communicate/adapters/crewai.d.ts.map +1 -0
  32. package/packages/sdk/dist/communicate/adapters/crewai.js +95 -0
  33. package/packages/sdk/dist/communicate/adapters/crewai.js.map +1 -0
  34. package/packages/sdk/dist/communicate/adapters/google-adk.d.ts +53 -0
  35. package/packages/sdk/dist/communicate/adapters/google-adk.d.ts.map +1 -0
  36. package/packages/sdk/dist/communicate/adapters/google-adk.js +77 -0
  37. package/packages/sdk/dist/communicate/adapters/google-adk.js.map +1 -0
  38. package/packages/sdk/dist/communicate/adapters/index.d.ts +7 -0
  39. package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -0
  40. package/packages/sdk/dist/communicate/adapters/index.js +7 -0
  41. package/packages/sdk/dist/communicate/adapters/index.js.map +1 -0
  42. package/packages/sdk/dist/communicate/adapters/langgraph.d.ts +40 -0
  43. package/packages/sdk/dist/communicate/adapters/langgraph.d.ts.map +1 -0
  44. package/packages/sdk/dist/communicate/adapters/langgraph.js +77 -0
  45. package/packages/sdk/dist/communicate/adapters/langgraph.js.map +1 -0
  46. package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts +25 -0
  47. package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts.map +1 -0
  48. package/packages/sdk/dist/communicate/adapters/openai-agents.js +70 -0
  49. package/packages/sdk/dist/communicate/adapters/openai-agents.js.map +1 -0
  50. package/packages/sdk/dist/communicate/adapters/pi.d.ts +45 -0
  51. package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -0
  52. package/packages/sdk/dist/communicate/adapters/pi.js +59 -0
  53. package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -0
  54. package/packages/sdk/dist/communicate/core.d.ts +58 -0
  55. package/packages/sdk/dist/communicate/core.d.ts.map +1 -0
  56. package/packages/sdk/dist/communicate/core.js +128 -0
  57. package/packages/sdk/dist/communicate/core.js.map +1 -0
  58. package/packages/sdk/dist/communicate/index.d.ts +4 -0
  59. package/packages/sdk/dist/communicate/index.d.ts.map +1 -0
  60. package/packages/sdk/dist/communicate/index.js +4 -0
  61. package/packages/sdk/dist/communicate/index.js.map +1 -0
  62. package/packages/sdk/dist/communicate/transport.d.ts +36 -0
  63. package/packages/sdk/dist/communicate/transport.d.ts.map +1 -0
  64. package/packages/sdk/dist/communicate/transport.js +371 -0
  65. package/packages/sdk/dist/communicate/transport.js.map +1 -0
  66. package/packages/sdk/dist/communicate/types.d.ts +58 -0
  67. package/packages/sdk/dist/communicate/types.d.ts.map +1 -0
  68. package/packages/sdk/dist/communicate/types.js +66 -0
  69. package/packages/sdk/dist/communicate/types.js.map +1 -0
  70. package/packages/sdk/dist/workflows/builder.d.ts +35 -5
  71. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  72. package/packages/sdk/dist/workflows/builder.js +81 -7
  73. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  74. package/packages/sdk/dist/workflows/cli.js +14 -1
  75. package/packages/sdk/dist/workflows/cli.js.map +1 -1
  76. package/packages/sdk/dist/workflows/runner.d.ts +10 -2
  77. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  78. package/packages/sdk/dist/workflows/runner.js +95 -1
  79. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  80. package/packages/sdk/dist/workflows/types.d.ts +11 -0
  81. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  82. package/packages/sdk/examples/communicate/claude_sdk_example.ts +5 -0
  83. package/packages/sdk/examples/communicate/pi_example.ts +8 -0
  84. package/packages/sdk/package.json +48 -2
  85. package/packages/sdk/src/__tests__/builder-deterministic.test.ts +132 -0
  86. package/packages/sdk/src/__tests__/communicate/a2a-bridge.test.ts +211 -0
  87. package/packages/sdk/src/__tests__/communicate/a2a-server.test.ts +359 -0
  88. package/packages/sdk/src/__tests__/communicate/a2a-transport.test.ts +537 -0
  89. package/packages/sdk/src/__tests__/communicate/a2a-types.test.ts +297 -0
  90. package/packages/sdk/src/__tests__/communicate/adapters/claude-sdk.test.ts +163 -0
  91. package/packages/sdk/src/__tests__/communicate/adapters/crewai.test.ts +219 -0
  92. package/packages/sdk/src/__tests__/communicate/adapters/e2e-crewai.test.ts +101 -0
  93. package/packages/sdk/src/__tests__/communicate/adapters/e2e-google-adk.test.ts +166 -0
  94. package/packages/sdk/src/__tests__/communicate/adapters/e2e-langgraph.test.ts +181 -0
  95. package/packages/sdk/src/__tests__/communicate/adapters/e2e-openai-agents.test.ts +137 -0
  96. package/packages/sdk/src/__tests__/communicate/adapters/e2e-pi.test.ts +140 -0
  97. package/packages/sdk/src/__tests__/communicate/adapters/google-adk.test.ts +200 -0
  98. package/packages/sdk/src/__tests__/communicate/adapters/langgraph.test.ts +162 -0
  99. package/packages/sdk/src/__tests__/communicate/adapters/openai-agents.test.ts +166 -0
  100. package/packages/sdk/src/__tests__/communicate/adapters/pi.test.ts +140 -0
  101. package/packages/sdk/src/__tests__/communicate/core.test.ts +574 -0
  102. package/packages/sdk/src/__tests__/communicate/integration/cross-framework.test.ts +353 -0
  103. package/packages/sdk/src/__tests__/communicate/transport.test.ts +613 -0
  104. package/packages/sdk/src/__tests__/start-from.test.ts +346 -0
  105. package/packages/sdk/src/communicate/a2a-bridge.ts +111 -0
  106. package/packages/sdk/src/communicate/a2a-server.ts +277 -0
  107. package/packages/sdk/src/communicate/a2a-transport.ts +395 -0
  108. package/packages/sdk/src/communicate/a2a-types.ts +338 -0
  109. package/packages/sdk/src/communicate/adapters/claude-sdk.ts +85 -0
  110. package/packages/sdk/src/communicate/adapters/crewai.ts +141 -0
  111. package/packages/sdk/src/communicate/adapters/google-adk.ts +139 -0
  112. package/packages/sdk/src/communicate/adapters/index.ts +6 -0
  113. package/packages/sdk/src/communicate/adapters/langgraph.ts +112 -0
  114. package/packages/sdk/src/communicate/adapters/openai-agents.ts +113 -0
  115. package/packages/sdk/src/communicate/adapters/pi.ts +105 -0
  116. package/packages/sdk/src/communicate/core.ts +157 -0
  117. package/packages/sdk/src/communicate/index.ts +3 -0
  118. package/packages/sdk/src/communicate/transport.ts +489 -0
  119. package/packages/sdk/src/communicate/types.ts +106 -0
  120. package/packages/sdk/src/examples/workflows/fix-dashboard-user-registration.yaml +182 -0
  121. package/packages/sdk/src/workflows/builder.ts +97 -9
  122. package/packages/sdk/src/workflows/cli.ts +16 -1
  123. package/packages/sdk/src/workflows/runner.ts +110 -1
  124. package/packages/sdk/src/workflows/types.ts +14 -0
  125. package/packages/sdk/tsconfig.build.json +1 -7
  126. package/packages/sdk/tsconfig.json +1 -7
  127. package/packages/sdk-py/README.md +67 -25
  128. package/packages/sdk-py/examples/communicate/agno_example.py +8 -0
  129. package/packages/sdk-py/examples/communicate/claude_sdk_example.py +6 -0
  130. package/packages/sdk-py/examples/communicate/crewai_example.py +7 -0
  131. package/packages/sdk-py/examples/communicate/google_adk_example.py +7 -0
  132. package/packages/sdk-py/examples/communicate/openai_agents_example.py +8 -0
  133. package/packages/sdk-py/examples/communicate/swarms_example.py +7 -0
  134. package/packages/sdk-py/pyproject.toml +12 -1
  135. package/packages/sdk-py/src/agent_relay/__init__.py +8 -0
  136. package/packages/sdk-py/src/agent_relay/builder.py +65 -26
  137. package/packages/sdk-py/src/agent_relay/communicate/__init__.py +6 -0
  138. package/packages/sdk-py/src/agent_relay/communicate/a2a_bridge.py +138 -0
  139. package/packages/sdk-py/src/agent_relay/communicate/a2a_server.py +242 -0
  140. package/packages/sdk-py/src/agent_relay/communicate/a2a_transport.py +366 -0
  141. package/packages/sdk-py/src/agent_relay/communicate/a2a_types.py +294 -0
  142. package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +10 -0
  143. package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +74 -0
  144. package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +78 -0
  145. package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +143 -0
  146. package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +69 -0
  147. package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +86 -0
  148. package/packages/sdk-py/src/agent_relay/communicate/adapters/pi.py +175 -0
  149. package/packages/sdk-py/src/agent_relay/communicate/adapters/swarms.py +44 -0
  150. package/packages/sdk-py/src/agent_relay/communicate/core.py +293 -0
  151. package/packages/sdk-py/src/agent_relay/communicate/transport.py +502 -0
  152. package/packages/sdk-py/src/agent_relay/communicate/types.py +89 -0
  153. package/packages/sdk-py/src/agent_relay/types.py +2 -1
  154. package/packages/sdk-py/tests/communicate/__init__.py +0 -0
  155. package/packages/sdk-py/tests/communicate/adapters/__init__.py +0 -0
  156. package/packages/sdk-py/tests/communicate/adapters/e2e_test_agno.py +154 -0
  157. package/packages/sdk-py/tests/communicate/adapters/e2e_test_claude_sdk.py +428 -0
  158. package/packages/sdk-py/tests/communicate/adapters/e2e_test_crewai.py +234 -0
  159. package/packages/sdk-py/tests/communicate/adapters/e2e_test_google_adk.py +182 -0
  160. package/packages/sdk-py/tests/communicate/adapters/e2e_test_langgraph.py +262 -0
  161. package/packages/sdk-py/tests/communicate/adapters/e2e_test_openai_agents.py +88 -0
  162. package/packages/sdk-py/tests/communicate/adapters/e2e_test_pi.py +156 -0
  163. package/packages/sdk-py/tests/communicate/adapters/e2e_test_swarms.py +239 -0
  164. package/packages/sdk-py/tests/communicate/adapters/test_agno.py +140 -0
  165. package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +147 -0
  166. package/packages/sdk-py/tests/communicate/adapters/test_crewai.py +136 -0
  167. package/packages/sdk-py/tests/communicate/adapters/test_google_adk.py +125 -0
  168. package/packages/sdk-py/tests/communicate/adapters/test_openai_agents.py +99 -0
  169. package/packages/sdk-py/tests/communicate/adapters/test_pi.py +270 -0
  170. package/packages/sdk-py/tests/communicate/adapters/test_swarms.py +113 -0
  171. package/packages/sdk-py/tests/communicate/conftest.py +555 -0
  172. package/packages/sdk-py/tests/communicate/integration/__init__.py +1 -0
  173. package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +331 -0
  174. package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +151 -0
  175. package/packages/sdk-py/tests/communicate/test_a2a_bridge.py +363 -0
  176. package/packages/sdk-py/tests/communicate/test_a2a_server.py +346 -0
  177. package/packages/sdk-py/tests/communicate/test_a2a_transport.py +561 -0
  178. package/packages/sdk-py/tests/communicate/test_a2a_types.py +342 -0
  179. package/packages/sdk-py/tests/communicate/test_auto_detect.py +67 -0
  180. package/packages/sdk-py/tests/communicate/test_core.py +331 -0
  181. package/packages/sdk-py/tests/communicate/test_transport.py +373 -0
  182. package/packages/sdk-py/tests/communicate/test_types.py +285 -0
  183. package/packages/sdk-py/tests/test_builder_deterministic.py +118 -0
  184. package/packages/telemetry/package.json +1 -1
  185. package/packages/trajectory/package.json +2 -2
  186. package/packages/user-directory/package.json +2 -2
  187. package/packages/utils/package.json +2 -2
  188. package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts +0 -14
  189. package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts.map +0 -1
  190. package/packages/sdk/dist/__tests__/completion-pipeline.test.js +0 -1476
  191. package/packages/sdk/dist/__tests__/completion-pipeline.test.js.map +0 -1
  192. package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts +0 -2
  193. package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts.map +0 -1
  194. package/packages/sdk/dist/__tests__/contract-fixtures.test.js +0 -152
  195. package/packages/sdk/dist/__tests__/contract-fixtures.test.js.map +0 -1
  196. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts +0 -16
  197. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts.map +0 -1
  198. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +0 -640
  199. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +0 -1
  200. package/packages/sdk/dist/__tests__/facade.test.d.ts +0 -2
  201. package/packages/sdk/dist/__tests__/facade.test.d.ts.map +0 -1
  202. package/packages/sdk/dist/__tests__/facade.test.js +0 -305
  203. package/packages/sdk/dist/__tests__/facade.test.js.map +0 -1
  204. package/packages/sdk/dist/__tests__/integration.test.d.ts +0 -2
  205. package/packages/sdk/dist/__tests__/integration.test.d.ts.map +0 -1
  206. package/packages/sdk/dist/__tests__/integration.test.js +0 -205
  207. package/packages/sdk/dist/__tests__/integration.test.js.map +0 -1
  208. package/packages/sdk/dist/__tests__/pty.test.d.ts +0 -2
  209. package/packages/sdk/dist/__tests__/pty.test.d.ts.map +0 -1
  210. package/packages/sdk/dist/__tests__/pty.test.js +0 -20
  211. package/packages/sdk/dist/__tests__/pty.test.js.map +0 -1
  212. package/packages/sdk/dist/__tests__/quickstart.test.d.ts +0 -2
  213. package/packages/sdk/dist/__tests__/quickstart.test.d.ts.map +0 -1
  214. package/packages/sdk/dist/__tests__/quickstart.test.js +0 -176
  215. package/packages/sdk/dist/__tests__/quickstart.test.js.map +0 -1
  216. package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts +0 -2
  217. package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts.map +0 -1
  218. package/packages/sdk/dist/__tests__/spawn-from-env.test.js +0 -222
  219. package/packages/sdk/dist/__tests__/spawn-from-env.test.js.map +0 -1
  220. package/packages/sdk/dist/__tests__/unit.test.d.ts +0 -2
  221. package/packages/sdk/dist/__tests__/unit.test.d.ts.map +0 -1
  222. package/packages/sdk/dist/__tests__/unit.test.js +0 -357
  223. package/packages/sdk/dist/__tests__/unit.test.js.map +0 -1
  224. package/packages/sdk-py/agent_relay/__init__.py +0 -21
  225. package/packages/sdk-py/agent_relay/models.py +0 -398
@@ -0,0 +1,285 @@
1
+ """Tests for communicate mode types (TDD - red phase).
2
+
3
+ Tests cover:
4
+ - Message dataclass (creation, immutability, defaults)
5
+ - RelayConfig dataclass (defaults, env var resolution)
6
+ - Exception types (RelayConnectionError, RelayConfigError, RelayAuthError)
7
+ - MessageCallback type alias compatibility
8
+ """
9
+
10
+ import asyncio
11
+ import dataclasses
12
+
13
+ import pytest
14
+
15
+ from agent_relay.communicate.types import (
16
+ Message,
17
+ MessageCallback,
18
+ RelayAuthError,
19
+ RelayConfig,
20
+ RelayConfigError,
21
+ RelayConnectionError,
22
+ )
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Message dataclass
27
+ # ---------------------------------------------------------------------------
28
+
29
+
30
+ class TestMessage:
31
+ """Tests for the Message frozen dataclass."""
32
+
33
+ def test_create_with_required_fields_only(self):
34
+ msg = Message(sender="Alice", text="Hello")
35
+ assert msg.sender == "Alice"
36
+ assert msg.text == "Hello"
37
+
38
+ def test_optional_fields_default_to_none(self):
39
+ msg = Message(sender="Alice", text="Hello")
40
+ assert msg.channel is None
41
+ assert msg.thread_id is None
42
+ assert msg.timestamp is None
43
+ assert msg.message_id is None
44
+
45
+ def test_create_with_all_fields(self):
46
+ msg = Message(
47
+ sender="Bob",
48
+ text="Hi there",
49
+ channel="general",
50
+ thread_id="thread-123",
51
+ timestamp=1710300000.0,
52
+ message_id="msg-456",
53
+ )
54
+ assert msg.sender == "Bob"
55
+ assert msg.text == "Hi there"
56
+ assert msg.channel == "general"
57
+ assert msg.thread_id == "thread-123"
58
+ assert msg.timestamp == 1710300000.0
59
+ assert msg.message_id == "msg-456"
60
+
61
+ def test_is_frozen_cannot_set_sender(self):
62
+ msg = Message(sender="Alice", text="Hello")
63
+ with pytest.raises(dataclasses.FrozenInstanceError):
64
+ msg.sender = "Eve" # type: ignore[misc]
65
+
66
+ def test_is_frozen_cannot_set_text(self):
67
+ msg = Message(sender="Alice", text="Hello")
68
+ with pytest.raises(dataclasses.FrozenInstanceError):
69
+ msg.text = "Tampered" # type: ignore[misc]
70
+
71
+ def test_is_frozen_cannot_set_optional_field(self):
72
+ msg = Message(sender="Alice", text="Hello")
73
+ with pytest.raises(dataclasses.FrozenInstanceError):
74
+ msg.channel = "secret" # type: ignore[misc]
75
+
76
+ def test_is_dataclass(self):
77
+ assert dataclasses.is_dataclass(Message)
78
+
79
+ def test_equality_same_values(self):
80
+ a = Message(sender="Alice", text="Hi")
81
+ b = Message(sender="Alice", text="Hi")
82
+ assert a == b
83
+
84
+ def test_equality_different_values(self):
85
+ a = Message(sender="Alice", text="Hi")
86
+ b = Message(sender="Bob", text="Hi")
87
+ assert a != b
88
+
89
+ def test_channel_none_means_dm(self):
90
+ """When channel is None, the message is a direct message."""
91
+ msg = Message(sender="Alice", text="DM content")
92
+ assert msg.channel is None
93
+
94
+ def test_channel_set_means_channel_message(self):
95
+ msg = Message(sender="Alice", text="Channel content", channel="general")
96
+ assert msg.channel == "general"
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # RelayConfig dataclass
101
+ # ---------------------------------------------------------------------------
102
+
103
+
104
+ class TestRelayConfig:
105
+ """Tests for the RelayConfig dataclass with env var defaults."""
106
+
107
+ def test_all_defaults(self, monkeypatch):
108
+ monkeypatch.delenv("RELAY_WORKSPACE", raising=False)
109
+ monkeypatch.delenv("RELAY_API_KEY", raising=False)
110
+ monkeypatch.delenv("RELAY_BASE_URL", raising=False)
111
+
112
+ config = RelayConfig()
113
+ assert config.workspace is None
114
+ assert config.api_key is None
115
+ assert config.base_url is None
116
+ assert config.channels == ["general"]
117
+ assert config.poll_interval_ms == 1000
118
+ assert config.auto_cleanup is True
119
+
120
+ def test_explicit_values_override_defaults(self):
121
+ config = RelayConfig(
122
+ workspace="my-ws",
123
+ api_key="key-123",
124
+ base_url="https://custom.api.dev",
125
+ channels=["dev", "ops"],
126
+ poll_interval_ms=500,
127
+ auto_cleanup=False,
128
+ )
129
+ assert config.workspace == "my-ws"
130
+ assert config.api_key == "key-123"
131
+ assert config.base_url == "https://custom.api.dev"
132
+ assert config.channels == ["dev", "ops"]
133
+ assert config.poll_interval_ms == 500
134
+ assert config.auto_cleanup is False
135
+
136
+ def test_channels_default_is_independent_per_instance(self):
137
+ """Each RelayConfig gets its own channels list (no shared mutable default)."""
138
+ a = RelayConfig()
139
+ b = RelayConfig()
140
+ a.channels.append("extra")
141
+ assert "extra" not in b.channels
142
+
143
+ def test_env_var_workspace(self, monkeypatch):
144
+ monkeypatch.setenv("RELAY_WORKSPACE", "env-workspace")
145
+ config = RelayConfig()
146
+ assert config.workspace == "env-workspace"
147
+
148
+ def test_env_var_api_key(self, monkeypatch):
149
+ monkeypatch.setenv("RELAY_API_KEY", "env-key-abc")
150
+ config = RelayConfig()
151
+ assert config.api_key == "env-key-abc"
152
+
153
+ def test_env_var_base_url(self, monkeypatch):
154
+ monkeypatch.setenv("RELAY_BASE_URL", "https://env.api.dev")
155
+ config = RelayConfig()
156
+ assert config.base_url == "https://env.api.dev"
157
+
158
+ def test_base_url_default_when_no_env(self, monkeypatch):
159
+ """When RELAY_BASE_URL is not set, base_url defaults to the Relaycast cloud URL."""
160
+ monkeypatch.delenv("RELAY_BASE_URL", raising=False)
161
+ config = RelayConfig()
162
+ # base_url should resolve to the default cloud URL when accessed
163
+ # The exact resolution may happen at init or lazily; either None or the default is acceptable
164
+ # but the resolved value should be "https://api.relaycast.dev"
165
+ assert config.base_url is None or config.base_url == "https://api.relaycast.dev"
166
+
167
+ def test_explicit_value_overrides_env_var(self, monkeypatch):
168
+ monkeypatch.setenv("RELAY_WORKSPACE", "env-workspace")
169
+ config = RelayConfig(workspace="explicit-workspace")
170
+ assert config.workspace == "explicit-workspace"
171
+
172
+ def test_is_dataclass(self):
173
+ assert dataclasses.is_dataclass(RelayConfig)
174
+
175
+ def test_is_not_frozen(self):
176
+ """RelayConfig should be mutable (not frozen)."""
177
+ config = RelayConfig()
178
+ config.workspace = "updated"
179
+ assert config.workspace == "updated"
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # Exception types
184
+ # ---------------------------------------------------------------------------
185
+
186
+
187
+ class TestRelayConnectionError:
188
+ """Tests for RelayConnectionError which stores status_code and message."""
189
+
190
+ def test_inherits_from_exception(self):
191
+ assert issubclass(RelayConnectionError, Exception)
192
+
193
+ def test_stores_status_code_and_message(self):
194
+ err = RelayConnectionError(status_code=500, message="Internal Server Error")
195
+ assert err.status_code == 500
196
+ assert err.message == "Internal Server Error"
197
+
198
+ def test_str_contains_status_and_message(self):
199
+ err = RelayConnectionError(status_code=503, message="Service Unavailable")
200
+ text = str(err)
201
+ assert "503" in text
202
+ assert "Service Unavailable" in text
203
+
204
+ def test_different_status_codes(self):
205
+ for code in [400, 404, 429, 500, 502, 503]:
206
+ err = RelayConnectionError(status_code=code, message=f"Error {code}")
207
+ assert err.status_code == code
208
+
209
+
210
+ class TestRelayConfigError:
211
+ """Tests for RelayConfigError which signals missing configuration."""
212
+
213
+ def test_inherits_from_exception(self):
214
+ assert issubclass(RelayConfigError, Exception)
215
+
216
+ def test_has_descriptive_message(self):
217
+ err = RelayConfigError("RELAY_API_KEY environment variable is required")
218
+ assert "RELAY_API_KEY" in str(err)
219
+
220
+ def test_message_accessible(self):
221
+ msg = "Missing RELAY_WORKSPACE. Set the environment variable or pass workspace= to RelayConfig."
222
+ err = RelayConfigError(msg)
223
+ assert str(err) == msg
224
+
225
+
226
+ class TestRelayAuthError:
227
+ """Tests for RelayAuthError which signals HTTP 401 responses."""
228
+
229
+ def test_inherits_from_exception(self):
230
+ assert issubclass(RelayAuthError, Exception)
231
+
232
+ def test_message(self):
233
+ err = RelayAuthError("Invalid API key")
234
+ assert "Invalid API key" in str(err)
235
+
236
+ def test_inherits_from_relay_connection_error_or_exception(self):
237
+ """RelayAuthError should be catchable as a general exception."""
238
+ err = RelayAuthError("Unauthorized")
239
+ assert isinstance(err, Exception)
240
+
241
+
242
+ # ---------------------------------------------------------------------------
243
+ # MessageCallback type alias
244
+ # ---------------------------------------------------------------------------
245
+
246
+
247
+ class TestMessageCallback:
248
+ """Tests that both sync and async callables satisfy MessageCallback."""
249
+
250
+ def test_sync_callable_is_valid(self):
251
+ """A synchronous function that takes a Message and returns None should be valid."""
252
+ received: list[Message] = []
253
+
254
+ def handler(msg: Message) -> None:
255
+ received.append(msg)
256
+
257
+ # Verify the callable works as expected
258
+ callback: MessageCallback = handler
259
+ msg = Message(sender="Alice", text="Test")
260
+ callback(msg)
261
+ assert len(received) == 1
262
+ assert received[0].sender == "Alice"
263
+
264
+ async def test_async_callable_is_valid(self):
265
+ """An async function that takes a Message and returns None should be valid."""
266
+ received: list[Message] = []
267
+
268
+ async def handler(msg: Message) -> None:
269
+ received.append(msg)
270
+
271
+ callback: MessageCallback = handler
272
+ msg = Message(sender="Bob", text="Async test")
273
+ result = callback(msg)
274
+ # If it returns a coroutine, await it
275
+ if asyncio.iscoroutine(result):
276
+ await result
277
+ assert len(received) == 1
278
+ assert received[0].sender == "Bob"
279
+
280
+ def test_lambda_callable_is_valid(self):
281
+ """A lambda that takes a Message should work as a callback."""
282
+ results: list[str] = []
283
+ callback: MessageCallback = lambda msg: results.append(msg.text)
284
+ callback(Message(sender="X", text="lambda-test"))
285
+ assert results == ["lambda-test"]
@@ -0,0 +1,118 @@
1
+ """Tests for deterministic and worktree step support in the Python workflow builder."""
2
+
3
+ import pytest
4
+ from agent_relay import workflow, VerificationCheck
5
+
6
+
7
+ def test_deterministic_step_emits_correct_config():
8
+ config = (
9
+ workflow("test")
10
+ .agent("worker", cli="claude")
11
+ .step("read-files", type="deterministic", command="cat src/index.ts",
12
+ verification=VerificationCheck(type="exit_code", value="0"))
13
+ .step("build", agent="worker", task="Build the project")
14
+ .to_config()
15
+ )
16
+
17
+ steps = config["workflows"][0]["steps"]
18
+ assert len(steps) == 2
19
+
20
+ # Deterministic step
21
+ assert steps[0]["name"] == "read-files"
22
+ assert steps[0]["type"] == "deterministic"
23
+ assert steps[0]["command"] == "cat src/index.ts"
24
+ assert "agent" not in steps[0]
25
+ assert "task" not in steps[0]
26
+ assert steps[0]["verification"] == {"type": "exit_code", "value": "0"}
27
+
28
+ # Agent step
29
+ assert steps[1]["name"] == "build"
30
+ assert steps[1]["agent"] == "worker"
31
+ assert steps[1]["task"] == "Build the project"
32
+ assert "type" not in steps[1]
33
+
34
+
35
+ def test_deterministic_step_with_all_options():
36
+ config = (
37
+ workflow("test")
38
+ .agent("worker", cli="claude")
39
+ .step("run-cmd", type="deterministic", command="npm test",
40
+ capture_output=True, fail_on_error=False,
41
+ depends_on=["build"], timeout_ms=30000)
42
+ .step("final", agent="worker", task="Finalize")
43
+ .to_config()
44
+ )
45
+
46
+ step = config["workflows"][0]["steps"][0]
47
+ assert step["captureOutput"] is True
48
+ assert step["failOnError"] is False
49
+ assert step["dependsOn"] == ["build"]
50
+ assert step["timeoutMs"] == 30000
51
+
52
+
53
+ def test_worktree_step_emits_correct_config():
54
+ config = (
55
+ workflow("test")
56
+ .agent("worker", cli="claude")
57
+ .step("setup-worktree", type="worktree", branch="feature/new",
58
+ base_branch="main", path=".worktrees/feature-new", create_branch=True)
59
+ .step("work", agent="worker", task="Do work", depends_on=["setup-worktree"])
60
+ .to_config()
61
+ )
62
+
63
+ step = config["workflows"][0]["steps"][0]
64
+ assert step["type"] == "worktree"
65
+ assert step["branch"] == "feature/new"
66
+ assert step["baseBranch"] == "main"
67
+ assert step["path"] == ".worktrees/feature-new"
68
+ assert step["createBranch"] is True
69
+ assert "agent" not in step
70
+ assert "command" not in step
71
+
72
+
73
+ def test_deterministic_only_workflow_no_agents_required():
74
+ config = (
75
+ workflow("infra")
76
+ .step("lint", type="deterministic", command="npm run lint")
77
+ .step("test", type="deterministic", command="npm test", depends_on=["lint"])
78
+ .to_config()
79
+ )
80
+
81
+ assert config["agents"] == []
82
+ assert len(config["workflows"][0]["steps"]) == 2
83
+
84
+
85
+ def test_deterministic_step_without_command_raises():
86
+ with pytest.raises(ValueError, match="deterministic steps must have a command"):
87
+ workflow("test").step("bad", type="deterministic")
88
+
89
+
90
+ def test_deterministic_step_with_agent_raises():
91
+ with pytest.raises(ValueError, match="deterministic steps must not have agent or task"):
92
+ workflow("test").step("bad", type="deterministic", command="ls", agent="x", task="y")
93
+
94
+
95
+ def test_agent_step_without_agent_task_raises():
96
+ with pytest.raises(ValueError, match="Agent steps must have both agent and task"):
97
+ workflow("test").step("bad")
98
+
99
+
100
+ def test_agent_steps_without_agent_definition_raises():
101
+ with pytest.raises(ValueError, match="Workflow must have at least one agent when using agent steps"):
102
+ workflow("test").step("work", agent="worker", task="Do work").to_config()
103
+
104
+
105
+ def test_worktree_step_without_branch_raises():
106
+ with pytest.raises(ValueError, match="worktree steps must have a branch"):
107
+ workflow("test").step("bad", type="worktree")
108
+
109
+
110
+ def test_to_yaml_includes_deterministic_steps():
111
+ yaml_str = (
112
+ workflow("test")
113
+ .step("check", type="deterministic", command="echo hello")
114
+ .to_yaml()
115
+ )
116
+
117
+ assert "type: deterministic" in yaml_str
118
+ assert "command: echo hello" in yaml_str
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/telemetry",
3
- "version": "3.2.3",
3
+ "version": "3.2.4",
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.3",
3
+ "version": "3.2.4",
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.3"
25
+ "@agent-relay/config": "3.2.4"
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",
3
+ "version": "3.2.4",
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.3"
25
+ "@agent-relay/utils": "3.2.4"
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",
3
+ "version": "3.2.4",
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.3",
115
+ "@agent-relay/config": "3.2.4",
116
116
  "compare-versions": "^6.1.1"
117
117
  },
118
118
  "publishConfig": {
@@ -1,14 +0,0 @@
1
- /**
2
- * Completion Pipeline tests for Point-Person-Led Completion spec.
3
- *
4
- * Validates:
5
- * 1. Evidence-based completion (verification passes without marker)
6
- * 2. Owner decision parsing (OWNER_DECISION: COMPLETE/INCOMPLETE_RETRY/INCOMPLETE_FAIL)
7
- * 3. Tolerant review parsing (accepts semantic equivalents)
8
- * 4. Channel evidence contributions (WORKER_DONE signals)
9
- * 5. Backward compatibility with marker-based workflows
10
- * 6. Codex/Gemini/Supervisor pattern compatibility
11
- * 7. Map-reduce workflows remain unaffected
12
- */
13
- export {};
14
- //# sourceMappingURL=completion-pipeline.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"completion-pipeline.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/completion-pipeline.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}