agent-relay 3.0.2 → 3.1.1

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 (266) hide show
  1. package/README.md +8 -0
  2. package/bin/agent-relay-broker-linux-x64 +0 -0
  3. package/dist/index.cjs +273 -56
  4. package/dist/src/cli/commands/core.d.ts +2 -0
  5. package/dist/src/cli/commands/core.d.ts.map +1 -1
  6. package/dist/src/cli/commands/core.js +9 -2
  7. package/dist/src/cli/commands/core.js.map +1 -1
  8. package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
  9. package/dist/src/cli/lib/broker-lifecycle.js +87 -28
  10. package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
  11. package/package.json +9 -9
  12. package/packages/acp-bridge/README.md +50 -67
  13. package/packages/acp-bridge/package.json +2 -2
  14. package/packages/config/package.json +1 -1
  15. package/packages/hooks/package.json +4 -4
  16. package/packages/memory/package.json +2 -2
  17. package/packages/openclaw/README.md +78 -0
  18. package/packages/openclaw/bin/relay-openclaw.mjs +2 -0
  19. package/packages/openclaw/bridge/bridge.mjs +305 -0
  20. package/packages/openclaw/dist/__tests__/gateway-threads.test.d.ts +2 -0
  21. package/packages/openclaw/dist/__tests__/gateway-threads.test.d.ts.map +1 -0
  22. package/packages/openclaw/dist/__tests__/gateway-threads.test.js +320 -0
  23. package/packages/openclaw/dist/__tests__/gateway-threads.test.js.map +1 -0
  24. package/packages/openclaw/dist/__tests__/naming.test.d.ts +2 -0
  25. package/packages/openclaw/dist/__tests__/naming.test.d.ts.map +1 -0
  26. package/packages/openclaw/dist/__tests__/naming.test.js +21 -0
  27. package/packages/openclaw/dist/__tests__/naming.test.js.map +1 -0
  28. package/packages/openclaw/dist/__tests__/spawn-manager.test.d.ts +2 -0
  29. package/packages/openclaw/dist/__tests__/spawn-manager.test.d.ts.map +1 -0
  30. package/packages/openclaw/dist/__tests__/spawn-manager.test.js +126 -0
  31. package/packages/openclaw/dist/__tests__/spawn-manager.test.js.map +1 -0
  32. package/packages/openclaw/dist/auth/converter.d.ts +28 -0
  33. package/packages/openclaw/dist/auth/converter.d.ts.map +1 -0
  34. package/packages/openclaw/dist/auth/converter.js +64 -0
  35. package/packages/openclaw/dist/auth/converter.js.map +1 -0
  36. package/packages/openclaw/dist/cli.d.ts +2 -0
  37. package/packages/openclaw/dist/cli.d.ts.map +1 -0
  38. package/packages/openclaw/dist/cli.js +230 -0
  39. package/packages/openclaw/dist/cli.js.map +1 -0
  40. package/packages/openclaw/dist/config.d.ts +27 -0
  41. package/packages/openclaw/dist/config.d.ts.map +1 -0
  42. package/packages/openclaw/dist/config.js +97 -0
  43. package/packages/openclaw/dist/config.js.map +1 -0
  44. package/packages/openclaw/dist/control.d.ts +22 -0
  45. package/packages/openclaw/dist/control.d.ts.map +1 -0
  46. package/packages/openclaw/dist/control.js +58 -0
  47. package/packages/openclaw/dist/control.js.map +1 -0
  48. package/packages/openclaw/dist/gateway.d.ts +71 -0
  49. package/packages/openclaw/dist/gateway.d.ts.map +1 -0
  50. package/packages/openclaw/dist/gateway.js +785 -0
  51. package/packages/openclaw/dist/gateway.js.map +1 -0
  52. package/packages/openclaw/dist/identity/contract.d.ts +11 -0
  53. package/packages/openclaw/dist/identity/contract.d.ts.map +1 -0
  54. package/packages/openclaw/dist/identity/contract.js +40 -0
  55. package/packages/openclaw/dist/identity/contract.js.map +1 -0
  56. package/packages/openclaw/dist/identity/files.d.ts +33 -0
  57. package/packages/openclaw/dist/identity/files.d.ts.map +1 -0
  58. package/packages/openclaw/dist/identity/files.js +145 -0
  59. package/packages/openclaw/dist/identity/files.js.map +1 -0
  60. package/packages/openclaw/dist/identity/model.d.ts +11 -0
  61. package/packages/openclaw/dist/identity/model.d.ts.map +1 -0
  62. package/packages/openclaw/dist/identity/model.js +28 -0
  63. package/packages/openclaw/dist/identity/model.js.map +1 -0
  64. package/packages/openclaw/dist/identity/naming.d.ts +5 -0
  65. package/packages/openclaw/dist/identity/naming.d.ts.map +1 -0
  66. package/packages/openclaw/dist/identity/naming.js +7 -0
  67. package/packages/openclaw/dist/identity/naming.js.map +1 -0
  68. package/packages/openclaw/dist/index.d.ts +20 -0
  69. package/packages/openclaw/dist/index.d.ts.map +1 -0
  70. package/packages/openclaw/dist/index.js +27 -0
  71. package/packages/openclaw/dist/index.js.map +1 -0
  72. package/packages/openclaw/dist/inject.d.ts +14 -0
  73. package/packages/openclaw/dist/inject.d.ts.map +1 -0
  74. package/packages/openclaw/dist/inject.js +66 -0
  75. package/packages/openclaw/dist/inject.js.map +1 -0
  76. package/packages/openclaw/dist/mcp/server.d.ts +8 -0
  77. package/packages/openclaw/dist/mcp/server.d.ts.map +1 -0
  78. package/packages/openclaw/dist/mcp/server.js +105 -0
  79. package/packages/openclaw/dist/mcp/server.js.map +1 -0
  80. package/packages/openclaw/dist/mcp/tools.d.ts +17 -0
  81. package/packages/openclaw/dist/mcp/tools.d.ts.map +1 -0
  82. package/packages/openclaw/dist/mcp/tools.js +145 -0
  83. package/packages/openclaw/dist/mcp/tools.js.map +1 -0
  84. package/packages/openclaw/dist/runtime/openclaw-config.d.ts +20 -0
  85. package/packages/openclaw/dist/runtime/openclaw-config.d.ts.map +1 -0
  86. package/packages/openclaw/dist/runtime/openclaw-config.js +50 -0
  87. package/packages/openclaw/dist/runtime/openclaw-config.js.map +1 -0
  88. package/packages/openclaw/dist/runtime/patch.d.ts +24 -0
  89. package/packages/openclaw/dist/runtime/patch.d.ts.map +1 -0
  90. package/packages/openclaw/dist/runtime/patch.js +92 -0
  91. package/packages/openclaw/dist/runtime/patch.js.map +1 -0
  92. package/packages/openclaw/dist/runtime/setup.d.ts +26 -0
  93. package/packages/openclaw/dist/runtime/setup.d.ts.map +1 -0
  94. package/packages/openclaw/dist/runtime/setup.js +58 -0
  95. package/packages/openclaw/dist/runtime/setup.js.map +1 -0
  96. package/packages/openclaw/dist/setup.d.ts +29 -0
  97. package/packages/openclaw/dist/setup.d.ts.map +1 -0
  98. package/packages/openclaw/dist/setup.js +300 -0
  99. package/packages/openclaw/dist/setup.js.map +1 -0
  100. package/packages/openclaw/dist/spawn/docker.d.ts +58 -0
  101. package/packages/openclaw/dist/spawn/docker.d.ts.map +1 -0
  102. package/packages/openclaw/dist/spawn/docker.js +222 -0
  103. package/packages/openclaw/dist/spawn/docker.js.map +1 -0
  104. package/packages/openclaw/dist/spawn/manager.d.ts +45 -0
  105. package/packages/openclaw/dist/spawn/manager.d.ts.map +1 -0
  106. package/packages/openclaw/dist/spawn/manager.js +140 -0
  107. package/packages/openclaw/dist/spawn/manager.js.map +1 -0
  108. package/packages/openclaw/dist/spawn/process.d.ts +16 -0
  109. package/packages/openclaw/dist/spawn/process.d.ts.map +1 -0
  110. package/packages/openclaw/dist/spawn/process.js +241 -0
  111. package/packages/openclaw/dist/spawn/process.js.map +1 -0
  112. package/packages/openclaw/dist/spawn/types.d.ts +42 -0
  113. package/packages/openclaw/dist/spawn/types.d.ts.map +1 -0
  114. package/packages/openclaw/dist/spawn/types.js +2 -0
  115. package/packages/openclaw/dist/spawn/types.js.map +1 -0
  116. package/packages/openclaw/dist/types.d.ts +37 -0
  117. package/packages/openclaw/dist/types.d.ts.map +1 -0
  118. package/packages/openclaw/dist/types.js +2 -0
  119. package/packages/openclaw/dist/types.js.map +1 -0
  120. package/packages/openclaw/package.json +63 -0
  121. package/packages/openclaw/skill/SKILL.md +194 -0
  122. package/packages/openclaw/src/__tests__/gateway-threads.test.ts +384 -0
  123. package/packages/openclaw/src/__tests__/naming.test.ts +24 -0
  124. package/packages/openclaw/src/__tests__/spawn-manager.test.ts +152 -0
  125. package/packages/openclaw/src/auth/converter.ts +90 -0
  126. package/packages/openclaw/src/cli.ts +269 -0
  127. package/packages/openclaw/src/config.ts +124 -0
  128. package/packages/openclaw/src/control.ts +100 -0
  129. package/packages/openclaw/src/gateway.ts +941 -0
  130. package/packages/openclaw/src/identity/contract.ts +44 -0
  131. package/packages/openclaw/src/identity/files.ts +198 -0
  132. package/packages/openclaw/src/identity/model.ts +27 -0
  133. package/packages/openclaw/src/identity/naming.ts +6 -0
  134. package/packages/openclaw/src/index.ts +59 -0
  135. package/packages/openclaw/src/inject.ts +77 -0
  136. package/packages/openclaw/src/mcp/server.ts +121 -0
  137. package/packages/openclaw/src/mcp/tools.ts +174 -0
  138. package/packages/openclaw/src/runtime/openclaw-config.ts +64 -0
  139. package/packages/openclaw/src/runtime/patch.ts +103 -0
  140. package/packages/openclaw/src/runtime/setup.ts +89 -0
  141. package/packages/openclaw/src/setup.ts +336 -0
  142. package/packages/openclaw/src/spawn/docker.ts +261 -0
  143. package/packages/openclaw/src/spawn/manager.ts +181 -0
  144. package/packages/openclaw/src/spawn/process.ts +272 -0
  145. package/packages/openclaw/src/spawn/types.ts +43 -0
  146. package/packages/openclaw/src/types.ts +38 -0
  147. package/packages/openclaw/templates/SOUL.md.template +34 -0
  148. package/packages/openclaw/tsconfig.json +12 -0
  149. package/packages/policy/package.json +2 -2
  150. package/packages/sdk/README.md +169 -64
  151. package/packages/sdk/dist/__tests__/contract-fixtures.test.js +76 -9
  152. package/packages/sdk/dist/__tests__/contract-fixtures.test.js.map +1 -1
  153. package/packages/sdk/dist/__tests__/integration.test.js +5 -4
  154. package/packages/sdk/dist/__tests__/integration.test.js.map +1 -1
  155. package/packages/sdk/dist/client.d.ts +34 -3
  156. package/packages/sdk/dist/client.d.ts.map +1 -1
  157. package/packages/sdk/dist/client.js +120 -10
  158. package/packages/sdk/dist/client.js.map +1 -1
  159. package/packages/sdk/dist/protocol.d.ts +7 -1
  160. package/packages/sdk/dist/protocol.d.ts.map +1 -1
  161. package/packages/sdk/dist/relay.d.ts +47 -11
  162. package/packages/sdk/dist/relay.d.ts.map +1 -1
  163. package/packages/sdk/dist/relay.js +114 -23
  164. package/packages/sdk/dist/relay.js.map +1 -1
  165. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  166. package/packages/sdk/dist/workflows/runner.js +71 -36
  167. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  168. package/packages/sdk/dist/workflows/types.d.ts +1 -1
  169. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  170. package/packages/sdk/package.json +2 -2
  171. package/packages/sdk/src/__tests__/contract-fixtures.test.ts +88 -9
  172. package/packages/sdk/src/__tests__/error-scenarios.test.ts +1 -1
  173. package/packages/sdk/src/__tests__/idle-nudge.test.ts +205 -257
  174. package/packages/sdk/src/__tests__/integration.test.ts +5 -4
  175. package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +277 -13
  176. package/packages/sdk/src/__tests__/swarm-coordinator.test.ts +1 -0
  177. package/packages/sdk/src/__tests__/workflow-runner.test.ts +67 -7
  178. package/packages/sdk/src/__tests__/workflow-trajectory.test.ts +4 -5
  179. package/packages/sdk/src/client.ts +171 -14
  180. package/packages/sdk/src/examples/workflows/runner-idle-refactor.yaml +306 -0
  181. package/packages/sdk/src/protocol.ts +7 -2
  182. package/packages/sdk/src/relay.ts +196 -34
  183. package/packages/sdk/src/workflows/runner.ts +73 -42
  184. package/packages/sdk/src/workflows/schema.json +1 -1
  185. package/packages/sdk/src/workflows/types.ts +1 -1
  186. package/packages/sdk/vitest.config.ts +1 -0
  187. package/packages/sdk-py/README.md +89 -102
  188. package/packages/sdk-py/agent_relay/__init__.py +16 -19
  189. package/packages/sdk-py/pyproject.toml +5 -1
  190. package/packages/sdk-py/src/agent_relay/__init__.py +35 -1
  191. package/packages/sdk-py/src/agent_relay/client.py +776 -0
  192. package/packages/sdk-py/src/agent_relay/models.py +27 -0
  193. package/packages/sdk-py/src/agent_relay/protocol.py +114 -0
  194. package/packages/sdk-py/src/agent_relay/relay.py +860 -0
  195. package/packages/sdk-py/tests/test_relay_lifecycle_hooks.py +250 -0
  196. package/packages/telemetry/package.json +1 -1
  197. package/packages/trajectory/package.json +2 -2
  198. package/packages/user-directory/package.json +2 -2
  199. package/packages/utils/package.json +2 -2
  200. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  201. package/bin/agent-relay-broker-darwin-x64 +0 -0
  202. package/bin/agent-relay-broker-linux-arm64 +0 -0
  203. package/packages/sdk/.trajectories/active/traj_1771875803391_84ca57b2.json +0 -50
  204. package/packages/sdk/.trajectories/active/traj_1771891934534_06504121.json +0 -50
  205. package/packages/sdk/.trajectories/active/traj_1771891957929_211afc4e.json +0 -50
  206. package/packages/sdk/.trajectories/active/traj_1771891982509_38c84638.json +0 -50
  207. package/packages/sdk/.trajectories/completed/traj_1771875803188_cd6d181c.json +0 -80
  208. package/packages/sdk/.trajectories/completed/traj_1771875803204_f2aeb8c8.json +0 -80
  209. package/packages/sdk/.trajectories/completed/traj_1771875803210_d65f3f1a.json +0 -80
  210. package/packages/sdk/.trajectories/completed/traj_1771875803218_e454a25d.json +0 -80
  211. package/packages/sdk/.trajectories/completed/traj_1771875803223_d7a64815.json +0 -80
  212. package/packages/sdk/.trajectories/completed/traj_1771875803227_7e56da5b.json +0 -80
  213. package/packages/sdk/.trajectories/completed/traj_1771875803235_4fbf93b4.json +0 -80
  214. package/packages/sdk/.trajectories/completed/traj_1771875803243_47931c71.json +0 -80
  215. package/packages/sdk/.trajectories/completed/traj_1771875803258_3816f3fe.json +0 -80
  216. package/packages/sdk/.trajectories/completed/traj_1771875803268_8061140e.json +0 -80
  217. package/packages/sdk/.trajectories/completed/traj_1771875803326_ae6f9c78.json +0 -80
  218. package/packages/sdk/.trajectories/completed/traj_1771875808396_cbde0a6c.json +0 -91
  219. package/packages/sdk/.trajectories/completed/traj_1771875812026_aa2442bb.json +0 -91
  220. package/packages/sdk/.trajectories/completed/traj_1771875815431_c2c656c5.json +0 -91
  221. package/packages/sdk/.trajectories/completed/traj_1771875818645_3a4dbf02.json +0 -91
  222. package/packages/sdk/.trajectories/completed/traj_1771891934403_24923c03.json +0 -80
  223. package/packages/sdk/.trajectories/completed/traj_1771891934421_dca16e24.json +0 -80
  224. package/packages/sdk/.trajectories/completed/traj_1771891934430_057706f7.json +0 -80
  225. package/packages/sdk/.trajectories/completed/traj_1771891934442_faf97382.json +0 -80
  226. package/packages/sdk/.trajectories/completed/traj_1771891934454_5542ecd5.json +0 -80
  227. package/packages/sdk/.trajectories/completed/traj_1771891934464_12202a08.json +0 -80
  228. package/packages/sdk/.trajectories/completed/traj_1771891934487_94378275.json +0 -80
  229. package/packages/sdk/.trajectories/completed/traj_1771891934503_ca728c13.json +0 -80
  230. package/packages/sdk/.trajectories/completed/traj_1771891934519_100af69a.json +0 -80
  231. package/packages/sdk/.trajectories/completed/traj_1771891934536_62ad39d9.json +0 -80
  232. package/packages/sdk/.trajectories/completed/traj_1771891934553_d6798a52.json +0 -80
  233. package/packages/sdk/.trajectories/completed/traj_1771891939537_541c8096.json +0 -91
  234. package/packages/sdk/.trajectories/completed/traj_1771891942985_36ab9a4d.json +0 -91
  235. package/packages/sdk/.trajectories/completed/traj_1771891946453_e8a6e05f.json +0 -91
  236. package/packages/sdk/.trajectories/completed/traj_1771891949838_5de0de84.json +0 -91
  237. package/packages/sdk/.trajectories/completed/traj_1771891957807_0ecfb4f4.json +0 -80
  238. package/packages/sdk/.trajectories/completed/traj_1771891957827_c4539239.json +0 -80
  239. package/packages/sdk/.trajectories/completed/traj_1771891957836_91168b48.json +0 -80
  240. package/packages/sdk/.trajectories/completed/traj_1771891957848_8c5cad0b.json +0 -80
  241. package/packages/sdk/.trajectories/completed/traj_1771891957857_0986b293.json +0 -80
  242. package/packages/sdk/.trajectories/completed/traj_1771891957872_8a3113af.json +0 -80
  243. package/packages/sdk/.trajectories/completed/traj_1771891957884_0bb85208.json +0 -80
  244. package/packages/sdk/.trajectories/completed/traj_1771891957892_86c75e2e.json +0 -80
  245. package/packages/sdk/.trajectories/completed/traj_1771891957907_98ca0e6f.json +0 -80
  246. package/packages/sdk/.trajectories/completed/traj_1771891957918_d9091231.json +0 -80
  247. package/packages/sdk/.trajectories/completed/traj_1771891957931_dcaf77ed.json +0 -80
  248. package/packages/sdk/.trajectories/completed/traj_1771891962931_eb1fdee2.json +0 -91
  249. package/packages/sdk/.trajectories/completed/traj_1771891966262_9061a93f.json +0 -91
  250. package/packages/sdk/.trajectories/completed/traj_1771891969915_1adaba19.json +0 -91
  251. package/packages/sdk/.trajectories/completed/traj_1771891973588_f08b79e9.json +0 -91
  252. package/packages/sdk/.trajectories/completed/traj_1771891982421_f1985bce.json +0 -80
  253. package/packages/sdk/.trajectories/completed/traj_1771891982432_e7a84163.json +0 -80
  254. package/packages/sdk/.trajectories/completed/traj_1771891982447_369b842a.json +0 -80
  255. package/packages/sdk/.trajectories/completed/traj_1771891982469_5fc45199.json +0 -80
  256. package/packages/sdk/.trajectories/completed/traj_1771891982495_454c7cb3.json +0 -80
  257. package/packages/sdk/.trajectories/completed/traj_1771891982514_08098e03.json +0 -80
  258. package/packages/sdk/.trajectories/completed/traj_1771891982526_b351d778.json +0 -80
  259. package/packages/sdk/.trajectories/completed/traj_1771891982533_fa542d83.json +0 -80
  260. package/packages/sdk/.trajectories/completed/traj_1771891982540_18ab24dc.json +0 -80
  261. package/packages/sdk/.trajectories/completed/traj_1771891982544_5b4fa163.json +0 -80
  262. package/packages/sdk/.trajectories/completed/traj_1771891982548_c13f089a.json +0 -80
  263. package/packages/sdk/.trajectories/completed/traj_1771891987510_23f6da1f.json +0 -91
  264. package/packages/sdk/.trajectories/completed/traj_1771891991466_912c2e04.json +0 -91
  265. package/packages/sdk/.trajectories/completed/traj_1771891994891_60604be2.json +0 -91
  266. package/packages/sdk/.trajectories/completed/traj_1771891998370_cfaf9b8b.json +0 -91
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Idle nudge detection and escalation tests.
3
3
  *
