agent-relay 3.2.2 → 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 (246) 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 +1358 -941
  6. package/dist/src/cli/commands/agent-management.d.ts +2 -2
  7. package/dist/src/cli/commands/agent-management.d.ts.map +1 -1
  8. package/dist/src/cli/commands/agent-management.js +41 -240
  9. package/dist/src/cli/commands/agent-management.js.map +1 -1
  10. package/dist/src/cli/commands/messaging.d.ts +1 -1
  11. package/dist/src/cli/commands/messaging.d.ts.map +1 -1
  12. package/dist/src/cli/commands/messaging.js +14 -5
  13. package/dist/src/cli/commands/messaging.js.map +1 -1
  14. package/dist/src/cli/lib/agent-management-listing.d.ts +4 -1
  15. package/dist/src/cli/lib/agent-management-listing.d.ts.map +1 -1
  16. package/dist/src/cli/lib/agent-management-listing.js +27 -2
  17. package/dist/src/cli/lib/agent-management-listing.js.map +1 -1
  18. package/package.json +11 -10
  19. package/packages/acp-bridge/package.json +2 -2
  20. package/packages/config/package.json +1 -1
  21. package/packages/hooks/package.json +4 -4
  22. package/packages/memory/package.json +2 -2
  23. package/packages/openclaw/package.json +2 -2
  24. package/packages/policy/package.json +2 -2
  25. package/packages/sdk/ADAPTER_REVIEW.md +109 -0
  26. package/packages/sdk/dist/client.d.ts +66 -0
  27. package/packages/sdk/dist/client.d.ts.map +1 -1
  28. package/packages/sdk/dist/client.js +230 -0
  29. package/packages/sdk/dist/client.js.map +1 -1
  30. package/packages/sdk/dist/communicate/a2a-bridge.d.ts +25 -0
  31. package/packages/sdk/dist/communicate/a2a-bridge.d.ts.map +1 -0
  32. package/packages/sdk/dist/communicate/a2a-bridge.js +89 -0
  33. package/packages/sdk/dist/communicate/a2a-bridge.js.map +1 -0
  34. package/packages/sdk/dist/communicate/a2a-server.d.ts +31 -0
  35. package/packages/sdk/dist/communicate/a2a-server.d.ts.map +1 -0
  36. package/packages/sdk/dist/communicate/a2a-server.js +220 -0
  37. package/packages/sdk/dist/communicate/a2a-server.js.map +1 -0
  38. package/packages/sdk/dist/communicate/a2a-transport.d.ts +48 -0
  39. package/packages/sdk/dist/communicate/a2a-transport.d.ts.map +1 -0
  40. package/packages/sdk/dist/communicate/a2a-transport.js +302 -0
  41. package/packages/sdk/dist/communicate/a2a-transport.js.map +1 -0
  42. package/packages/sdk/dist/communicate/a2a-types.d.ts +107 -0
  43. package/packages/sdk/dist/communicate/a2a-types.d.ts.map +1 -0
  44. package/packages/sdk/dist/communicate/a2a-types.js +209 -0
  45. package/packages/sdk/dist/communicate/a2a-types.js.map +1 -0
  46. package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts +28 -0
  47. package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts.map +1 -0
  48. package/packages/sdk/dist/communicate/adapters/claude-sdk.js +47 -0
  49. package/packages/sdk/dist/communicate/adapters/claude-sdk.js.map +1 -0
  50. package/packages/sdk/dist/communicate/adapters/crewai.d.ts +42 -0
  51. package/packages/sdk/dist/communicate/adapters/crewai.d.ts.map +1 -0
  52. package/packages/sdk/dist/communicate/adapters/crewai.js +95 -0
  53. package/packages/sdk/dist/communicate/adapters/crewai.js.map +1 -0
  54. package/packages/sdk/dist/communicate/adapters/google-adk.d.ts +53 -0
  55. package/packages/sdk/dist/communicate/adapters/google-adk.d.ts.map +1 -0
  56. package/packages/sdk/dist/communicate/adapters/google-adk.js +77 -0
  57. package/packages/sdk/dist/communicate/adapters/google-adk.js.map +1 -0
  58. package/packages/sdk/dist/communicate/adapters/index.d.ts +7 -0
  59. package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -0
  60. package/packages/sdk/dist/communicate/adapters/index.js +7 -0
  61. package/packages/sdk/dist/communicate/adapters/index.js.map +1 -0
  62. package/packages/sdk/dist/communicate/adapters/langgraph.d.ts +40 -0
  63. package/packages/sdk/dist/communicate/adapters/langgraph.d.ts.map +1 -0
  64. package/packages/sdk/dist/communicate/adapters/langgraph.js +77 -0
  65. package/packages/sdk/dist/communicate/adapters/langgraph.js.map +1 -0
  66. package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts +25 -0
  67. package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts.map +1 -0
  68. package/packages/sdk/dist/communicate/adapters/openai-agents.js +70 -0
  69. package/packages/sdk/dist/communicate/adapters/openai-agents.js.map +1 -0
  70. package/packages/sdk/dist/communicate/adapters/pi.d.ts +45 -0
  71. package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -0
  72. package/packages/sdk/dist/communicate/adapters/pi.js +59 -0
  73. package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -0
  74. package/packages/sdk/dist/communicate/core.d.ts +58 -0
  75. package/packages/sdk/dist/communicate/core.d.ts.map +1 -0
  76. package/packages/sdk/dist/communicate/core.js +128 -0
  77. package/packages/sdk/dist/communicate/core.js.map +1 -0
  78. package/packages/sdk/dist/communicate/index.d.ts +4 -0
  79. package/packages/sdk/dist/communicate/index.d.ts.map +1 -0
  80. package/packages/sdk/dist/communicate/index.js +4 -0
  81. package/packages/sdk/dist/communicate/index.js.map +1 -0
  82. package/packages/sdk/dist/communicate/transport.d.ts +36 -0
  83. package/packages/sdk/dist/communicate/transport.d.ts.map +1 -0
  84. package/packages/sdk/dist/communicate/transport.js +371 -0
  85. package/packages/sdk/dist/communicate/transport.js.map +1 -0
  86. package/packages/sdk/dist/communicate/types.d.ts +58 -0
  87. package/packages/sdk/dist/communicate/types.d.ts.map +1 -0
  88. package/packages/sdk/dist/communicate/types.js +66 -0
  89. package/packages/sdk/dist/communicate/types.js.map +1 -0
  90. package/packages/sdk/dist/workflows/builder.d.ts +35 -5
  91. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  92. package/packages/sdk/dist/workflows/builder.js +81 -7
  93. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  94. package/packages/sdk/dist/workflows/cli.js +14 -1
  95. package/packages/sdk/dist/workflows/cli.js.map +1 -1
  96. package/packages/sdk/dist/workflows/runner.d.ts +10 -2
  97. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  98. package/packages/sdk/dist/workflows/runner.js +95 -1
  99. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  100. package/packages/sdk/dist/workflows/types.d.ts +11 -0
  101. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  102. package/packages/sdk/examples/communicate/claude_sdk_example.ts +5 -0
  103. package/packages/sdk/examples/communicate/pi_example.ts +8 -0
  104. package/packages/sdk/package.json +48 -2
  105. package/packages/sdk/src/__tests__/builder-deterministic.test.ts +132 -0
  106. package/packages/sdk/src/__tests__/communicate/a2a-bridge.test.ts +211 -0
  107. package/packages/sdk/src/__tests__/communicate/a2a-server.test.ts +359 -0
  108. package/packages/sdk/src/__tests__/communicate/a2a-transport.test.ts +537 -0
  109. package/packages/sdk/src/__tests__/communicate/a2a-types.test.ts +297 -0
  110. package/packages/sdk/src/__tests__/communicate/adapters/claude-sdk.test.ts +163 -0
  111. package/packages/sdk/src/__tests__/communicate/adapters/crewai.test.ts +219 -0
  112. package/packages/sdk/src/__tests__/communicate/adapters/e2e-crewai.test.ts +101 -0
  113. package/packages/sdk/src/__tests__/communicate/adapters/e2e-google-adk.test.ts +166 -0
  114. package/packages/sdk/src/__tests__/communicate/adapters/e2e-langgraph.test.ts +181 -0
  115. package/packages/sdk/src/__tests__/communicate/adapters/e2e-openai-agents.test.ts +137 -0
  116. package/packages/sdk/src/__tests__/communicate/adapters/e2e-pi.test.ts +140 -0
  117. package/packages/sdk/src/__tests__/communicate/adapters/google-adk.test.ts +200 -0
  118. package/packages/sdk/src/__tests__/communicate/adapters/langgraph.test.ts +162 -0
  119. package/packages/sdk/src/__tests__/communicate/adapters/openai-agents.test.ts +166 -0
  120. package/packages/sdk/src/__tests__/communicate/adapters/pi.test.ts +140 -0
  121. package/packages/sdk/src/__tests__/communicate/core.test.ts +574 -0
  122. package/packages/sdk/src/__tests__/communicate/integration/cross-framework.test.ts +353 -0
  123. package/packages/sdk/src/__tests__/communicate/transport.test.ts +613 -0
  124. package/packages/sdk/src/__tests__/start-from.test.ts +346 -0
  125. package/packages/sdk/src/client.ts +301 -0
  126. package/packages/sdk/src/communicate/a2a-bridge.ts +111 -0
  127. package/packages/sdk/src/communicate/a2a-server.ts +277 -0
  128. package/packages/sdk/src/communicate/a2a-transport.ts +395 -0
  129. package/packages/sdk/src/communicate/a2a-types.ts +338 -0
  130. package/packages/sdk/src/communicate/adapters/claude-sdk.ts +85 -0
  131. package/packages/sdk/src/communicate/adapters/crewai.ts +141 -0
  132. package/packages/sdk/src/communicate/adapters/google-adk.ts +139 -0
  133. package/packages/sdk/src/communicate/adapters/index.ts +6 -0
  134. package/packages/sdk/src/communicate/adapters/langgraph.ts +112 -0
  135. package/packages/sdk/src/communicate/adapters/openai-agents.ts +113 -0
  136. package/packages/sdk/src/communicate/adapters/pi.ts +105 -0
  137. package/packages/sdk/src/communicate/core.ts +157 -0
  138. package/packages/sdk/src/communicate/index.ts +3 -0
  139. package/packages/sdk/src/communicate/transport.ts +489 -0
  140. package/packages/sdk/src/communicate/types.ts +106 -0
  141. package/packages/sdk/src/examples/workflows/fix-dashboard-user-registration.yaml +182 -0
  142. package/packages/sdk/src/workflows/builder.ts +97 -9
  143. package/packages/sdk/src/workflows/cli.ts +16 -1
  144. package/packages/sdk/src/workflows/runner.ts +110 -1
  145. package/packages/sdk/src/workflows/types.ts +14 -0
  146. package/packages/sdk/tsconfig.build.json +1 -7
  147. package/packages/sdk/tsconfig.json +1 -7
  148. package/packages/sdk-py/README.md +67 -25
  149. package/packages/sdk-py/examples/communicate/agno_example.py +8 -0
  150. package/packages/sdk-py/examples/communicate/claude_sdk_example.py +6 -0
  151. package/packages/sdk-py/examples/communicate/crewai_example.py +7 -0
  152. package/packages/sdk-py/examples/communicate/google_adk_example.py +7 -0
  153. package/packages/sdk-py/examples/communicate/openai_agents_example.py +8 -0
  154. package/packages/sdk-py/examples/communicate/swarms_example.py +7 -0
  155. package/packages/sdk-py/pyproject.toml +12 -1
  156. package/packages/sdk-py/src/agent_relay/__init__.py +8 -0
  157. package/packages/sdk-py/src/agent_relay/builder.py +65 -26
  158. package/packages/sdk-py/src/agent_relay/communicate/__init__.py +6 -0
  159. package/packages/sdk-py/src/agent_relay/communicate/a2a_bridge.py +138 -0
  160. package/packages/sdk-py/src/agent_relay/communicate/a2a_server.py +242 -0
  161. package/packages/sdk-py/src/agent_relay/communicate/a2a_transport.py +366 -0
  162. package/packages/sdk-py/src/agent_relay/communicate/a2a_types.py +294 -0
  163. package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +10 -0
  164. package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +74 -0
  165. package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +78 -0
  166. package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +143 -0
  167. package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +69 -0
  168. package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +86 -0
  169. package/packages/sdk-py/src/agent_relay/communicate/adapters/pi.py +175 -0
  170. package/packages/sdk-py/src/agent_relay/communicate/adapters/swarms.py +44 -0
  171. package/packages/sdk-py/src/agent_relay/communicate/core.py +293 -0
  172. package/packages/sdk-py/src/agent_relay/communicate/transport.py +502 -0
  173. package/packages/sdk-py/src/agent_relay/communicate/types.py +89 -0
  174. package/packages/sdk-py/src/agent_relay/types.py +2 -1
  175. package/packages/sdk-py/tests/communicate/__init__.py +0 -0
  176. package/packages/sdk-py/tests/communicate/adapters/__init__.py +0 -0
  177. package/packages/sdk-py/tests/communicate/adapters/e2e_test_agno.py +154 -0
  178. package/packages/sdk-py/tests/communicate/adapters/e2e_test_claude_sdk.py +428 -0
  179. package/packages/sdk-py/tests/communicate/adapters/e2e_test_crewai.py +234 -0
  180. package/packages/sdk-py/tests/communicate/adapters/e2e_test_google_adk.py +182 -0
  181. package/packages/sdk-py/tests/communicate/adapters/e2e_test_langgraph.py +262 -0
  182. package/packages/sdk-py/tests/communicate/adapters/e2e_test_openai_agents.py +88 -0
  183. package/packages/sdk-py/tests/communicate/adapters/e2e_test_pi.py +156 -0
  184. package/packages/sdk-py/tests/communicate/adapters/e2e_test_swarms.py +239 -0
  185. package/packages/sdk-py/tests/communicate/adapters/test_agno.py +140 -0
  186. package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +147 -0
  187. package/packages/sdk-py/tests/communicate/adapters/test_crewai.py +136 -0
  188. package/packages/sdk-py/tests/communicate/adapters/test_google_adk.py +125 -0
  189. package/packages/sdk-py/tests/communicate/adapters/test_openai_agents.py +99 -0
  190. package/packages/sdk-py/tests/communicate/adapters/test_pi.py +270 -0
  191. package/packages/sdk-py/tests/communicate/adapters/test_swarms.py +113 -0
  192. package/packages/sdk-py/tests/communicate/conftest.py +555 -0
  193. package/packages/sdk-py/tests/communicate/integration/__init__.py +1 -0
  194. package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +331 -0
  195. package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +151 -0
  196. package/packages/sdk-py/tests/communicate/test_a2a_bridge.py +363 -0
  197. package/packages/sdk-py/tests/communicate/test_a2a_server.py +346 -0
  198. package/packages/sdk-py/tests/communicate/test_a2a_transport.py +561 -0
  199. package/packages/sdk-py/tests/communicate/test_a2a_types.py +342 -0
  200. package/packages/sdk-py/tests/communicate/test_auto_detect.py +67 -0
  201. package/packages/sdk-py/tests/communicate/test_core.py +331 -0
  202. package/packages/sdk-py/tests/communicate/test_transport.py +373 -0
  203. package/packages/sdk-py/tests/communicate/test_types.py +285 -0
  204. package/packages/sdk-py/tests/test_builder_deterministic.py +118 -0
  205. package/packages/telemetry/package.json +1 -1
  206. package/packages/trajectory/package.json +2 -2
  207. package/packages/user-directory/package.json +2 -2
  208. package/packages/utils/package.json +2 -2
  209. package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts +0 -14
  210. package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts.map +0 -1
  211. package/packages/sdk/dist/__tests__/completion-pipeline.test.js +0 -1476
  212. package/packages/sdk/dist/__tests__/completion-pipeline.test.js.map +0 -1
  213. package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts +0 -2
  214. package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts.map +0 -1
  215. package/packages/sdk/dist/__tests__/contract-fixtures.test.js +0 -152
  216. package/packages/sdk/dist/__tests__/contract-fixtures.test.js.map +0 -1
  217. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts +0 -16
  218. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts.map +0 -1
  219. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +0 -640
  220. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +0 -1
  221. package/packages/sdk/dist/__tests__/facade.test.d.ts +0 -2
  222. package/packages/sdk/dist/__tests__/facade.test.d.ts.map +0 -1
  223. package/packages/sdk/dist/__tests__/facade.test.js +0 -305
  224. package/packages/sdk/dist/__tests__/facade.test.js.map +0 -1
  225. package/packages/sdk/dist/__tests__/integration.test.d.ts +0 -2
  226. package/packages/sdk/dist/__tests__/integration.test.d.ts.map +0 -1
  227. package/packages/sdk/dist/__tests__/integration.test.js +0 -205
  228. package/packages/sdk/dist/__tests__/integration.test.js.map +0 -1
  229. package/packages/sdk/dist/__tests__/pty.test.d.ts +0 -2
  230. package/packages/sdk/dist/__tests__/pty.test.d.ts.map +0 -1
  231. package/packages/sdk/dist/__tests__/pty.test.js +0 -20
  232. package/packages/sdk/dist/__tests__/pty.test.js.map +0 -1
  233. package/packages/sdk/dist/__tests__/quickstart.test.d.ts +0 -2
  234. package/packages/sdk/dist/__tests__/quickstart.test.d.ts.map +0 -1
  235. package/packages/sdk/dist/__tests__/quickstart.test.js +0 -176
  236. package/packages/sdk/dist/__tests__/quickstart.test.js.map +0 -1
  237. package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts +0 -2
  238. package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts.map +0 -1
  239. package/packages/sdk/dist/__tests__/spawn-from-env.test.js +0 -222
  240. package/packages/sdk/dist/__tests__/spawn-from-env.test.js.map +0 -1
  241. package/packages/sdk/dist/__tests__/unit.test.d.ts +0 -2
  242. package/packages/sdk/dist/__tests__/unit.test.d.ts.map +0 -1
  243. package/packages/sdk/dist/__tests__/unit.test.js +0 -357
  244. package/packages/sdk/dist/__tests__/unit.test.js.map +0 -1
  245. package/packages/sdk-py/agent_relay/__init__.py +0 -21
  246. package/packages/sdk-py/agent_relay/models.py +0 -398
