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.
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +3853 -17163
- package/package.json +8 -8
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/broker-path.d.ts +19 -0
- package/packages/sdk/dist/broker-path.d.ts.map +1 -0
- package/packages/sdk/dist/broker-path.js +71 -0
- package/packages/sdk/dist/broker-path.js.map +1 -0
- package/packages/sdk/dist/cli-registry.d.ts.map +1 -1
- package/packages/sdk/dist/cli-registry.js +4 -0
- package/packages/sdk/dist/cli-registry.js.map +1 -1
- package/packages/sdk/dist/client.d.ts +6 -1
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +18 -0
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/communicate/adapters/index.d.ts +0 -5
- package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/adapters/index.js +0 -5
- package/packages/sdk/dist/communicate/adapters/index.js.map +1 -1
- package/packages/sdk/dist/communicate/adapters/pi.d.ts +0 -1
- package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/adapters/pi.js +0 -4
- package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -1
- package/packages/sdk/dist/communicate/core.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/core.js +2 -3
- package/packages/sdk/dist/communicate/core.js.map +1 -1
- package/packages/sdk/dist/communicate/index.d.ts +17 -1
- package/packages/sdk/dist/communicate/index.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/index.js +40 -1
- package/packages/sdk/dist/communicate/index.js.map +1 -1
- package/packages/sdk/dist/communicate/transport.d.ts +0 -1
- package/packages/sdk/dist/communicate/transport.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/transport.js +42 -134
- package/packages/sdk/dist/communicate/transport.js.map +1 -1
- package/packages/sdk/dist/http.d.ts +38 -0
- package/packages/sdk/dist/http.d.ts.map +1 -0
- package/packages/sdk/dist/http.js +60 -0
- package/packages/sdk/dist/http.js.map +1 -0
- package/packages/sdk/dist/protocol.d.ts +25 -0
- package/packages/sdk/dist/protocol.d.ts.map +1 -1
- package/packages/sdk/dist/relay.d.ts +26 -3
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +62 -4
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/workflows/api-executor.d.ts +16 -0
- package/packages/sdk/dist/workflows/api-executor.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/api-executor.js +94 -0
- package/packages/sdk/dist/workflows/api-executor.js.map +1 -0
- package/packages/sdk/dist/workflows/builder.d.ts +14 -0
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +26 -0
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/cloud-runner.d.ts +15 -0
- package/packages/sdk/dist/workflows/cloud-runner.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/cloud-runner.js +41 -0
- package/packages/sdk/dist/workflows/cloud-runner.js.map +1 -0
- package/packages/sdk/dist/workflows/index.d.ts +2 -0
- package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/index.js +1 -0
- package/packages/sdk/dist/workflows/index.js.map +1 -1
- package/packages/sdk/dist/workflows/run.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/run.js +4 -0
- package/packages/sdk/dist/workflows/run.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +14 -0
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +154 -10
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/types.d.ts +13 -3
- package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/types.js +5 -1
- package/packages/sdk/dist/workflows/types.js.map +1 -1
- package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/validator.js +12 -0
- package/packages/sdk/dist/workflows/validator.js.map +1 -1
- package/packages/sdk/package.json +13 -3
- package/packages/sdk/src/__tests__/channel-management.test.ts +131 -0
- package/packages/sdk/src/__tests__/communicate/core.test.ts +36 -88
- package/packages/sdk/src/__tests__/communicate/transport.test.ts +41 -80
- package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +120 -0
- package/packages/sdk/src/__tests__/relay-channel-ops.test.ts +121 -0
- package/packages/sdk/src/broker-path.ts +74 -0
- package/packages/sdk/src/cli-registry.ts +4 -0
- package/packages/sdk/src/client.ts +28 -0
- package/packages/sdk/src/communicate/adapters/index.ts +0 -5
- package/packages/sdk/src/communicate/adapters/pi.ts +1 -5
- package/packages/sdk/src/communicate/core.ts +6 -10
- package/packages/sdk/src/communicate/index.ts +57 -1
- package/packages/sdk/src/communicate/transport.ts +46 -177
- package/packages/sdk/src/http.ts +96 -0
- package/packages/sdk/src/protocol.ts +24 -0
- package/packages/sdk/src/relay.ts +93 -8
- package/packages/sdk/src/workflows/README.md +5 -2
- package/packages/sdk/src/workflows/api-executor.ts +108 -0
- package/packages/sdk/src/workflows/builder.ts +40 -0
- package/packages/sdk/src/workflows/cloud-runner.ts +56 -0
- package/packages/sdk/src/workflows/index.ts +2 -0
- package/packages/sdk/src/workflows/run.ts +5 -0
- package/packages/sdk/src/workflows/runner.ts +181 -11
- package/packages/sdk/src/workflows/types.ts +19 -4
- package/packages/sdk/src/workflows/validator.ts +15 -0
- package/packages/sdk-py/README.md +7 -0
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +2 -0
- package/packages/sdk-py/src/agent_relay/client.py +4 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +0 -9
- package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +5 -9
- package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +5 -7
- package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +3 -13
- package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +5 -2
- package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +5 -9
- package/packages/sdk-py/src/agent_relay/communicate/core.py +7 -24
- package/packages/sdk-py/src/agent_relay/communicate/transport.py +35 -212
- package/packages/sdk-py/src/agent_relay/communicate/types.py +1 -1
- package/packages/sdk-py/src/agent_relay/protocol.py +1 -0
- package/packages/sdk-py/src/agent_relay/relay.py +9 -1
- package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +6 -6
- package/packages/sdk-py/tests/communicate/conftest.py +86 -233
- package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +2 -2
- package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +14 -24
- package/packages/sdk-py/tests/communicate/test_transport.py +65 -54
- package/packages/sdk-py/tests/test_send_message_mode.py +91 -0
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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, {
|
|
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, {
|
|
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
|
|
252
|
-
assert.
|
|
253
|
-
assert.
|
|
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
|
|
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
|
|
267
|
-
assert.ok(
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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.
|
|
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
|
+
}
|
|
@@ -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,
|