4
- * Tests that the WorkflowRunner correctly detects idle agents, sends nudges
5
- * (hub-mediated or direct), and force-releases after maxNudges.
4
+ * Covers both modes:
5
+ * - No idleNudge config: idle is treated as completion.
6
+ * - idleNudge config enabled: waitForExit timeout drives nudges/escalation.
6
7
  */
7
8
 
8
9
  import { describe, it, expect, vi, beforeEach } from 'vitest';
@@ -18,7 +19,7 @@ const mockFetch = vi.fn().mockResolvedValue({
18
19
  });
19
20
  vi.stubGlobal('fetch', mockFetch);
20
21
 
21
- // ── Mock RelayCast SDK ──────────────────────────────────────────────────────
22
+ // ── Mock RelayCast SDK ────────────────────────────────────────────────────────
22
23
 
23
24
  const mockRelaycastAgent = {
24
25
  send: vi.fn().mockResolvedValue(undefined),
@@ -52,9 +53,8 @@ vi.mock('@relaycast/sdk', () => ({
52
53
  RelayError: MockRelayError,
53
54
  }));
54
55
 
55
- // ── Mock AgentRelay ──────────────────────────────────────────────────────────
56
+ // ── Mock AgentRelay ───────────────────────────────────────────────────────────
56
57
 
57
- /** Control how waitForExit / waitForIdle resolve in each test. */
58
58
  let waitForExitFn: (ms?: number) => Promise<'exited' | 'timeout' | 'released'>;
59
59
  let waitForIdleFn: (ms?: number) => Promise<'idle' | 'timeout' | 'exited'>;
60
60
 
@@ -84,15 +84,15 @@ vi.mock('../relay.js', () => ({
84
84
  spawnPty: vi.fn().mockResolvedValue(mockAgent),
85
85
  human: vi.fn().mockReturnValue(mockHuman),
86
86
  shutdown: vi.fn().mockResolvedValue(undefined),
87
+ onBrokerStderr: vi.fn().mockReturnValue(() => {}),
87
88
  onWorkerOutput: null,
88
89
  listAgentsRaw: vi.fn().mockResolvedValue([]),
89
90
  })),
90
91
  }));