@@ -0,0 +1,613 @@
1
+ import assert from 'node:assert/strict';
2
+ import { once } from 'node:events';
3
+ import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
4
+ import { setTimeout as sleep } from 'node:timers/promises';
5
+ import test from 'node:test';
6
+
7
+ import { WebSocketServer, WebSocket } from 'ws';
8
+
9
+ const transportModulePath = '../../communicate/transport.js';
10
+ const typesModulePath = '../../communicate/types.js';
11
+
12
+ async function loadModules() {
13
+ const transport = await import(transportModulePath);
14
+ const types = await import(typesModulePath);
15
+ return { RelayTransport: transport.RelayTransport, ...types };
16
+ }
17
+
18
+ async function readJson(request: IncomingMessage): Promise<any> {
19
+ const chunks: Buffer[] = [];
20
+ for await (const chunk of request) {
21
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
22
+ }
23
+ const raw = Buffer.concat(chunks).toString('utf8');
24
+ return raw.length > 0 ? JSON.parse(raw) : undefined;
25
+ }
26
+
27
+ function sendJson(response: ServerResponse, statusCode: number, payload: unknown): void {
28
+ response.statusCode = statusCode;
29
+ response.setHeader('content-type', 'application/json');
30
+ response.end(JSON.stringify(payload));
31
+ }
32
+
33
+ class MockServer {
34
+ readonly apiKey = 'test-key';
35
+ readonly workspace = 'test-workspace';
36
+
37
+ private readonly requestLog: Array<{ method: string; path: string; json?: any; auth?: string }> = [];
38
+ private server = createServer(this.handleRequest.bind(this));
39
+ private wsServer = new WebSocketServer({ noServer: true });
40
+ private wsClients: WebSocket[] = [];
41
+ private nextAgentId = 1;
42
+ private nextMessageId = 1;
43
+
44
+ /** Track agent tokens from registration: token -> agentId */
45
+ private tokenToAgentId = new Map<string, string>();
46
+
47
+ /** Override to customize HTTP responses */
48
+ responseOverride?: (method: string, path: string, json?: any) => { status: number; body: unknown } | undefined;
49
+
50
+ baseUrl = '';
51
+
52
+ constructor() {
53
+ this.server.on('upgrade', (req, socket, head) => {
54
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
55
+ if (url.pathname !== '/v1/ws') {
56
+ socket.destroy();
57
+ return;
58
+ }
59
+ const token = url.searchParams.get('token');
60
+ if (!token || !this.tokenToAgentId.has(token)) {
61
+ socket.destroy();
62
+ return;
63
+ }
64
+ this.wsServer.handleUpgrade(req, socket as any, head, (ws) => {
65
+ this.wsClients.push(ws);
66
+ this.wsServer.emit('connection', ws, req);
67
+ });
68
+ });
69
+ }
70
+
71
+ async start(): Promise<void> {
72
+ this.server.listen(0, '127.0.0.1');
73
+ await once(this.server, 'listening');
74
+ const address = this.server.address();
75
+ if (!address || typeof address === 'string') {
76
+ throw new Error('Failed to start mock server.');
77
+ }
78
+ this.baseUrl = `http://127.0.0.1:${address.port}`;
79
+ }
80
+
81
+ async stop(): Promise<void> {
82
+ for (const ws of this.wsClients) ws.close();
83
+ this.wsServer.close();
84
+ this.server.close();
85
+ await once(this.server, 'close');
86
+ }
87
+
88
+ makeConfig() {
89
+ return {
90
+ workspace: this.workspace,
91
+ apiKey: this.apiKey,
92
+ baseUrl: this.baseUrl,
93
+ autoCleanup: false,
94
+ };
95
+ }
96
+
97
+ get requests() {
98
+ return this.requestLog;
99
+ }
100
+
101
+ requestsFor(path: string) {
102
+ return this.requestLog.filter((r) => r.path.includes(path));
103
+ }
104
+
105
+ sendToAllWs(payload: unknown): void {
106
+ const data = JSON.stringify(payload);
107
+ for (const ws of this.wsClients) {
108
+ if (ws.readyState === WebSocket.OPEN) ws.send(data);
109
+ }
110
+ }
111
+
112
+ onWsMessage(callback: (data: string, ws: WebSocket) => void): void {
113
+ this.wsServer.on('connection', (ws) => {
114
+ ws.on('message', (raw) => callback(raw.toString(), ws));
115
+ });
116
+ }
117
+
118
+ private resolveAgentFromToken(request: IncomingMessage): string | undefined {
119
+ const auth = request.headers.authorization ?? '';
120
+ if (auth.startsWith('Bearer ')) {
121
+ const token = auth.slice(7);
122
+ return this.tokenToAgentId.get(token);
123
+ }
124
+ return undefined;
125
+ }
126
+
127
+ private async handleRequest(request: IncomingMessage, response: ServerResponse): Promise<void> {
128
+ const url = new URL(request.url ?? '/', this.baseUrl || 'http://127.0.0.1');
129
+ const method = request.method ?? 'GET';
130
+ const path = url.pathname;
131
+ const json = method === 'GET' || method === 'DELETE' ? undefined : await readJson(request);
132
+
133
+ this.requestLog.push({ method, path, json, auth: request.headers.authorization });
134
+
135
+ if (this.responseOverride) {
136
+ const override = this.responseOverride(method, path, json);
137
+ if (override) {
138
+ sendJson(response, override.status, override.body);
139
+ return;
140
+ }
141
+ }
142
+
143
+ // POST /v1/agents — register (workspace key auth)
144
+ if (method === 'POST' && path === '/v1/agents') {
145
+ if (request.headers.authorization !== `Bearer ${this.apiKey}`) {
146
+ sendJson(response, 401, { message: 'Unauthorized' });
147
+ return;
148
+ }
149
+ const agentId = `agent-${this.nextAgentId++}`;
150
+ const token = `token-${agentId}`;
151
+ this.tokenToAgentId.set(token, agentId);
152
+ sendJson(response, 200, { ok: true, data: { id: agentId, name: json?.name, token, status: 'online' } });
153
+ return;
154
+ }
155
+
156
+ // POST /v1/agents/disconnect — unregister (agent token auth)
157
+ if (method === 'POST' && path === '/v1/agents/disconnect') {
158
+ const agentId = this.resolveAgentFromToken(request);
159
+ if (!agentId) {
160
+ sendJson(response, 401, { message: 'Unauthorized' });
161
+ return;
162
+ }
163
+ // Remove token mapping
164
+ const auth = request.headers.authorization ?? '';
165
+ const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
166
+ this.tokenToAgentId.delete(token);
167
+ sendJson(response, 200, { ok: true });
168
+ return;
169
+ }
170
+
171
+ // POST /v1/dm — send DM (agent token auth)
172
+ if (method === 'POST' && path === '/v1/dm') {
173
+ if (!this.resolveAgentFromToken(request)) {
174
+ sendJson(response, 401, { message: 'Unauthorized' });
175
+ return;
176
+ }
177
+ const msgId = `msg-${this.nextMessageId++}`;
178
+ sendJson(response, 201, { ok: true, data: { id: msgId, text: json?.text } });
179
+ return;
180
+ }
181
+
182
+ // POST /v1/channels/{channel}/messages — channel post (agent token auth)
183
+ const channelMatch = path.match(/^\/v1\/channels\/([^/]+)\/messages$/);
184
+ if (method === 'POST' && channelMatch) {
185
+ if (!this.resolveAgentFromToken(request)) {
186
+ sendJson(response, 401, { message: 'Unauthorized' });
187
+ return;
188
+ }
189
+ const msgId = `msg-${this.nextMessageId++}`;
190
+ sendJson(response, 201, { ok: true, data: { id: msgId, channel_name: channelMatch[1], text: json?.text } });
191
+ return;
192
+ }
193
+
194
+ // POST /v1/messages/{id}/replies — reply (agent token auth)
195
+ const replyMatch = path.match(/^\/v1\/messages\/([^/]+)\/replies$/);
196
+ if (method === 'POST' && replyMatch) {
197
+ if (!this.resolveAgentFromToken(request)) {
198
+ sendJson(response, 401, { message: 'Unauthorized' });
199
+ return;
200
+ }
201
+ const msgId = `msg-${this.nextMessageId++}`;
202
+ sendJson(response, 201, { ok: true, data: { id: msgId, text: json?.text } });
203
+ return;
204
+ }
205
+
206
+ // GET /v1/inbox — inbox (agent token auth)
207
+ if (method === 'GET' && path === '/v1/inbox') {
208
+ if (!this.resolveAgentFromToken(request)) {
209
+ sendJson(response, 401, { message: 'Unauthorized' });
210
+ return;
211
+ }
212
+ sendJson(response, 200, { ok: true, data: { unread_channels: [], mentions: [], unread_dms: [], recent_reactions: [] } });
213
+ return;
214
+ }
215
+
216
+ // GET /v1/agents — list agents (workspace key auth)
217
+ if (method === 'GET' && path === '/v1/agents') {
218
+ if (request.headers.authorization !== `Bearer ${this.apiKey}`) {
219
+ sendJson(response, 401, { message: 'Unauthorized' });
220
+ return;
221
+ }
222
+ sendJson(response, 200, { ok: true, data: [{ name: 'TestAgent', id: 'extra-TestAgent', status: 'online' }] });
223
+ return;
224
+ }
225
+
226
+ sendJson(response, 404, { message: 'Not found' });
227
+ }
228
+ }
229
+
230
+ async function withServer(run: (server: MockServer) => Promise<void>): Promise<void> {
231
+ const server = new MockServer();
232
+ await server.start();
233
+ try {
234
+ await run(server);
235
+ } finally {
236
+ await server.stop();
237
+ }
238
+ }
239
+
240
+ // --- HTTP method tests ---
241
+
242
+ test('registerAgent sends POST /v1/agents with auth header', async () => {
243
+ await withServer(async (server) => {
244
+ const { RelayTransport } = await loadModules();
245
+ const transport = new RelayTransport('TestAgent', server.makeConfig());
246
+
247
+ const agentId = await transport.registerAgent();
248
+
249
+ assert.ok(agentId.startsWith('agent-'));
250
+ assert.ok(transport.token);
251
+ const reqs = server.requestsFor('/v1/agents').filter((r) => r.method === 'POST');
252
+ assert.ok(reqs.length > 0);
253
+ assert.equal(reqs[0].auth, `Bearer ${server.apiKey}`);
254
+ assert.deepEqual(reqs[0].json, { name: 'TestAgent', type: 'agent' });
255
+ });
256
+ });
257
+
258
+ test('unregisterAgent sends POST /v1/agents/disconnect', async () => {
259
+ await withServer(async (server) => {
260
+ const { RelayTransport } = await loadModules();
261
+ const transport = new RelayTransport('TestAgent', server.makeConfig());
262
+
263
+ await transport.registerAgent();
264
+ await transport.unregisterAgent();
265
+
266
+ const disconnectReqs = server.requestsFor('/v1/agents/disconnect');
267
+ assert.ok(disconnectReqs.some((r) => r.method === 'POST'));
268
+ assert.equal(transport.agentId, undefined);
269
+ assert.equal(transport.token, undefined);
270
+ });
271
+ });
272
+
273
+ test('sendDm sends POST /v1/dm with correct payload', async () => {
274
+ await withServer(async (server) => {
275
+ const { RelayTransport } = await loadModules();
276
+ const transport = new RelayTransport('Sender', server.makeConfig());
277
+
278
+ const messageId = await transport.sendDm('Recipient', 'hello');
279
+
280
+ assert.ok(messageId.startsWith('msg-'));
281
+ const req = server.requestsFor('/v1/dm')[0];
282
+ assert.deepEqual(req.json, { to: 'Recipient', text: 'hello' });
283
+ // Agent-authenticated endpoint uses the per-agent token, not workspace key
284
+ assert.ok(req.auth?.startsWith('Bearer token-agent-'));
285
+ });
286
+ });
287
+
288
+ test('postMessage sends POST /v1/channels/{channel}/messages', async () => {
289
+ await withServer(async (server) => {
290
+ const { RelayTransport } = await loadModules();
291
+ const transport = new RelayTransport('Poster', server.makeConfig());
292
+
293
+ const messageId = await transport.postMessage('general', 'update');
294
+
295
+ assert.ok(messageId.startsWith('msg-'));
296
+ const req = server.requestsFor('/v1/channels/general/messages')[0];
297
+ assert.deepEqual(req.json, { text: 'update' });
298
+ assert.ok(req.auth?.startsWith('Bearer token-agent-'));
299
+ });
300
+ });
301
+
302
+ test('reply sends POST /v1/messages/{id}/replies', async () => {
303
+ await withServer(async (server) => {
304
+ const { RelayTransport } = await loadModules();
305
+ const transport = new RelayTransport('Replier', server.makeConfig());
306
+
307
+ const messageId = await transport.reply('msg-42', 'response');
308
+
309
+ assert.ok(messageId.startsWith('msg-'));
310
+ const req = server.requestsFor('/v1/messages/msg-42/replies')[0];
311
+ assert.deepEqual(req.json, { text: 'response' });
312
+ assert.ok(req.auth?.startsWith('Bearer token-agent-'));
313
+ });
314
+ });
315
+
316
+ test('listAgents sends GET /v1/agents', async () => {
317
+ await withServer(async (server) => {
318
+ const { RelayTransport } = await loadModules();
319
+ const transport = new RelayTransport('Lister', server.makeConfig());
320
+
321
+ const agents = await transport.listAgents();
322
+
323
+ assert.deepEqual(agents, ['TestAgent']);
324
+ const req = server.requestsFor('/v1/agents')[0];
325
+ assert.equal(req.auth, `Bearer ${server.apiKey}`);
326
+ });
327
+ });
328
+
329
+ test('checkInbox sends GET /v1/inbox', async () => {
330
+ await withServer(async (server) => {
331
+ const { RelayTransport } = await loadModules();
332
+ const transport = new RelayTransport('Checker', server.makeConfig());
333
+
334
+ const messages = await transport.checkInbox();
335
+
336
+ assert.deepEqual(messages, []);
337
+ assert.ok(server.requestsFor('/v1/inbox').length > 0);
338
+ });
339
+ });
340
+
341
+ // --- Error handling tests ---
342
+
343
+ test('401 response throws RelayAuthError', async () => {
344
+ await withServer(async (server) => {
345
+ const { RelayTransport, RelayAuthError } = await loadModules();
346
+ const transport = new RelayTransport('BadAuth', {
347
+ ...server.makeConfig(),
348
+ apiKey: 'wrong-key',
349
+ });
350
+
351
+ await assert.rejects(() => transport.registerAgent(), (err: any) => {
352
+ assert.ok(err instanceof RelayAuthError);
353
+ assert.equal(err.statusCode, 401);
354
+ return true;
355
+ });
356
+ });
357
+ });
358
+
359
+ test('4xx response throws RelayConnectionError', async () => {
360
+ await withServer(async (server) => {
361
+ const { RelayTransport, RelayConnectionError } = await loadModules();
362
+ server.responseOverride = (method, path) => {
363
+ if (path === '/v1/agents') return { status: 403, body: { message: 'Forbidden' } };
364
+ return undefined;
365
+ };
366
+ const transport = new RelayTransport('Forbidden', server.makeConfig());
367
+
368
+ await assert.rejects(() => transport.listAgents(), (err: any) => {
369
+ assert.ok(err instanceof RelayConnectionError);
370
+ assert.equal(err.statusCode, 403);
371
+ return true;
372
+ });
373
+ });
374
+ });
375
+
376
+ test('5xx response retries up to 3 times then throws', async () => {
377
+ await withServer(async (server) => {
378
+ const { RelayTransport, RelayConnectionError } = await loadModules();
379
+ let attempts = 0;
380
+ server.responseOverride = (method, path) => {
381
+ if (path === '/v1/agents') {
382
+ attempts++;
383
+ return { status: 500, body: { message: 'Internal error' } };
384
+ }
385
+ return undefined;
386
+ };
387
+ const transport = new RelayTransport('RetryAgent', server.makeConfig());
388
+
389
+ await assert.rejects(() => transport.listAgents(), (err: any) => {
390
+ assert.ok(err instanceof RelayConnectionError);
391
+ assert.equal(err.statusCode, 500);
392
+ return true;
393
+ });
394
+ assert.equal(attempts, 3);
395
+ });
396
+ });
397
+
398
+ test('missing apiKey throws RelayConfigError', async () => {
399
+ const savedKey = process.env.RELAY_API_KEY;
400
+ delete process.env.RELAY_API_KEY;
401
+ try {
402
+ const { RelayTransport } = await loadModules();
403
+ const transport = new RelayTransport('NoKey', {
404
+ workspace: 'test',
405
+ baseUrl: 'http://localhost:9999',
406
+ autoCleanup: false,
407
+ });
408
+
409
+ await assert.rejects(() => transport.listAgents(), (err: any) => {
410
+ assert.equal(err.name, 'RelayConfigError');
411
+ assert.ok(err.message.includes('RELAY_API_KEY'));
412
+ return true;
413
+ });
414
+ } finally {
415
+ if (savedKey !== undefined) process.env.RELAY_API_KEY = savedKey;
416
+ }
417
+ });
418
+
419
+ test('missing workspace throws RelayConfigError on connect', async () => {
420
+ const savedWorkspace = process.env.RELAY_WORKSPACE;
421
+ delete process.env.RELAY_WORKSPACE;
422
+ try {
423
+ const { RelayTransport } = await loadModules();
424
+ const transport = new RelayTransport('NoWorkspace', {
425
+ apiKey: 'some-key',
426
+ baseUrl: 'http://localhost:9999',
427
+ autoCleanup: false,
428
+ });
429
+
430
+ await assert.rejects(() => transport.connect(), (err: any) => {
431
+ assert.equal(err.name, 'RelayConfigError');
432
+ assert.ok(err.message.includes('RELAY_WORKSPACE'));
433
+ return true;
434
+ });
435
+ } finally {
436
+ if (savedWorkspace !== undefined) process.env.RELAY_WORKSPACE = savedWorkspace;
437
+ }
438
+ });
439
+
440
+ // --- WebSocket tests ---
441
+
442
+ test('WebSocket receives messages and dispatches to callback', async () => {
443
+ await withServer(async (server) => {
444
+ const { RelayTransport } = await loadModules();
445
+ const transport = new RelayTransport('WsAgent', server.makeConfig());
446
+ const received: any[] = [];
447
+
448
+ transport.onWsMessage((message: any) => {
449
+ received.push(message);
450
+ });
451
+
452
+ await transport.connect();
453
+ await sleep(50);
454
+
455
+ server.sendToAllWs({
456
+ type: 'message',
457
+ sender: 'Other',
458
+ text: 'hello ws',
459
+ message_id: 'ws-msg-1',
460
+ });
461
+
462
+ await sleep(100);
463
+ assert.equal(received.length, 1);
464
+ assert.equal(received[0].sender, 'Other');
465
+ assert.equal(received[0].text, 'hello ws');
466
+ assert.equal(received[0].messageId, 'ws-msg-1');
467
+
468
+ await transport.disconnect();
469
+ });
470
+ });
471
+
472
+ test('WebSocket responds to ping with pong', async () => {
473
+ await withServer(async (server) => {
474
+ const { RelayTransport } = await loadModules();
475
+ const transport = new RelayTransport('PingAgent', server.makeConfig());
476
+ const pongs: string[] = [];
477
+
478
+ server.onWsMessage((data) => {
479
+ const parsed = JSON.parse(data);
480
+ if (parsed.type === 'pong') pongs.push(parsed.type);
481
+ });
482
+
483
+ await transport.connect();
484
+ await sleep(50);
485
+
486
+ server.sendToAllWs({ type: 'ping' });
487
+ await sleep(100);
488
+
489
+ assert.equal(pongs.length, 1);
490
+ await transport.disconnect();
491
+ });
492
+ });
493
+
494
+ test('WebSocket ignores non-message payloads', async () => {
495
+ await withServer(async (server) => {
496
+ const { RelayTransport } = await loadModules();
497
+ const transport = new RelayTransport('FilterAgent', server.makeConfig());
498
+ const received: any[] = [];
499
+
500
+ transport.onWsMessage((message: any) => {
501
+ received.push(message);
502
+ });
503
+
504
+ await transport.connect();
505
+ await sleep(50);
506
+
507
+ server.sendToAllWs({ type: 'status', data: 'online' });
508
+ server.sendToAllWs({ type: 'message', sender: 'Real', text: 'real message', message_id: 'r1' });
509
+ await sleep(100);
510
+
511
+ assert.equal(received.length, 1);
512
+ assert.equal(received[0].text, 'real message');
513
+
514
+ await transport.disconnect();
515
+ });
516
+ });
517
+
518
+ test('disconnect closes WebSocket and unregisters agent', async () => {
519
+ await withServer(async (server) => {
520
+ const { RelayTransport } = await loadModules();
521
+ const transport = new RelayTransport('DisconnectAgent', server.makeConfig());
522
+
523
+ await transport.connect();
524
+ assert.ok(transport.agentId);
525
+ await sleep(50);
526
+
527
+ await transport.disconnect();
528
+
529
+ assert.ok(server.requestsFor('/v1/agents/disconnect').some((r) => r.method === 'POST'));
530
+ });
531
+ });
532
+
533
+ // --- URL conversion test ---
534
+
535
+ test('wsBaseUrl converts https to wss and http to ws', async () => {
536
+ const { RelayTransport } = await loadModules();
537
+
538
+ const httpsTransport = new RelayTransport('Agent', {
539
+ workspace: 'test',
540
+ apiKey: 'key',
541
+ baseUrl: 'https://api.example.com',
542
+ autoCleanup: false,
543
+ });
544
+
545
+ const httpTransport = new RelayTransport('Agent', {
546
+ workspace: 'test',
547
+ apiKey: 'key',
548
+ baseUrl: 'http://localhost:8080',
549
+ autoCleanup: false,
550
+ });
551
+
552
+ // Access private method via bracket notation for testing
553
+ assert.equal((httpsTransport as any)['wsBaseUrl'](), 'wss://api.example.com');
554
+ assert.equal((httpTransport as any)['wsBaseUrl'](), 'ws://localhost:8080');
555
+ });
556
+
557
+ // --- Auth header on all requests ---
558
+
559
+ test('all HTTP methods include Authorization header', async () => {
560
+ await withServer(async (server) => {
561
+ const { RelayTransport } = await loadModules();
562
+ const transport = new RelayTransport('AuthCheck', server.makeConfig());
563
+
564
+ await transport.registerAgent();
565
+ await transport.sendDm('other', 'hi');
566
+ await transport.postMessage('general', 'msg');
567
+ await transport.reply('msg-1', 'reply');
568
+ await transport.checkInbox();
569
+ await transport.listAgents();
570
+ await transport.unregisterAgent();
571
+
572
+ for (const req of server.requests) {
573
+ assert.ok(req.auth?.startsWith('Bearer '), `Missing auth on ${req.method} ${req.path}`);
574
+ }
575
+ });
576
+ });
577
+
578
+ // --- messageFromPayload field mapping ---
579
+
580
+ test('messageFromPayload maps thread_id and message_id to camelCase', async () => {
581
+ await withServer(async (server) => {
582
+ const { RelayTransport } = await loadModules();
583
+ const transport = new RelayTransport('MapAgent', server.makeConfig());
584
+ const received: any[] = [];
585
+
586
+ transport.onWsMessage((message: any) => {
587
+ received.push(message);
588
+ });
589
+
590
+ await transport.connect();
591
+ await sleep(50);
592
+
593
+ server.sendToAllWs({
594
+ type: 'message',
595
+ sender: 'Lead',
596
+ text: 'threaded',
597
+ channel: 'general',
598
+ thread_id: 'thread-42',
599
+ message_id: 'msg-99',
600
+ timestamp: 1700000000,
601
+ });
602
+
603
+ await sleep(100);
604
+
605
+ assert.equal(received.length, 1);
606
+ assert.equal(received[0].threadId, 'thread-42');
607
+ assert.equal(received[0].messageId, 'msg-99');
608
+ assert.equal(received[0].channel, 'general');
609
+ assert.equal(received[0].timestamp, 1700000000);
610
+
611
+ await transport.disconnect();
612
+ });
613
+ });