agent-relay 3.2.15 → 3.2.16

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 (133) 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 +3853 -17163
  6. package/package.json +8 -8
  7. package/packages/acp-bridge/package.json +2 -2
  8. package/packages/config/package.json +1 -1
  9. package/packages/hooks/package.json +4 -4
  10. package/packages/memory/package.json +2 -2
  11. package/packages/openclaw/package.json +2 -2
  12. package/packages/policy/package.json +2 -2
  13. package/packages/sdk/dist/broker-path.d.ts +19 -0
  14. package/packages/sdk/dist/broker-path.d.ts.map +1 -0
  15. package/packages/sdk/dist/broker-path.js +71 -0
  16. package/packages/sdk/dist/broker-path.js.map +1 -0
  17. package/packages/sdk/dist/cli-registry.d.ts.map +1 -1
  18. package/packages/sdk/dist/cli-registry.js +4 -0
  19. package/packages/sdk/dist/cli-registry.js.map +1 -1
  20. package/packages/sdk/dist/client.d.ts +6 -1
  21. package/packages/sdk/dist/client.d.ts.map +1 -1
  22. package/packages/sdk/dist/client.js +18 -0
  23. package/packages/sdk/dist/client.js.map +1 -1
  24. package/packages/sdk/dist/communicate/adapters/index.d.ts +0 -5
  25. package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -1
  26. package/packages/sdk/dist/communicate/adapters/index.js +0 -5
  27. package/packages/sdk/dist/communicate/adapters/index.js.map +1 -1
  28. package/packages/sdk/dist/communicate/adapters/pi.d.ts +0 -1
  29. package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -1
  30. package/packages/sdk/dist/communicate/adapters/pi.js +0 -4
  31. package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -1
  32. package/packages/sdk/dist/communicate/core.d.ts.map +1 -1
  33. package/packages/sdk/dist/communicate/core.js +2 -3
  34. package/packages/sdk/dist/communicate/core.js.map +1 -1
  35. package/packages/sdk/dist/communicate/index.d.ts +17 -1
  36. package/packages/sdk/dist/communicate/index.d.ts.map +1 -1
  37. package/packages/sdk/dist/communicate/index.js +40 -1
  38. package/packages/sdk/dist/communicate/index.js.map +1 -1
  39. package/packages/sdk/dist/communicate/transport.d.ts +0 -1
  40. package/packages/sdk/dist/communicate/transport.d.ts.map +1 -1
  41. package/packages/sdk/dist/communicate/transport.js +42 -134
  42. package/packages/sdk/dist/communicate/transport.js.map +1 -1
  43. package/packages/sdk/dist/http.d.ts +38 -0
  44. package/packages/sdk/dist/http.d.ts.map +1 -0
  45. package/packages/sdk/dist/http.js +60 -0
  46. package/packages/sdk/dist/http.js.map +1 -0
  47. package/packages/sdk/dist/protocol.d.ts +25 -0
  48. package/packages/sdk/dist/protocol.d.ts.map +1 -1
  49. package/packages/sdk/dist/relay.d.ts +26 -3
  50. package/packages/sdk/dist/relay.d.ts.map +1 -1
  51. package/packages/sdk/dist/relay.js +62 -4
  52. package/packages/sdk/dist/relay.js.map +1 -1
  53. package/packages/sdk/dist/workflows/api-executor.d.ts +16 -0
  54. package/packages/sdk/dist/workflows/api-executor.d.ts.map +1 -0
  55. package/packages/sdk/dist/workflows/api-executor.js +94 -0
  56. package/packages/sdk/dist/workflows/api-executor.js.map +1 -0
  57. package/packages/sdk/dist/workflows/builder.d.ts +14 -0
  58. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  59. package/packages/sdk/dist/workflows/builder.js +26 -0
  60. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  61. package/packages/sdk/dist/workflows/cloud-runner.d.ts +15 -0
  62. package/packages/sdk/dist/workflows/cloud-runner.d.ts.map +1 -0
  63. package/packages/sdk/dist/workflows/cloud-runner.js +41 -0
  64. package/packages/sdk/dist/workflows/cloud-runner.js.map +1 -0
  65. package/packages/sdk/dist/workflows/index.d.ts +2 -0
  66. package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
  67. package/packages/sdk/dist/workflows/index.js +1 -0
  68. package/packages/sdk/dist/workflows/index.js.map +1 -1
  69. package/packages/sdk/dist/workflows/run.d.ts.map +1 -1
  70. package/packages/sdk/dist/workflows/run.js +4 -0
  71. package/packages/sdk/dist/workflows/run.js.map +1 -1
  72. package/packages/sdk/dist/workflows/runner.d.ts +14 -0
  73. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  74. package/packages/sdk/dist/workflows/runner.js +154 -10
  75. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  76. package/packages/sdk/dist/workflows/types.d.ts +13 -3
  77. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  78. package/packages/sdk/dist/workflows/types.js +5 -1
  79. package/packages/sdk/dist/workflows/types.js.map +1 -1
  80. package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
  81. package/packages/sdk/dist/workflows/validator.js +12 -0
  82. package/packages/sdk/dist/workflows/validator.js.map +1 -1
  83. package/packages/sdk/package.json +13 -3
  84. package/packages/sdk/src/__tests__/channel-management.test.ts +131 -0
  85. package/packages/sdk/src/__tests__/communicate/core.test.ts +36 -88
  86. package/packages/sdk/src/__tests__/communicate/transport.test.ts +41 -80
  87. package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +120 -0
  88. package/packages/sdk/src/__tests__/relay-channel-ops.test.ts +121 -0
  89. package/packages/sdk/src/broker-path.ts +74 -0
  90. package/packages/sdk/src/cli-registry.ts +4 -0
  91. package/packages/sdk/src/client.ts +28 -0
  92. package/packages/sdk/src/communicate/adapters/index.ts +0 -5
  93. package/packages/sdk/src/communicate/adapters/pi.ts +1 -5
  94. package/packages/sdk/src/communicate/core.ts +6 -10
  95. package/packages/sdk/src/communicate/index.ts +57 -1
  96. package/packages/sdk/src/communicate/transport.ts +46 -177
  97. package/packages/sdk/src/http.ts +96 -0
  98. package/packages/sdk/src/protocol.ts +24 -0
  99. package/packages/sdk/src/relay.ts +93 -8
  100. package/packages/sdk/src/workflows/README.md +5 -2
  101. package/packages/sdk/src/workflows/api-executor.ts +108 -0
  102. package/packages/sdk/src/workflows/builder.ts +40 -0
  103. package/packages/sdk/src/workflows/cloud-runner.ts +56 -0
  104. package/packages/sdk/src/workflows/index.ts +2 -0
  105. package/packages/sdk/src/workflows/run.ts +5 -0
  106. package/packages/sdk/src/workflows/runner.ts +181 -11
  107. package/packages/sdk/src/workflows/types.ts +19 -4
  108. package/packages/sdk/src/workflows/validator.ts +15 -0
  109. package/packages/sdk-py/README.md +7 -0
  110. package/packages/sdk-py/pyproject.toml +1 -1
  111. package/packages/sdk-py/src/agent_relay/__init__.py +2 -0
  112. package/packages/sdk-py/src/agent_relay/client.py +4 -0
  113. package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +0 -9
  114. package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +5 -9
  115. package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +5 -7
  116. package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +3 -13
  117. package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +5 -2
  118. package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +5 -9
  119. package/packages/sdk-py/src/agent_relay/communicate/core.py +7 -24
  120. package/packages/sdk-py/src/agent_relay/communicate/transport.py +35 -212
  121. package/packages/sdk-py/src/agent_relay/communicate/types.py +1 -1
  122. package/packages/sdk-py/src/agent_relay/protocol.py +1 -0
  123. package/packages/sdk-py/src/agent_relay/relay.py +9 -1
  124. package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +6 -6
  125. package/packages/sdk-py/tests/communicate/conftest.py +86 -233
  126. package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +2 -2
  127. package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +14 -24
  128. package/packages/sdk-py/tests/communicate/test_transport.py +65 -54
  129. package/packages/sdk-py/tests/test_send_message_mode.py +91 -0
  130. package/packages/telemetry/package.json +1 -1
  131. package/packages/trajectory/package.json +2 -2
  132. package/packages/user-directory/package.json +2 -2
  133. package/packages/utils/package.json +2 -2