91
92
 
92
- // Import after mocking
93
93
  const { WorkflowRunner } = await import('../workflows/runner.js');
94
94
 
95
- // ── Test fixtures ────────────────────────────────────────────────────────────
95
+ // ── Test fixtures ─────────────────────────────────────────────────────────────
96
96
 
97
97
  function makeDb(): WorkflowDb {
98
98
  const runs = new Map<string, WorkflowRunRow>();
@@ -135,11 +135,14 @@ function makeConfig(overrides: Partial<RelayYamlConfig> = {}): RelayYamlConfig {
135
135
  steps: [{ name: 'step-1', agent: 'agent-a', task: 'Do step 1' }],
136
136
  },
137
137
  ],
138
+ trajectories: false,
138
139
  ...overrides,
139
140
  };
140
141
  }
141
142
 
142
- // ── Tests ────────────────────────────────────────────────────────────────────
143
+ function never<T>(): Promise<T> {
144
+ return new Promise(() => {});
145
+ }
143
146
 
144
147
  describe('Idle Nudge Detection', () => {
145
148
  let db: WorkflowDb;
@@ -150,289 +153,234 @@ describe('Idle Nudge Detection', () => {
150
153
  db = makeDb();
151
154
  runner = new WorkflowRunner({ db, workspaceId: 'ws-test' });
152
155
 
153
- // Default: agent exits immediately (no idle)
154
156
  waitForExitFn = vi.fn().mockResolvedValue('exited');
155
157
  waitForIdleFn = vi.fn().mockResolvedValue('timeout');
156
158
  });
157
159
 
158
- it('should not nudge when idleNudge config is absent', async () => {
159
- // No idleNudge in swarm config simple waitForExit
160
- const config = makeConfig();
161
- const run = await runner.execute(config, 'default');
162
-
163
- expect(run.status).toBe('completed');
164
- expect(mockHumanSendMessage).not.toHaveBeenCalled();
165
- expect(mockSendMessage).not.toHaveBeenCalled();
166
- expect(mockRelease).not.toHaveBeenCalled();
167
- });
168
-
169
- it('should send direct nudge after idle detection', async () => {
170
- let idleCallCount = 0;
171
- // First waitForIdle resolves with 'idle', second with 'exited' (agent responds)
172
- waitForIdleFn = vi.fn().mockImplementation(() => {
173
- idleCallCount++;
174
- if (idleCallCount === 1) return Promise.resolve('idle');
175
- return Promise.resolve('exited');
176
- });
177
- // waitForExit never resolves quickly — make it lose the race
178
- waitForExitFn = vi.fn().mockImplementation(() => new Promise(() => {}));
179
- // But after nudge, agent exits — so second iteration exit resolves
180
- let exitCallCount = 0;
181
- waitForExitFn = vi.fn().mockImplementation(() => {
182
- exitCallCount++;
183
- if (exitCallCount === 1) return new Promise(() => {}); // lose first race
184
- return Promise.resolve('exited'); // win second race
160
+ describe('idleNudge enabled', () => {
161
+ it('sends direct nudge then completes when exit follows', async () => {
162
+ let exitCallCount = 0;
163
+ waitForExitFn = vi.fn().mockImplementation(() => {
164
+ exitCallCount++;
165
+ return Promise.resolve(exitCallCount === 1 ? 'timeout' : 'exited');
166
+ });
167
+
168
+ const run = await runner.execute(
169
+ makeConfig({
170
+ swarm: {
171
+ pattern: 'mesh',
172
+ idleNudge: { nudgeAfterMs: 100, escalateAfterMs: 100, maxNudges: 1 },
173
+ },
174
+ }),
175
+ 'default'
176
+ );
177
+
178
+ expect(run.status).toBe('completed');
179
+ expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
180
+ expect(mockHumanSendMessage).toHaveBeenCalledWith(
181
+ expect.objectContaining({
182
+ to: 'test-agent-abc',
183
+ text: expect.stringContaining('/exit'),
184
+ })
185
+ );
186
+ expect(mockRelease).not.toHaveBeenCalled();
187
+ expect(waitForIdleFn).not.toHaveBeenCalled();
185
188
  });
186
189
 
187
- const config = makeConfig({
188
- swarm: {
189
- pattern: 'mesh', // non-hub pattern → direct nudge
190
- idleNudge: { nudgeAfterMs: 100, escalateAfterMs: 100, maxNudges: 1 },
191
- },
190
+ it('uses hub fallback behavior without failing when hub is not active', async () => {
191
+ let exitCallCount = 0;
192
+ waitForExitFn = vi.fn().mockImplementation(() => {
193
+ exitCallCount++;
194
+ return Promise.resolve(exitCallCount === 1 ? 'timeout' : 'exited');
195
+ });
196
+
197
+ const run = await runner.execute(
198
+ makeConfig({
199
+ swarm: {
200
+ pattern: 'hub-spoke',
201
+ idleNudge: { nudgeAfterMs: 100, escalateAfterMs: 100, maxNudges: 1 },
202
+ },
203
+ agents: [
204
+ { name: 'lead', cli: 'claude', role: 'Lead coordinator' },
205
+ { name: 'worker', cli: 'claude' },
206
+ ],
207
+ workflows: [
208
+ {
209
+ name: 'default',
210
+ steps: [{ name: 'step-1', agent: 'worker', task: 'Do work' }],
211
+ },
212
+ ],
213
+ }),
214
+ 'default'
215
+ );
216
+
217
+ expect(run.status).toBe('completed');
218
+ expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
192
219
  });
193
220
 
194
- const run = await runner.execute(config, 'default');
195
-
196
- expect(run.status).toBe('completed');
197
- // Direct nudge via human handle
198
- expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
199
- expect(mockHumanSendMessage).toHaveBeenCalledWith(
200
- expect.objectContaining({
201
- to: 'test-agent-abc',
202
- text: expect.stringContaining('/exit'),
203
- })
204
- );
205
- });
206
-
207
- it('should use hub-mediated nudge for hub patterns when hub exists', async () => {
208
- let idleCallCount = 0;
209
- waitForIdleFn = vi.fn().mockImplementation(() => {
210
- idleCallCount++;
211
- if (idleCallCount === 1) return Promise.resolve('idle');
212
- return Promise.resolve('exited');
213
- });
214
- let exitCallCount = 0;
215
- waitForExitFn = vi.fn().mockImplementation(() => {
216
- exitCallCount++;
217
- if (exitCallCount === 1) return new Promise(() => {});
218
- return Promise.resolve('exited');
221
+ it('force-releases after maxNudges is exceeded', async () => {
222
+ waitForExitFn = vi.fn().mockResolvedValue('timeout');
223
+
224
+ const run = await runner.execute(
225
+ makeConfig({
226
+ swarm: {
227
+ pattern: 'dag',
228
+ idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 1 },
229
+ },
230
+ }),
231
+ 'default'
232
+ );
233
+
234
+ expect(run.status).toBe('completed');
235
+ expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
236
+ expect(mockRelease).toHaveBeenCalledTimes(1);
237
+ expect(waitForIdleFn).not.toHaveBeenCalled();
219
238
  });
220
239
 
221
- const config = makeConfig({
222
- swarm: {
223
- pattern: 'hub-spoke', // hub pattern
224
- idleNudge: { nudgeAfterMs: 100, escalateAfterMs: 100, maxNudges: 1 },
225
- },
226
- agents: [
227
- { name: 'lead', cli: 'claude', role: 'Lead coordinator' },
228
- { name: 'worker', cli: 'claude' },
229
- ],
230
- workflows: [
231
- {
232
- name: 'default',
233
- steps: [{ name: 'step-1', agent: 'worker', task: 'Do work' }],
234
- },
235
- ],
240
+ it('force-releases after multiple nudges', async () => {
241
+ waitForExitFn = vi.fn().mockResolvedValue('timeout');
242
+
243
+ const run = await runner.execute(
244
+ makeConfig({
245
+ swarm: {
246
+ pattern: 'dag',
247
+ idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 3 },
248
+ },
249
+ }),
250
+ 'default'
251
+ );
252
+
253
+ expect(run.status).toBe('completed');
254
+ expect(mockHumanSendMessage).toHaveBeenCalledTimes(3);
255
+ expect(mockRelease).toHaveBeenCalledTimes(1);
236
256
  });
237
257
 
238
- const run = await runner.execute(config, 'default');
239
-
240
- // Since the hub (lead) is not spawned in this workflow step, it falls back to direct nudge
241
- // The hub-mediated path requires the hub to be in activeAgentHandles
242
- expect(run.status).toBe('completed');
243
- });
244
-
245
- it('should send direct nudge when idle agent IS the hub', async () => {
246
- let idleCallCount = 0;
247
- waitForIdleFn = vi.fn().mockImplementation(() => {
248
- idleCallCount++;
249
- if (idleCallCount === 1) return Promise.resolve('idle');
250
- return Promise.resolve('exited');
251
- });
252
- let exitCallCount = 0;
253
- waitForExitFn = vi.fn().mockImplementation(() => {
254
- exitCallCount++;
255
- if (exitCallCount === 1) return new Promise(() => {});
256
- return Promise.resolve('exited');
257
- });
258
-
259
- const config = makeConfig({
260
- swarm: {
261
- pattern: 'hub-spoke',
262
- idleNudge: { nudgeAfterMs: 100, escalateAfterMs: 100, maxNudges: 1 },
263
- },
264
- agents: [{ name: 'lead', cli: 'claude', role: 'Lead coordinator' }],
265
- workflows: [
266
- {
267
- name: 'default',
268
- steps: [{ name: 'step-1', agent: 'lead', task: 'Coordinate work' }],
269
- },
270
- ],
258
+ it('emits step:nudged event', async () => {
259
+ let exitCallCount = 0;
260
+ waitForExitFn = vi.fn().mockImplementation(() => {
261
+ exitCallCount++;
262
+ return Promise.resolve(exitCallCount === 1 ? 'timeout' : 'exited');
263
+ });
264
+
265
+ const events: Array<{ type: string }> = [];
266
+ runner.on((event) => events.push(event));
267
+
268
+ await runner.execute(
269
+ makeConfig({
270
+ swarm: {
271
+ pattern: 'dag',
272
+ idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 1 },
273
+ },
274
+ }),
275
+ 'default'
276
+ );
277
+
278
+ expect(events.filter((e) => e.type === 'step:nudged')).toHaveLength(1);
271
279
  });
272
280
 
273
- const run = await runner.execute(config, 'default');
281
+ it('emits step:force-released event on escalation', async () => {
282
+ waitForExitFn = vi.fn().mockResolvedValue('timeout');
274
283
 
275
- expect(run.status).toBe('completed');
276
- // Should use direct nudge since idle agent is the hub itself
277
- expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
278
- });
284
+ const events: Array<{ type: string }> = [];
285
+ runner.on((event) => events.push(event));
279
286
 
280
- it('should force-release after maxNudges exceeded', async () => {
281
- // Idle always fires, never exits
282
- waitForIdleFn = vi.fn().mockResolvedValue('idle');
283
- waitForExitFn = vi.fn().mockImplementation(() => new Promise(() => {})); // never resolves
287
+ await runner.execute(
288
+ makeConfig({
289
+ swarm: {
290
+ pattern: 'dag',
291
+ idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 1 },
292
+ },
293
+ }),
294
+ 'default'
295
+ );
284
296
 
285
- const config = makeConfig({
286
- swarm: {
287
- pattern: 'dag',
288
- idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 1 },
289
- },
297
+ expect(events.filter((e) => e.type === 'step:force-released')).toHaveLength(1);
290
298
  });
291
299
 
292
- const run = await runner.execute(config, 'default');
293
-
294
- // Force-released → still captures output → completes
295
- expect(run.status).toBe('completed');
296
- expect(mockRelease).toHaveBeenCalledTimes(1);
297
- expect(mockHumanSendMessage).toHaveBeenCalledTimes(1); // 1 nudge before escalation
298
- });
299
-
300
- it('should force-release after multiple nudges', async () => {
301
- waitForIdleFn = vi.fn().mockResolvedValue('idle');
302
- waitForExitFn = vi.fn().mockImplementation(() => new Promise(() => {}));
303
-
304
- const config = makeConfig({
305
- swarm: {
306
- pattern: 'dag',
307
- idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 3 },
308
- },
300
+ it('uses defaults when idleNudge is empty object', async () => {
301
+ waitForExitFn = vi.fn().mockResolvedValue('timeout');
302
+
303
+ const run = await runner.execute(
304
+ makeConfig({
305
+ swarm: {
306
+ pattern: 'dag',
307
+ idleNudge: {},
308
+ },
309
+ }),
310
+ 'default'
311
+ );
312
+
313
+ expect(run.status).toBe('completed');
314
+ // default maxNudges is 1
315
+ expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
316
+ expect(mockRelease).toHaveBeenCalledTimes(1);
309
317
  });
310
318
 
311
- const run = await runner.execute(config, 'default');
312
-
313
- expect(run.status).toBe('completed');
314
- expect(mockRelease).toHaveBeenCalledTimes(1);
315
- expect(mockHumanSendMessage).toHaveBeenCalledTimes(3); // 3 nudges before escalation
316
- });
317
-
318
- it('should respect overall timeout despite nudge loop', async () => {
319
- // Idle fires quickly, but overall timeout is very short
320
- waitForIdleFn = vi.fn().mockResolvedValue('idle');
321
- waitForExitFn = vi
322
- .fn()
323
- .mockImplementation(() => new Promise((resolve) => setTimeout(() => resolve('timeout'), 50)));
324
-
325
- const config = makeConfig({
326
- swarm: {
327
- pattern: 'dag',
328
- idleNudge: { nudgeAfterMs: 10, escalateAfterMs: 10, maxNudges: 10 },
329
- },
330
- agents: [{ name: 'agent-a', cli: 'claude', constraints: { timeoutMs: 100 } }],
319
+ it('respects overall timeout during nudge loop', async () => {
320
+ // Each waitForExit call takes 100ms (real timer), but the overall timeout
321
+ // is only 80ms. After the first call (~100ms elapsed), the loop detects
322
+ // that remaining time is exhausted and returns 'timeout'.
323
+ waitForExitFn = vi
324
+ .fn()
325
+ .mockImplementation(
326
+ () => new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), 100))
327
+ );
328
+
329
+ const run = await runner.execute(
330
+ makeConfig({
331
+ swarm: {
332
+ pattern: 'dag',
333
+ idleNudge: { nudgeAfterMs: 10, escalateAfterMs: 10, maxNudges: 10 },
334
+ },
335
+ agents: [{ name: 'agent-a', cli: 'claude', constraints: { timeoutMs: 80 } }],
336
+ }),
337
+ 'default'
338
+ );
339
+
340
+ expect(run.status).toBe('failed');
341
+ expect(run.error).toContain('timed out');
331
342
  });
332
-
333
- // The step has a short timeout — should not loop forever
334
- const run = await runner.execute(config, 'default');
335
- // Either completed (force-released) or failed (timeout) — either is acceptable
336
- expect(['completed', 'failed']).toContain(run.status);
337
343
  });
338
344
 
339
- it('should emit step:nudged event', async () => {
340
- let idleCallCount = 0;
341
- waitForIdleFn = vi.fn().mockImplementation(() => {
342
- idleCallCount++;
343
- if (idleCallCount === 1) return Promise.resolve('idle');
344
- return Promise.resolve('exited');
345
- });
346
- let exitCallCount = 0;
347
- waitForExitFn = vi.fn().mockImplementation(() => {
348
- exitCallCount++;
349
- if (exitCallCount === 1) return new Promise(() => {});
350
- return Promise.resolve('exited');
351
- });
352
-
353
- const events: Array<{ type: string }> = [];
354
- const config = makeConfig({
355
- swarm: {
356
- pattern: 'dag',
357
- idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 1 },
358
- },
359
- });
360
-
361
- runner.on((event) => events.push(event));
362
- await runner.execute(config, 'default');
363
-
364
- const nudgeEvents = events.filter((e) => e.type === 'step:nudged');
365
- expect(nudgeEvents).toHaveLength(1);
366
- });
345
+ describe('Idle = done (no idleNudge config)', () => {
346
+ it('idle fires first: releases agent and completes step', async () => {
347
+ waitForIdleFn = vi.fn().mockResolvedValue('idle');
348
+ waitForExitFn = vi.fn().mockImplementation(() => never());
367
349
 
368
- it('should emit step:force-released event on escalation', async () => {
369
- waitForIdleFn = vi.fn().mockResolvedValue('idle');
370
- waitForExitFn = vi.fn().mockImplementation(() => new Promise(() => {}));
350
+ const run = await runner.execute(makeConfig(), 'default');
351
+ const steps = await db.getStepsByRunId(run.id);
371
352
 
372
- const events: Array<{ type: string }> = [];
373
- const config = makeConfig({
374
- swarm: {
375
- pattern: 'dag',
376
- idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 1 },
377
- },
353
+ expect(run.status).toBe('completed');
354
+ expect(steps).toHaveLength(1);
355
+ expect(steps[0]?.status).toBe('completed');
356
+ expect(mockRelease).toHaveBeenCalledTimes(1);
378
357
  });
379
358
 
380
- runner.on((event) => events.push(event));
381
- await runner.execute(config, 'default');
382
-
383
- const forceReleasedEvents = events.filter((e) => e.type === 'step:force-released');
384
- expect(forceReleasedEvents).toHaveLength(1);
385
- });
359
+ it('exit fires first: completes without idle-based release', async () => {
360
+ waitForExitFn = vi.fn().mockResolvedValue('exited');
361
+ waitForIdleFn = vi.fn().mockResolvedValue('timeout');
386
362
 
387
- it('should handle agent responding to nudge: idle → nudge → output → idle → escalate', async () => {
388
- let idleCallCount = 0;
389
- // First idle → nudge. Second idle → escalate (maxNudges: 1)
390
- waitForIdleFn = vi.fn().mockImplementation(() => {
391
- idleCallCount++;
392
- return Promise.resolve('idle');
393
- });
394
- waitForExitFn = vi.fn().mockImplementation(() => new Promise(() => {}));
363
+ const run = await runner.execute(makeConfig(), 'default');
364
+ const steps = await db.getStepsByRunId(run.id);
395
365
 
396
- const config = makeConfig({
397
- swarm: {
398
- pattern: 'dag',
399
- idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 1 },
400
- },
366
+ expect(run.status).toBe('completed');
367
+ expect(steps).toHaveLength(1);
368
+ expect(steps[0]?.status).toBe('completed');
369
+ expect(mockRelease).not.toHaveBeenCalled();
401
370
  });
402
371
 
403
- const run = await runner.execute(config, 'default');
404
-
405
- expect(run.status).toBe('completed');
406
- // 1 nudge sent, then force-released
407
- expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
408
- expect(mockRelease).toHaveBeenCalledTimes(1);
409
- });
372
+ it('both timeout: fails step with timeout error', async () => {
373
+ waitForExitFn = vi.fn().mockResolvedValue('timeout');
374
+ waitForIdleFn = vi.fn().mockResolvedValue('timeout');
410
375
 
411
- it('should use defaults when idleNudge is empty object', async () => {
412
- let idleCallCount = 0;
413
- waitForIdleFn = vi.fn().mockImplementation(() => {
414
- idleCallCount++;
415
- if (idleCallCount === 1) return Promise.resolve('idle');
416
- return Promise.resolve('exited');
417
- });
418
- let exitCallCount = 0;
419
- waitForExitFn = vi.fn().mockImplementation(() => {
420
- exitCallCount++;
421
- if (exitCallCount === 1) return new Promise(() => {});
422
- return Promise.resolve('exited');
423
- });
376
+ const run = await runner.execute(makeConfig(), 'default');
377
+ const steps = await db.getStepsByRunId(run.id);
424
378
 
425
- const config = makeConfig({
426
- swarm: {
427
- pattern: 'dag',
428
- idleNudge: {}, // empty — should use defaults
429
- },
379
+ expect(run.status).toBe('failed');
380
+ expect(run.error).toContain('timed out');
381
+ expect(steps).toHaveLength(1);
382
+ expect(steps[0]?.status).toBe('failed');
383
+ expect(steps[0]?.error).toContain('timed out');
430
384
  });
431
-
432
- const run = await runner.execute(config, 'default');
433
-
434
- expect(run.status).toBe('completed');
435
- // Default maxNudges: 1, so one nudge should have been sent
436
- expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
437
385
  });
438
386
  });
@@ -103,7 +103,7 @@ test('sdk can start broker and manage agent lifecycle', async (t) => {
103
103
  }
104
104
  });
105
105
 
106
- test('sdk can spawn and release headless claude worker', async (t) => {
106
+ test('sdk can spawn and release provider worker with transport override', async (t) => {
107
107
  const binaryPath = resolveBinaryPath();
108
108
  if (!fs.existsSync(binaryPath)) {
109
109
  t.skip(`agent-relay-broker binary not found at ${binaryPath}`);
@@ -124,17 +124,18 @@ test('sdk can spawn and release headless claude worker', async (t) => {
124
124
  });
125
125
 
126
126
  try {
127
- const spawned = await client.spawnHeadlessClaude({
127
+ const spawned = await client.spawnClaude({
128
128
  name: spawnedName,
129
+ transport: 'headless',
129
130
  channels: ['general'],
130
131
  });
131
132
  assert.equal(spawned.name, spawnedName);
132
- assert.equal(spawned.runtime, 'headless_claude');
133
+ assert.equal(spawned.runtime, 'headless');
133
134
 
134
135
  const agentsAfterSpawn = await client.listAgents();
135
136
  const spawnedAgent = agentsAfterSpawn.find((agent) => agent.name === spawnedName);
136
137
  assert.ok(spawnedAgent, 'spawned headless agent should be present in listAgents()');
137
- assert.equal(spawnedAgent?.runtime, 'headless_claude');
138
+ assert.equal(spawnedAgent?.runtime, 'headless');
138
139
 
139
140
  const released = await client.release(spawnedName);
140
141
  assert.equal(released.name, spawnedName);