@@ -41,9 +41,6 @@ class MockServer {
41
41
  private nextAgentId = 1;
42
42
  private nextMessageId = 1;
43
43
 
44
- /** Track agent tokens from registration: token -> agentId */
45
- private tokenToAgentId = new Map<string, string>();
46
-
47
44
  /** Override to customize HTTP responses */
48
45
  responseOverride?: (method: string, path: string, json?: any) => { status: number; body: unknown } | undefined;
49
46
 
@@ -52,12 +49,7 @@ class MockServer {
52
49
  constructor() {
53
50
  this.server.on('upgrade', (req, socket, head) => {
54
51
  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)) {
52
+ if (!url.pathname.startsWith('/v1/ws/')) {
61
53
  socket.destroy();
62
54
  return;
63
55
  }
@@ -115,15 +107,6 @@ class MockServer {
115
107
  });
116
108
  }
117
109
 
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
110
  private async handleRequest(request: IncomingMessage, response: ServerResponse): Promise<void> {
128
111
  const url = new URL(request.url ?? '/', this.baseUrl || 'http://127.0.0.1');
129
112
  const method = request.method ?? 'GET';
@@ -140,86 +123,68 @@ class MockServer {
140
123
  }
141
124
  }
142
125
 
143
- // POST /v1/agentsregister (workspace key auth)
144
- if (method === 'POST' && path === '/v1/agents') {
126
+ if (method === 'POST' && path === '/v1/agents/register') {
145
127
  if (request.headers.authorization !== `Bearer ${this.apiKey}`) {
146
128
  sendJson(response, 401, { message: 'Unauthorized' });
147
129
  return;
148
130
  }
149
131
  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' } });
132
+ sendJson(response, 200, { agent_id: agentId, token: `token-${agentId}` });
153
133
  return;
154
134
  }
155
135
 
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) {
136
+ if (method === 'DELETE' && path.startsWith('/v1/agents/')) {
137
+ if (request.headers.authorization !== `Bearer ${this.apiKey}`) {
160
138
  sendJson(response, 401, { message: 'Unauthorized' });
161
139
  return;
162
140
  }
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 });
141
+ response.statusCode = 204;
142
+ response.end();
168
143
  return;
169
144
  }
170
145
 
171
- // POST /v1/dm — send DM (agent token auth)
172
- if (method === 'POST' && path === '/v1/dm') {
173
- if (!this.resolveAgentFromToken(request)) {
146
+ if (method === 'POST' && path === '/v1/messages/dm') {
147
+ if (request.headers.authorization !== `Bearer ${this.apiKey}`) {
174
148
  sendJson(response, 401, { message: 'Unauthorized' });
175
149
  return;
176
150
  }
177
- const msgId = `msg-${this.nextMessageId++}`;
178
- sendJson(response, 201, { ok: true, data: { id: msgId, text: json?.text } });
151
+ sendJson(response, 200, { message_id: `msg-${this.nextMessageId++}` });
179
152
  return;
180
153
  }
181
154
 
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)) {
155
+ if (method === 'POST' && path === '/v1/messages/channel') {
156
+ if (request.headers.authorization !== `Bearer ${this.apiKey}`) {
186
157
  sendJson(response, 401, { message: 'Unauthorized' });
187
158
  return;
188
159
  }
189
- const msgId = `msg-${this.nextMessageId++}`;
190
- sendJson(response, 201, { ok: true, data: { id: msgId, channel_name: channelMatch[1], text: json?.text } });
160
+ sendJson(response, 200, { message_id: `msg-${this.nextMessageId++}` });
191
161
  return;
192
162
  }
193
163
 
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)) {
164
+ if (method === 'POST' && path === '/v1/messages/reply') {
165
+ if (request.headers.authorization !== `Bearer ${this.apiKey}`) {
198
166
  sendJson(response, 401, { message: 'Unauthorized' });
199
167
  return;
200
168
  }
201
- const msgId = `msg-${this.nextMessageId++}`;
202
- sendJson(response, 201, { ok: true, data: { id: msgId, text: json?.text } });
169
+ sendJson(response, 200, { message_id: `msg-${this.nextMessageId++}` });
203
170
  return;
204
171
  }
205
172
 
206
- // GET /v1/inbox — inbox (agent token auth)
207
- if (method === 'GET' && path === '/v1/inbox') {
208
- if (!this.resolveAgentFromToken(request)) {
173
+ if (method === 'GET' && path.startsWith('/v1/inbox/')) {
174
+ if (request.headers.authorization !== `Bearer ${this.apiKey}`) {
209
175
  sendJson(response, 401, { message: 'Unauthorized' });
210
176
  return;
211
177
  }
212
- sendJson(response, 200, { ok: true, data: { unread_channels: [], mentions: [], unread_dms: [], recent_reactions: [] } });
178
+ sendJson(response, 200, { messages: [] });
213
179
  return;
214
180
  }
215
181
 
216
- // GET /v1/agents — list agents (workspace key auth)
217
182
  if (method === 'GET' && path === '/v1/agents') {
218
183
  if (request.headers.authorization !== `Bearer ${this.apiKey}`) {
219
184
  sendJson(response, 401, { message: 'Unauthorized' });
220
185
  return;
221
186
  }
222
- sendJson(response, 200, { ok: true, data: [{ name: 'TestAgent', id: 'extra-TestAgent', status: 'online' }] });
187
+ sendJson(response, 200, { agents: ['TestAgent'] });
223
188
  return;
224
189
  }
225
190
 
@@ -239,7 +204,7 @@ async function withServer(run: (server: MockServer) => Promise<void>): Promise<v
239
204
 
240
205
  // --- HTTP method tests ---
241
206
 
242
- test('registerAgent sends POST /v1/agents with auth header', async () => {
207
+ test('registerAgent sends POST /v1/agents/register with auth header', async () => {
243
208
  await withServer(async (server) => {
244
209
  const { RelayTransport } = await loadModules();
245
210
  const transport = new RelayTransport('TestAgent', server.makeConfig());
@@ -248,14 +213,13 @@ test('registerAgent sends POST /v1/agents with auth header', async () => {
248
213
 
249
214
  assert.ok(agentId.startsWith('agent-'));
250
215
  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' });
216
+ const req = server.requestsFor('/v1/agents/register')[0];
217
+ assert.equal(req.auth, `Bearer ${server.apiKey}`);
218
+ assert.deepEqual(req.json, { name: 'TestAgent', workspace: server.workspace });
255
219
  });
256
220
  });
257
221
 
258
- test('unregisterAgent sends POST /v1/agents/disconnect', async () => {
222
+ test('unregisterAgent sends DELETE /v1/agents/{id}', async () => {
259
223
  await withServer(async (server) => {
260
224
  const { RelayTransport } = await loadModules();
261
225
  const transport = new RelayTransport('TestAgent', server.makeConfig());
@@ -263,14 +227,14 @@ test('unregisterAgent sends POST /v1/agents/disconnect', async () => {
263
227
  await transport.registerAgent();
264
228
  await transport.unregisterAgent();
265
229
 
266
- const disconnectReqs = server.requestsFor('/v1/agents/disconnect');
267
- assert.ok(disconnectReqs.some((r) => r.method === 'POST'));
230
+ const deleteReqs = server.requestsFor('/v1/agents/agent-');
231
+ assert.ok(deleteReqs.some((r) => r.method === 'DELETE'));
268
232
  assert.equal(transport.agentId, undefined);
269
233
  assert.equal(transport.token, undefined);
270
234
  });
271
235
  });
272
236
 
273
- test('sendDm sends POST /v1/dm with correct payload', async () => {
237
+ test('sendDm sends POST /v1/messages/dm with correct payload', async () => {
274
238
  await withServer(async (server) => {
275
239
  const { RelayTransport } = await loadModules();
276
240
  const transport = new RelayTransport('Sender', server.makeConfig());
@@ -278,14 +242,13 @@ test('sendDm sends POST /v1/dm with correct payload', async () => {
278
242
  const messageId = await transport.sendDm('Recipient', 'hello');
279
243
 
280
244
  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-'));
245
+ const req = server.requestsFor('/v1/messages/dm')[0];
246
+ assert.deepEqual(req.json, { to: 'Recipient', text: 'hello', from: 'Sender' });
247
+ assert.equal(req.auth, `Bearer ${server.apiKey}`);
285
248
  });
286
249
  });
287
250
 
288
- test('postMessage sends POST /v1/channels/{channel}/messages', async () => {
251
+ test('postMessage sends POST /v1/messages/channel', async () => {
289
252
  await withServer(async (server) => {
290
253
  const { RelayTransport } = await loadModules();
291
254
  const transport = new RelayTransport('Poster', server.makeConfig());
@@ -293,13 +256,12 @@ test('postMessage sends POST /v1/channels/{channel}/messages', async () => {
293
256
  const messageId = await transport.postMessage('general', 'update');
294
257
 
295
258
  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-'));
259
+ const req = server.requestsFor('/v1/messages/channel')[0];
260
+ assert.deepEqual(req.json, { channel: 'general', text: 'update', from: 'Poster' });
299
261
  });
300
262
  });
301
263
 
302
- test('reply sends POST /v1/messages/{id}/replies', async () => {
264
+ test('reply sends POST /v1/messages/reply', async () => {
303
265
  await withServer(async (server) => {
304
266
  const { RelayTransport } = await loadModules();
305
267
  const transport = new RelayTransport('Replier', server.makeConfig());
@@ -307,9 +269,8 @@ test('reply sends POST /v1/messages/{id}/replies', async () => {
307
269
  const messageId = await transport.reply('msg-42', 'response');
308
270
 
309
271
  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-'));
272
+ const req = server.requestsFor('/v1/messages/reply')[0];
273
+ assert.deepEqual(req.json, { message_id: 'msg-42', text: 'response', from: 'Replier' });
313
274
  });
314
275
  });
315
276
 
@@ -326,7 +287,7 @@ test('listAgents sends GET /v1/agents', async () => {
326
287
  });
327
288
  });
328
289
 
329
- test('checkInbox sends GET /v1/inbox', async () => {
290
+ test('checkInbox sends GET /v1/inbox/{agentId}', async () => {
330
291
  await withServer(async (server) => {
331
292
  const { RelayTransport } = await loadModules();
332
293
  const transport = new RelayTransport('Checker', server.makeConfig());
@@ -334,7 +295,7 @@ test('checkInbox sends GET /v1/inbox', async () => {
334
295
  const messages = await transport.checkInbox();
335
296
 
336
297
  assert.deepEqual(messages, []);
337
- assert.ok(server.requestsFor('/v1/inbox').length > 0);
298
+ assert.ok(server.requestsFor('/v1/inbox/').length > 0);
338
299
  });
339
300
  });
340
301
 
@@ -526,7 +487,7 @@ test('disconnect closes WebSocket and unregisters agent', async () => {
526
487
 
527
488
  await transport.disconnect();
528
489
 
529
- assert.ok(server.requestsFor('/v1/agents/disconnect').some((r) => r.method === 'POST'));
490
+ assert.ok(server.requestsFor('/v1/agents/agent-').some((r) => r.method === 'DELETE'));
530
491
  });
531
492
  });
532
493
 
@@ -570,7 +531,7 @@ test('all HTTP methods include Authorization header', async () => {
570
531
  await transport.unregisterAgent();
571
532
 
572
533
  for (const req of server.requests) {
573
- assert.ok(req.auth?.startsWith('Bearer '), `Missing auth on ${req.method} ${req.path}`);
534
+ assert.equal(req.auth, `Bearer ${server.apiKey}`, `Missing auth on ${req.method} ${req.path}`);
574
535
  }
575
536
  });
576
537
  });
@@ -180,6 +180,29 @@ describe('AgentRelayClient orchestration payloads', () => {
180
180
  );
181
181
  });
182
182
 
183
+ it('sendMessage forwards mode for injection behavior', async () => {
184
+ const client = new AgentRelayClient();
185
+ vi.spyOn(client, 'start').mockResolvedValue(undefined);
186
+ const requestOk = vi
187
+ .spyOn(client as any, 'requestOk')
188
+ .mockResolvedValue({ event_id: 'evt_mode', targets: ['worker'] });
189
+
190
+ await client.sendMessage({
191
+ to: 'worker',
192
+ text: 'urgent update',
193
+ mode: 'steer',
194
+ });
195
+
196
+ expect(requestOk).toHaveBeenCalledWith(
197
+ 'send_message',
198
+ expect.objectContaining({
199
+ to: 'worker',
200
+ text: 'urgent update',
201
+ mode: 'steer',
202
+ })
203
+ );
204
+ });
205
+
183
206
  it('release forwards optional reason', async () => {
184
207
  const client = new AgentRelayClient();
185
208
  vi.spyOn(client, 'start').mockResolvedValue(undefined);
@@ -1107,4 +1130,101 @@ describe('Agent.onOutput', () => {
1107
1130
  await relay.shutdown();
1108
1131
  }
1109
1132
  });
1133
+
1134
+ it('onOutput with { stream: "stdout" } only receives stdout events', async () => {
1135
+ const { client, emit } = createMockFacadeClient();
1136
+ vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client);
1137
+
1138
+ const relay = new AgentRelay();
1139
+ try {
1140
+ const agent = await relay.spawnPty({
1141
+ name: 'stream-filter-agent',
1142
+ cli: 'claude',
1143
+ channels: ['general'],
1144
+ });
1145
+
1146
+ const chunks: string[] = [];
1147
+ agent.onOutput((chunk: string) => chunks.push(chunk), { stream: 'stdout' });
1148
+
1149
+ emit({ kind: 'worker_stream', name: 'stream-filter-agent', stream: 'stdout', chunk: 'out1' });
1150
+ emit({ kind: 'worker_stream', name: 'stream-filter-agent', stream: 'stderr', chunk: 'err1' });
1151
+ emit({ kind: 'worker_stream', name: 'stream-filter-agent', stream: 'stdout', chunk: 'out2' });
1152
+
1153
+ expect(chunks).toEqual(['out1', 'out2']);
1154
+ } finally {
1155
+ await relay.shutdown();
1156
+ }
1157
+ });
1158
+
1159
+ it('onOutput without filter receives all streams', async () => {
1160
+ const { client, emit } = createMockFacadeClient();
1161
+ vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client);
1162
+
1163
+ const relay = new AgentRelay();
1164
+ try {
1165
+ const agent = await relay.spawnPty({
1166
+ name: 'all-streams-agent',
1167
+ cli: 'claude',
1168
+ channels: ['general'],
1169
+ });
1170
+
1171
+ const chunks: string[] = [];
1172
+ agent.onOutput((chunk: string) => chunks.push(chunk));
1173
+
1174
+ emit({ kind: 'worker_stream', name: 'all-streams-agent', stream: 'stdout', chunk: 'out' });
1175
+ emit({ kind: 'worker_stream', name: 'all-streams-agent', stream: 'stderr', chunk: 'err' });
1176
+
1177
+ expect(chunks).toEqual(['out', 'err']);
1178
+ } finally {
1179
+ await relay.shutdown();
1180
+ }
1181
+ });
1182
+
1183
+ it('onOutput with { stream: "stderr" } ignores stdout events', async () => {
1184
+ const { client, emit } = createMockFacadeClient();
1185
+ vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client);
1186
+
1187
+ const relay = new AgentRelay();
1188
+ try {
1189
+ const agent = await relay.spawnPty({
1190
+ name: 'stderr-filter-agent',
1191
+ cli: 'claude',
1192
+ channels: ['general'],
1193
+ });
1194
+
1195
+ const chunks: string[] = [];
1196
+ agent.onOutput((chunk: string) => chunks.push(chunk), { stream: 'stderr' });
1197
+
1198
+ emit({ kind: 'worker_stream', name: 'stderr-filter-agent', stream: 'stdout', chunk: 'ignored' });
1199
+ emit({ kind: 'worker_stream', name: 'stderr-filter-agent', stream: 'stderr', chunk: 'captured' });
1200
+
1201
+ expect(chunks).toEqual(['captured']);
1202
+ } finally {
1203
+ await relay.shutdown();
1204
+ }
1205
+ });
1206
+
1207
+ it('onOutput with explicit mode: "structured" receives { stream, chunk } objects', async () => {
1208
+ const { client, emit } = createMockFacadeClient();
1209
+ vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client);
1210
+
1211
+ const relay = new AgentRelay();
1212
+ try {
1213
+ const agent = await relay.spawnPty({
1214
+ name: 'explicit-mode-agent',
1215
+ cli: 'claude',
1216
+ channels: ['general'],
1217
+ });
1218
+
1219
+ const payloads: Array<{ stream: string; chunk: string }> = [];
1220
+ // Use a plain (chunk) => ... signature but force structured mode via options
1221
+ agent.onOutput(((data: { stream: string; chunk: string }) => payloads.push(data)) as any, { mode: 'structured' });
1222
+
1223
+ emit({ kind: 'worker_stream', name: 'explicit-mode-agent', stream: 'stdout', chunk: 'hello' });
1224
+
1225
+ expect(payloads).toEqual([{ stream: 'stdout', chunk: 'hello' }]);
1226
+ } finally {
1227
+ await relay.shutdown();
1228
+ }
1229
+ });
1110
1230
  });
@@ -0,0 +1,121 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import type { BrokerEvent } from '../protocol.js';
4
+ import { AgentRelay } from '../relay.js';
5
+
6
+ function createMockFacadeClient() {
7
+ const listeners = new Set<(event: BrokerEvent) => void>();
8
+
9
+ const mock = {
10
+ subscribeChannels: vi.fn(async () => undefined),
11
+ unsubscribeChannels: vi.fn(async () => undefined),
12
+ muteChannel: vi.fn(async () => undefined),
13
+ unmuteChannel: vi.fn(async () => undefined),
14
+ onEvent: vi.fn((listener: (event: BrokerEvent) => void) => {
15
+ listeners.add(listener);
16
+ return () => {
17
+ listeners.delete(listener);
18
+ };
19
+ }),
20
+ };
21
+
22
+ const emit = (event: BrokerEvent) => {
23
+ for (const listener of Array.from(listeners)) {
24
+ listener(event);
25
+ }
26
+ };
27
+
28
+ return {
29
+ client: mock,
30
+ emit,
31
+ };
32
+ }
33
+
34
+ function setupRelay() {
35
+ const relay = new AgentRelay();
36
+ const mockClient = createMockFacadeClient();
37
+ (relay as any).client = mockClient.client;
38
+ (relay as any).wireEvents(mockClient.client);
39
+ return { relay, ...mockClient };
40
+ }
41
+
42
+ afterEach(() => {
43
+ vi.restoreAllMocks();
44
+ });
45
+
46
+ describe('AgentRelay channel operations', () => {
47
+ it('relay.subscribe delegates to client', async () => {
48
+ const { relay, client } = setupRelay();
49
+
50
+ await relay.subscribe({ agent: 'worker-1', channels: ['ch-a', 'ch-b'] });
51
+
52
+ expect(client.subscribeChannels).toHaveBeenCalledWith('worker-1', ['ch-a', 'ch-b']);
53
+ });
54
+
55
+ it('relay.mute delegates to client', async () => {
56
+ const { relay, client } = setupRelay();
57
+
58
+ await relay.mute({ agent: 'worker-1', channel: 'ch-a' });
59
+
60
+ expect(client.muteChannel).toHaveBeenCalledWith('worker-1', 'ch-a');
61
+ });
62
+
63
+ it('Agent.subscribe updates the local channel list on success', async () => {
64
+ const { relay, client } = setupRelay();
65
+ const agent = (relay as any).ensureAgentHandle('worker-1', 'pty', ['ch-a']);
66
+
67
+ await agent.subscribe(['ch-b']);
68
+
69
+ expect(client.subscribeChannels).toHaveBeenCalledWith('worker-1', ['ch-b']);
70
+ expect(agent.channels).toEqual(['ch-a', 'ch-b']);
71
+ });
72
+
73
+ it('Agent.mute adds the channel to mutedChannels', async () => {
74
+ const { relay, client } = setupRelay();
75
+ const agent = (relay as any).ensureAgentHandle('worker-1', 'pty', ['ch-a']);
76
+
77
+ await agent.mute('ch-a');
78
+
79
+ expect(client.muteChannel).toHaveBeenCalledWith('worker-1', 'ch-a');
80
+ expect(agent.mutedChannels).toEqual(['ch-a']);
81
+ });
82
+
83
+ it('Agent.unmute removes the channel from mutedChannels', async () => {
84
+ const { relay, client } = setupRelay();
85
+ const agent = (relay as any).ensureAgentHandle('worker-1', 'pty', ['ch-a']);
86
+
87
+ await agent.mute('ch-a');
88
+ await agent.unmute('ch-a');
89
+
90
+ expect(client.unmuteChannel).toHaveBeenCalledWith('worker-1', 'ch-a');
91
+ expect(agent.mutedChannels).toEqual([]);
92
+ });
93
+
94
+ it('onChannelSubscribed fires on channel_subscribed events', () => {
95
+ const { relay, emit } = setupRelay();
96
+ const callback = vi.fn();
97
+ relay.onChannelSubscribed = callback;
98
+
99
+ emit({
100
+ kind: 'channel_subscribed',
101
+ name: 'worker-1',
102
+ channels: ['ch-a'],
103
+ });
104
+
105
+ expect(callback).toHaveBeenCalledWith('worker-1', ['ch-a']);
106
+ });
107
+
108
+ it('onChannelMuted fires on channel_muted events', () => {
109
+ const { relay, emit } = setupRelay();
110
+ const callback = vi.fn();
111
+ relay.onChannelMuted = callback;
112
+
113
+ emit({
114
+ kind: 'channel_muted',
115
+ name: 'worker-1',
116
+ channel: 'ch-a',
117
+ });
118
+
119
+ expect(callback).toHaveBeenCalledWith('worker-1', 'ch-a');
120
+ });
121
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Resolves the agent-relay-broker binary path at runtime.
3
+ *
4
+ * Usage:
5
+ * import { getBrokerBinaryPath } from '@agent-relay/sdk/broker-path';
6
+ * const binPath = getBrokerBinaryPath();
7
+ */
8
+
9
+ import { existsSync } from 'node:fs';
10
+ import { join, dirname } from 'node:path';
11
+ import { execSync } from 'node:child_process';
12
+ import { createRequire } from 'node:module';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const BROKER_NAME = 'agent-relay-broker';
16
+
17
+ /**
18
+ * Resolve the agent-relay-broker binary path.
19
+ *
20
+ * Search order:
21
+ * 1. SDK's bin/ directory (resolved via createRequire or import.meta.url)
22
+ * 2. Platform-specific name (agent-relay-broker-{platform}-{arch}) in bin/
23
+ * 3. PATH lookup via `which` / `where`
24
+ *
25
+ * @returns Absolute path to the broker binary, or null if not found
26
+ */
27
+ export function getBrokerBinaryPath(): string | null {
28
+ let binDir: string | null = null;
29
+ try {
30
+ // Use createRequire for ESM-compatible require.resolve
31
+ const esmRequire = createRequire(import.meta.url);
32
+ const sdkEntry = esmRequire.resolve('@agent-relay/sdk');
33
+ binDir = join(dirname(sdkEntry), '..', 'bin');
34
+ } catch {
35
+ try {
36
+ // Fallback: derive from import.meta.url
37
+ binDir = join(dirname(dirname(fileURLToPath(import.meta.url))), 'bin');
38
+ } catch {
39
+ // Neither method worked
40
+ }
41
+ }
42
+ if (!binDir) return null;
43
+
44
+ const ext = process.platform === 'win32' ? '.exe' : '';
45
+
46
+ // 1. Exact name in bin/
47
+ const exactPath = join(binDir, `${BROKER_NAME}${ext}`);
48
+ if (existsSync(exactPath)) {
49
+ return exactPath;
50
+ }
51
+
52
+ // 2. Platform-specific name in bin/
53
+ const platformSpecific = `${BROKER_NAME}-${process.platform}-${process.arch}${ext}`;
54
+ const platformPath = join(binDir, platformSpecific);
55
+ if (existsSync(platformPath)) {
56
+ return platformPath;
57
+ }
58
+
59
+ // 3. PATH lookup
60
+ try {
61
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
62
+ const result = execSync(`${cmd} ${BROKER_NAME}`, {
63
+ encoding: 'utf-8',
64
+ stdio: ['pipe', 'pipe', 'pipe'],
65
+ }).trim();
66
+ if (result) {
67
+ return result.split('\n')[0].trim();
68
+ }
69
+ } catch {
70
+ // Not found on PATH
71
+ }
72
+
73
+ return null;
74
+ }
@@ -137,6 +137,10 @@ const CLI_REGISTRY: Record<AgentCli, CliDefinition> = {
137
137
  ...extra,
138
138
  ],
139
139
  },
140
+ api: {
141
+ binaries: [],
142
+ nonInteractiveArgs: (task) => [task],
143
+ },
140
144
  };
141
145
 
142
146
  /**
@@ -20,6 +20,7 @@ import {
20
20
  type ProtocolEnvelope,
21
21
  type ProtocolError,
22
22
  type RestartPolicy,
23
+ type MessageInjectionMode,
23
24
  } from './protocol.js';
24
25
 
25
26
  export interface AgentRelayClientOptions {
@@ -99,6 +100,7 @@ export interface SendMessageInput {
99
100
  workspaceAlias?: string;
100
101
  priority?: number;
101
102
  data?: Record<string, unknown>;
103
+ mode?: MessageInjectionMode;
102
104
  }
103
105
 
104
106
  export interface ListAgent {
@@ -369,6 +371,16 @@ export class AgentRelayClient {
369
371
  return this.requestOk<{ name: string; bytes_written: number }>('send_input', { name, data });
370
372
  }
371
373
 
374
+ async subscribeChannels(name: string, channels: string[]): Promise<void> {
375
+ await this.start();
376
+ await this.requestOk<void>('subscribe_channels', { name, channels });
377
+ }
378
+
379
+ async unsubscribeChannels(name: string, channels: string[]): Promise<void> {
380
+ await this.start();
381
+ await this.requestOk<void>('unsubscribe_channels', { name, channels });
382
+ }
383
+
372
384
  async resizePty(
373
385
  name: string,
374
386
  rows: number,
@@ -423,6 +435,7 @@ export class AgentRelayClient {
423
435
  workspace_alias: input.workspaceAlias,
424
436
  priority: input.priority,
425
437
  data: input.data,
438
+ mode: input.mode,
426
439
  });
427
440
  } catch (error) {
428
441
  if (error instanceof AgentRelayProtocolError && error.code === 'unsupported_operation') {
@@ -1154,6 +1167,7 @@ export class HttpAgentRelayClient {
1154
1167
  workspaceAlias: input.workspaceAlias,
1155
1168
  priority: input.priority,
1156
1169
  data: input.data,
1170
+ mode: input.mode,
1157
1171
  }),
1158
1172
  });
1159
1173
  }
@@ -1176,6 +1190,20 @@ export class HttpAgentRelayClient {
1176
1190
  return { name: typeof payload?.name === 'string' ? payload.name : name };
1177
1191
  }
1178
1192
 
1193
+ async subscribeChannels(_name: string, _channels: string[]): Promise<void> {
1194
+ throw new Error(
1195
+ 'subscribeChannels is only available via the broker protocol (BrokerAgentRelayClient). ' +
1196
+ 'The HTTP API does not support dynamic channel subscription.'
1197
+ );
1198
+ }
1199
+
1200
+ async unsubscribeChannels(_name: string, _channels: string[]): Promise<void> {
1201
+ throw new Error(
1202
+ 'unsubscribeChannels is only available via the broker protocol (BrokerAgentRelayClient). ' +
1203
+ 'The HTTP API does not support dynamic channel unsubscription.'
1204
+ );
1205
+ }
1206
+
1179
1207
  async setModel(
1180
1208
  name: string,
1181
1209
  model: string,