@agent-relay/acp-bridge 0.1.0
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/README.md +171 -0
- package/dist/acp-agent.d.ts +114 -0
- package/dist/acp-agent.d.ts.map +1 -0
- package/dist/acp-agent.js +702 -0
- package/dist/acp-agent.js.map +1 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +125 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP Agent Implementation
|
|
3
|
+
*
|
|
4
|
+
* Implements the ACP Agent interface to bridge relay agents to ACP clients.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
8
|
+
import { RelayClient } from '@agent-relay/sdk';
|
|
9
|
+
/**
|
|
10
|
+
* ACP Agent that bridges to Agent Relay
|
|
11
|
+
*/
|
|
12
|
+
export class RelayACPAgent {
|
|
13
|
+
config;
|
|
14
|
+
relayClient = null;
|
|
15
|
+
connection = null;
|
|
16
|
+
sessions = new Map();
|
|
17
|
+
messageBuffer = new Map();
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Start the ACP agent with stdio transport
|
|
23
|
+
*/
|
|
24
|
+
async start() {
|
|
25
|
+
// Connect to relay daemon
|
|
26
|
+
const relayConfig = {
|
|
27
|
+
agentName: this.config.agentName,
|
|
28
|
+
program: '@agent-relay/acp-bridge',
|
|
29
|
+
cli: 'acp-bridge',
|
|
30
|
+
quiet: true,
|
|
31
|
+
};
|
|
32
|
+
if (this.config.socketPath) {
|
|
33
|
+
relayConfig.socketPath = this.config.socketPath;
|
|
34
|
+
}
|
|
35
|
+
this.relayClient = new RelayClient(relayConfig);
|
|
36
|
+
// Set up message handlers
|
|
37
|
+
this.relayClient.onMessage = (from, payload, messageId) => {
|
|
38
|
+
if (typeof payload.body !== 'string') {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.handleRelayMessage({
|
|
42
|
+
id: messageId,
|
|
43
|
+
from,
|
|
44
|
+
body: payload.body,
|
|
45
|
+
thread: payload.thread,
|
|
46
|
+
timestamp: Date.now(),
|
|
47
|
+
data: payload.data,
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
// Handle channel messages (e.g., #general)
|
|
51
|
+
this.relayClient.onChannelMessage = (from, channel, body) => {
|
|
52
|
+
this.debug('Received channel message:', from, channel, body.substring(0, 50));
|
|
53
|
+
// Route channel messages to all sessions
|
|
54
|
+
this.handleRelayMessage({
|
|
55
|
+
id: `channel-${Date.now()}`,
|
|
56
|
+
from: `${from} [${channel}]`,
|
|
57
|
+
body,
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
this.relayClient.onStateChange = (state) => {
|
|
62
|
+
this.debug('Relay client state:', state);
|
|
63
|
+
};
|
|
64
|
+
this.relayClient.onError = (error) => {
|
|
65
|
+
this.debug('Relay client error:', error);
|
|
66
|
+
};
|
|
67
|
+
try {
|
|
68
|
+
await this.relayClient.connect();
|
|
69
|
+
this.debug('Connected to relay daemon via SDK');
|
|
70
|
+
// Subscribe to #general channel to receive broadcast messages
|
|
71
|
+
this.relayClient.subscribe('#general');
|
|
72
|
+
this.debug('Subscribed to #general channel');
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
this.debug('Failed to connect to relay daemon via SDK:', err);
|
|
76
|
+
// Continue anyway - we can still function without relay
|
|
77
|
+
}
|
|
78
|
+
// Create ACP connection over stdio using ndJsonStream
|
|
79
|
+
const readable = this.nodeToWebReadable(process.stdin);
|
|
80
|
+
const writable = this.nodeToWebWritable(process.stdout);
|
|
81
|
+
const stream = acp.ndJsonStream(writable, readable);
|
|
82
|
+
// Create connection with agent factory
|
|
83
|
+
this.connection = new acp.AgentSideConnection((conn) => {
|
|
84
|
+
// Store connection reference for later use
|
|
85
|
+
this.connection = conn;
|
|
86
|
+
return this;
|
|
87
|
+
}, stream);
|
|
88
|
+
this.debug('ACP agent started');
|
|
89
|
+
// Keep alive by waiting for connection to close
|
|
90
|
+
await this.connection.closed;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Stop the agent
|
|
94
|
+
*/
|
|
95
|
+
async stop() {
|
|
96
|
+
this.relayClient?.destroy();
|
|
97
|
+
this.relayClient = null;
|
|
98
|
+
this.connection = null;
|
|
99
|
+
this.debug('ACP agent stopped');
|
|
100
|
+
}
|
|
101
|
+
// =========================================================================
|
|
102
|
+
// ACP Agent Interface Implementation
|
|
103
|
+
// =========================================================================
|
|
104
|
+
/**
|
|
105
|
+
* Initialize the agent connection
|
|
106
|
+
*/
|
|
107
|
+
async initialize(_params) {
|
|
108
|
+
return {
|
|
109
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
110
|
+
agentCapabilities: {
|
|
111
|
+
loadSession: this.config.capabilities?.supportsSessionLoading ?? false,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Authenticate with the client (no auth required for relay)
|
|
117
|
+
*/
|
|
118
|
+
async authenticate(_params) {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Create a new session
|
|
123
|
+
*/
|
|
124
|
+
async newSession(_params) {
|
|
125
|
+
const sessionId = randomUUID();
|
|
126
|
+
const session = {
|
|
127
|
+
id: sessionId,
|
|
128
|
+
createdAt: new Date(),
|
|
129
|
+
messages: [],
|
|
130
|
+
isProcessing: false,
|
|
131
|
+
};
|
|
132
|
+
this.sessions.set(sessionId, session);
|
|
133
|
+
this.messageBuffer.set(sessionId, []);
|
|
134
|
+
this.debug('Created new session:', sessionId);
|
|
135
|
+
// Show quick help in the editor panel
|
|
136
|
+
await this.sendTextUpdate(sessionId, this.getHelpText());
|
|
137
|
+
return { sessionId };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Load an existing session (not supported)
|
|
141
|
+
*/
|
|
142
|
+
async loadSession(_params) {
|
|
143
|
+
throw new Error('Session loading not supported');
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Set session mode (optional)
|
|
147
|
+
*/
|
|
148
|
+
async setSessionMode(_params) {
|
|
149
|
+
// Mode changes not implemented
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Handle a prompt from the client
|
|
154
|
+
*/
|
|
155
|
+
async prompt(params) {
|
|
156
|
+
const session = this.sessions.get(params.sessionId);
|
|
157
|
+
if (!session) {
|
|
158
|
+
throw new Error(`Session not found: ${params.sessionId}`);
|
|
159
|
+
}
|
|
160
|
+
if (session.isProcessing) {
|
|
161
|
+
throw new Error('Session is already processing a prompt');
|
|
162
|
+
}
|
|
163
|
+
session.isProcessing = true;
|
|
164
|
+
session.abortController = new AbortController();
|
|
165
|
+
try {
|
|
166
|
+
// Extract text content from the prompt
|
|
167
|
+
const userMessage = this.extractTextContent(params.prompt);
|
|
168
|
+
// Add to session history
|
|
169
|
+
session.messages.push({
|
|
170
|
+
role: 'user',
|
|
171
|
+
content: userMessage,
|
|
172
|
+
timestamp: new Date(),
|
|
173
|
+
});
|
|
174
|
+
// Handle agent-relay CLI-style commands locally before broadcasting
|
|
175
|
+
const handled = await this.tryHandleCliCommand(userMessage, params.sessionId);
|
|
176
|
+
if (handled) {
|
|
177
|
+
return { stopReason: 'end_turn' };
|
|
178
|
+
}
|
|
179
|
+
// Send to relay agents
|
|
180
|
+
const result = await this.bridgeToRelay(session, userMessage, params.sessionId, session.abortController.signal);
|
|
181
|
+
if (result.stopReason === 'cancelled') {
|
|
182
|
+
return { stopReason: 'cancelled' };
|
|
183
|
+
}
|
|
184
|
+
return { stopReason: 'end_turn' };
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
session.isProcessing = false;
|
|
188
|
+
session.abortController = undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Cancel the current operation
|
|
193
|
+
*/
|
|
194
|
+
async cancel(params) {
|
|
195
|
+
const session = this.sessions.get(params.sessionId);
|
|
196
|
+
if (session?.abortController) {
|
|
197
|
+
session.abortController.abort();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// =========================================================================
|
|
201
|
+
// Relay Bridge Logic
|
|
202
|
+
// =========================================================================
|
|
203
|
+
/**
|
|
204
|
+
* Parse @mentions from a message.
|
|
205
|
+
* Returns { targets: string[], message: string } where targets are agent names
|
|
206
|
+
* and message is the text with @mentions removed.
|
|
207
|
+
*
|
|
208
|
+
* Examples:
|
|
209
|
+
* "@Worker hello" -> { targets: ["Worker"], message: "hello" }
|
|
210
|
+
* "@Worker @Reviewer review this" -> { targets: ["Worker", "Reviewer"], message: "review this" }
|
|
211
|
+
* "hello everyone" -> { targets: [], message: "hello everyone" }
|
|
212
|
+
*/
|
|
213
|
+
parseAtMentions(text) {
|
|
214
|
+
const mentionRegex = /@(\w+)/g;
|
|
215
|
+
const targets = [];
|
|
216
|
+
let match;
|
|
217
|
+
while ((match = mentionRegex.exec(text)) !== null) {
|
|
218
|
+
targets.push(match[1]);
|
|
219
|
+
}
|
|
220
|
+
// Remove @mentions from message
|
|
221
|
+
const message = text.replace(/@\w+\s*/g, '').trim();
|
|
222
|
+
return { targets, message: message || text };
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Bridge a user prompt to relay agents and collect responses
|
|
226
|
+
*/
|
|
227
|
+
async bridgeToRelay(session, userMessage, sessionId, signal) {
|
|
228
|
+
if (!this.connection) {
|
|
229
|
+
return {
|
|
230
|
+
success: false,
|
|
231
|
+
stopReason: 'error',
|
|
232
|
+
responses: [],
|
|
233
|
+
error: 'No ACP connection',
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (!this.relayClient || this.relayClient.state !== 'READY') {
|
|
237
|
+
// If not connected to relay, return a helpful message
|
|
238
|
+
await this.connection.sessionUpdate({
|
|
239
|
+
sessionId,
|
|
240
|
+
update: {
|
|
241
|
+
sessionUpdate: 'agent_message_chunk',
|
|
242
|
+
content: {
|
|
243
|
+
type: 'text',
|
|
244
|
+
text: 'Agent Relay daemon is not connected. Please ensure the relay daemon is running.',
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
return {
|
|
249
|
+
success: false,
|
|
250
|
+
stopReason: 'end_turn',
|
|
251
|
+
responses: [],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const responses = [];
|
|
255
|
+
// Clear buffer
|
|
256
|
+
this.messageBuffer.set(session.id, []);
|
|
257
|
+
// Parse @mentions to target specific agents
|
|
258
|
+
const { targets, message: cleanMessage } = this.parseAtMentions(userMessage);
|
|
259
|
+
const hasTargets = targets.length > 0;
|
|
260
|
+
// Send "thinking" indicator with target info
|
|
261
|
+
const targetInfo = hasTargets
|
|
262
|
+
? `Sending to ${targets.map(t => `@${t}`).join(', ')}...\n\n`
|
|
263
|
+
: 'Broadcasting to all agents...\n\n';
|
|
264
|
+
await this.connection.sessionUpdate({
|
|
265
|
+
sessionId,
|
|
266
|
+
update: {
|
|
267
|
+
sessionUpdate: 'agent_message_chunk',
|
|
268
|
+
content: {
|
|
269
|
+
type: 'text',
|
|
270
|
+
text: targetInfo,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
// Send to specific agents or broadcast
|
|
275
|
+
let sent = false;
|
|
276
|
+
if (hasTargets) {
|
|
277
|
+
// Send to each mentioned agent
|
|
278
|
+
for (const target of targets) {
|
|
279
|
+
const result = this.relayClient.sendMessage(target, cleanMessage, 'message', undefined, session.id);
|
|
280
|
+
if (result)
|
|
281
|
+
sent = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
// Broadcast to all agents
|
|
286
|
+
sent = this.relayClient.sendMessage('*', userMessage, 'message', undefined, session.id);
|
|
287
|
+
}
|
|
288
|
+
if (!sent) {
|
|
289
|
+
await this.connection.sessionUpdate({
|
|
290
|
+
sessionId,
|
|
291
|
+
update: {
|
|
292
|
+
sessionUpdate: 'agent_message_chunk',
|
|
293
|
+
content: {
|
|
294
|
+
type: 'text',
|
|
295
|
+
text: 'Failed to send message to relay agents. Please check the relay daemon connection.',
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
return {
|
|
300
|
+
success: false,
|
|
301
|
+
stopReason: 'error',
|
|
302
|
+
responses,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
// Wait for responses with timeout
|
|
306
|
+
const responseTimeout = 30000; // 30 seconds
|
|
307
|
+
const startTime = Date.now();
|
|
308
|
+
while (Date.now() - startTime < responseTimeout) {
|
|
309
|
+
if (signal.aborted) {
|
|
310
|
+
return {
|
|
311
|
+
success: false,
|
|
312
|
+
stopReason: 'cancelled',
|
|
313
|
+
responses,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
// Check for new messages in buffer
|
|
317
|
+
const newMessages = this.messageBuffer.get(session.id) || [];
|
|
318
|
+
if (newMessages.length > 0) {
|
|
319
|
+
responses.push(...newMessages);
|
|
320
|
+
this.messageBuffer.set(session.id, []);
|
|
321
|
+
// Stream each response as it arrives
|
|
322
|
+
for (const msg of newMessages) {
|
|
323
|
+
await this.connection.sessionUpdate({
|
|
324
|
+
sessionId,
|
|
325
|
+
update: {
|
|
326
|
+
sessionUpdate: 'agent_message_chunk',
|
|
327
|
+
content: {
|
|
328
|
+
type: 'text',
|
|
329
|
+
text: `**${msg.from}**: ${msg.body}\n\n`,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
// Add to session history
|
|
334
|
+
session.messages.push({
|
|
335
|
+
role: 'assistant',
|
|
336
|
+
content: msg.body,
|
|
337
|
+
timestamp: new Date(msg.timestamp),
|
|
338
|
+
fromAgent: msg.from,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Small delay to prevent busy waiting
|
|
343
|
+
await this.sleep(100);
|
|
344
|
+
// If we have responses and nothing new for 2 seconds, consider it done
|
|
345
|
+
if (responses.length > 0) {
|
|
346
|
+
const lastMessage = responses[responses.length - 1];
|
|
347
|
+
if (Date.now() - lastMessage.timestamp > 2000) {
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
success: true,
|
|
354
|
+
stopReason: 'end_turn',
|
|
355
|
+
responses,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Handle incoming relay messages
|
|
360
|
+
*/
|
|
361
|
+
handleRelayMessage(message) {
|
|
362
|
+
this.debug('Received relay message:', message.from, message.body.substring(0, 50));
|
|
363
|
+
// Check for system messages (crash notifications, etc.)
|
|
364
|
+
if (message.data?.isSystemMessage) {
|
|
365
|
+
this.handleSystemMessage(message);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// Route to appropriate session based on thread
|
|
369
|
+
if (message.thread) {
|
|
370
|
+
const buffer = this.messageBuffer.get(message.thread);
|
|
371
|
+
if (buffer) {
|
|
372
|
+
buffer.push(message);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// If no specific session, add to all active sessions
|
|
377
|
+
for (const [sessionId, session] of this.sessions) {
|
|
378
|
+
if (session.isProcessing) {
|
|
379
|
+
const buffer = this.messageBuffer.get(sessionId) || [];
|
|
380
|
+
buffer.push(message);
|
|
381
|
+
this.messageBuffer.set(sessionId, buffer);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Handle system messages (crash notifications, etc.)
|
|
387
|
+
* These are displayed to all sessions regardless of processing state.
|
|
388
|
+
*/
|
|
389
|
+
handleSystemMessage(message) {
|
|
390
|
+
const data = message.data || {};
|
|
391
|
+
// Format crash notifications nicely
|
|
392
|
+
if (data.crashType) {
|
|
393
|
+
const agentName = data.agentName || message.from || 'Unknown agent';
|
|
394
|
+
const signal = data.signal ? ` (${data.signal})` : '';
|
|
395
|
+
const exitCode = data.exitCode !== undefined ? ` [exit code: ${data.exitCode}]` : '';
|
|
396
|
+
const crashNotification = [
|
|
397
|
+
'',
|
|
398
|
+
`⚠️ **Agent Crashed**: \`${agentName}\`${signal}${exitCode}`,
|
|
399
|
+
'',
|
|
400
|
+
message.body,
|
|
401
|
+
'',
|
|
402
|
+
].join('\n');
|
|
403
|
+
// Send to all sessions (not just processing ones)
|
|
404
|
+
this.broadcastToAllSessions(crashNotification);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
// Generic system message
|
|
408
|
+
this.broadcastToAllSessions(`**System**: ${message.body}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Broadcast a message to all active sessions.
|
|
413
|
+
*/
|
|
414
|
+
broadcastToAllSessions(text) {
|
|
415
|
+
for (const [sessionId] of this.sessions) {
|
|
416
|
+
this.sendTextUpdate(sessionId, text).catch((err) => {
|
|
417
|
+
this.debug('Failed to send broadcast to session:', sessionId, err);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// =========================================================================
|
|
422
|
+
// CLI Command Handling (Zed Agent Panel)
|
|
423
|
+
// =========================================================================
|
|
424
|
+
/**
|
|
425
|
+
* Parse and handle agent-relay CLI-style commands coming from the editor.
|
|
426
|
+
*/
|
|
427
|
+
async tryHandleCliCommand(userMessage, sessionId) {
|
|
428
|
+
const tokens = this.parseCliArgs(userMessage);
|
|
429
|
+
if (tokens.length === 0) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
let command = tokens[0];
|
|
433
|
+
let args = tokens.slice(1);
|
|
434
|
+
// Support "agent-relay ..." and "relay ..." prefixes
|
|
435
|
+
if (command === 'agent-relay' || command === 'relay') {
|
|
436
|
+
if (args.length === 0)
|
|
437
|
+
return false;
|
|
438
|
+
command = args[0];
|
|
439
|
+
args = args.slice(1);
|
|
440
|
+
}
|
|
441
|
+
else if (command === 'create' && args[0] === 'agent') {
|
|
442
|
+
command = 'spawn';
|
|
443
|
+
args = args.slice(1);
|
|
444
|
+
}
|
|
445
|
+
switch (command) {
|
|
446
|
+
case 'spawn':
|
|
447
|
+
case 'create-agent':
|
|
448
|
+
return this.handleSpawnCommand(args, sessionId);
|
|
449
|
+
case 'release':
|
|
450
|
+
return this.handleReleaseCommand(args, sessionId);
|
|
451
|
+
case 'agents':
|
|
452
|
+
case 'who':
|
|
453
|
+
return this.handleListAgentsCommand(sessionId);
|
|
454
|
+
case 'status':
|
|
455
|
+
return this.handleStatusCommand(sessionId);
|
|
456
|
+
case 'help':
|
|
457
|
+
await this.sendTextUpdate(sessionId, this.getHelpText());
|
|
458
|
+
return true;
|
|
459
|
+
default:
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async handleSpawnCommand(args, sessionId) {
|
|
464
|
+
const [name, cli, ...taskParts] = args;
|
|
465
|
+
if (!name || !cli) {
|
|
466
|
+
await this.sendTextUpdate(sessionId, 'Usage: agent-relay spawn <name> <cli> "<task>"');
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
if (!this.relayClient || this.relayClient.state !== 'READY') {
|
|
470
|
+
await this.sendTextUpdate(sessionId, 'Relay daemon is not connected (cannot spawn).');
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
const task = taskParts.join(' ').trim() || undefined;
|
|
474
|
+
await this.sendTextUpdate(sessionId, `Spawning ${name} (${cli})${task ? `: ${task}` : ''}`);
|
|
475
|
+
try {
|
|
476
|
+
const result = await this.relayClient.spawn({
|
|
477
|
+
name,
|
|
478
|
+
cli,
|
|
479
|
+
task,
|
|
480
|
+
waitForReady: true,
|
|
481
|
+
});
|
|
482
|
+
if (result.success) {
|
|
483
|
+
const readyText = result.ready ? ' (ready)' : '';
|
|
484
|
+
await this.sendTextUpdate(sessionId, `Spawned ${name}${readyText}.`);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
await this.sendTextUpdate(sessionId, `Failed to spawn ${name}: ${result.error || 'unknown error'}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
await this.sendTextUpdate(sessionId, `Spawn error for ${name}: ${err.message}`);
|
|
492
|
+
}
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
async handleReleaseCommand(args, sessionId) {
|
|
496
|
+
const [name] = args;
|
|
497
|
+
if (!name) {
|
|
498
|
+
await this.sendTextUpdate(sessionId, 'Usage: agent-relay release <name>');
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
if (!this.relayClient || this.relayClient.state !== 'READY') {
|
|
502
|
+
await this.sendTextUpdate(sessionId, 'Relay daemon is not connected (cannot release).');
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
await this.sendTextUpdate(sessionId, `Releasing ${name}...`);
|
|
506
|
+
try {
|
|
507
|
+
const result = await this.relayClient.release(name);
|
|
508
|
+
if (result.success) {
|
|
509
|
+
await this.sendTextUpdate(sessionId, `Released ${name}.`);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
await this.sendTextUpdate(sessionId, `Failed to release ${name}: ${result.error || 'unknown error'}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
await this.sendTextUpdate(sessionId, `Release error for ${name}: ${err.message}`);
|
|
517
|
+
}
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
async handleListAgentsCommand(sessionId) {
|
|
521
|
+
if (!this.relayClient || this.relayClient.state !== 'READY') {
|
|
522
|
+
await this.sendTextUpdate(sessionId, 'Relay daemon is not connected (cannot list agents).');
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const agents = await this.relayClient.listConnectedAgents();
|
|
527
|
+
if (!agents.length) {
|
|
528
|
+
await this.sendTextUpdate(sessionId, 'No agents are currently connected.');
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
const lines = agents.map((agent) => `- ${agent.name}${agent.cli ? ` (${agent.cli})` : ''}`);
|
|
532
|
+
await this.sendTextUpdate(sessionId, ['Connected agents:', ...lines].join('\n'));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
await this.sendTextUpdate(sessionId, `Failed to list agents: ${err.message}`);
|
|
537
|
+
}
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
async handleStatusCommand(sessionId) {
|
|
541
|
+
const lines = ['Agent Relay Status', ''];
|
|
542
|
+
if (!this.relayClient) {
|
|
543
|
+
lines.push('Relay client: Not initialized');
|
|
544
|
+
await this.sendTextUpdate(sessionId, lines.join('\n'));
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
const state = this.relayClient.state;
|
|
548
|
+
const isConnected = state === 'READY';
|
|
549
|
+
lines.push(`Connection: ${isConnected ? 'Connected' : 'Disconnected'}`);
|
|
550
|
+
lines.push(`State: ${state}`);
|
|
551
|
+
lines.push(`Agent name: ${this.config.agentName}`);
|
|
552
|
+
if (isConnected) {
|
|
553
|
+
// Try to get connected agents count
|
|
554
|
+
try {
|
|
555
|
+
const agents = await this.relayClient.listConnectedAgents();
|
|
556
|
+
lines.push(`Connected agents: ${agents.length}`);
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
// Ignore errors when listing agents
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
await this.sendTextUpdate(sessionId, lines.join('\n'));
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
async sendTextUpdate(sessionId, text) {
|
|
566
|
+
if (!this.connection)
|
|
567
|
+
return;
|
|
568
|
+
await this.connection.sessionUpdate({
|
|
569
|
+
sessionId,
|
|
570
|
+
update: {
|
|
571
|
+
sessionUpdate: 'agent_message_chunk',
|
|
572
|
+
content: {
|
|
573
|
+
type: 'text',
|
|
574
|
+
text,
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
parseCliArgs(input) {
|
|
580
|
+
const args = [];
|
|
581
|
+
let current = '';
|
|
582
|
+
let inQuote = null;
|
|
583
|
+
let escape = false;
|
|
584
|
+
for (const char of input.trim()) {
|
|
585
|
+
if (escape) {
|
|
586
|
+
current += char;
|
|
587
|
+
escape = false;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (char === '\\') {
|
|
591
|
+
escape = true;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (inQuote) {
|
|
595
|
+
if (char === inQuote) {
|
|
596
|
+
inQuote = null;
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
current += char;
|
|
600
|
+
}
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (char === '"' || char === "'") {
|
|
604
|
+
inQuote = char;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (/\s/.test(char)) {
|
|
608
|
+
if (current) {
|
|
609
|
+
args.push(current);
|
|
610
|
+
current = '';
|
|
611
|
+
}
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
current += char;
|
|
615
|
+
}
|
|
616
|
+
if (current) {
|
|
617
|
+
args.push(current);
|
|
618
|
+
}
|
|
619
|
+
return args;
|
|
620
|
+
}
|
|
621
|
+
getHelpText() {
|
|
622
|
+
return [
|
|
623
|
+
'Agent Relay (Zed)',
|
|
624
|
+
'',
|
|
625
|
+
'Commands:',
|
|
626
|
+
'- agent-relay spawn <name> <cli> "task"',
|
|
627
|
+
'- agent-relay release <name>',
|
|
628
|
+
'- agent-relay agents',
|
|
629
|
+
'- agent-relay status',
|
|
630
|
+
'- agent-relay help',
|
|
631
|
+
'',
|
|
632
|
+
'Other messages are broadcast to connected agents.',
|
|
633
|
+
].join('\n');
|
|
634
|
+
}
|
|
635
|
+
// =========================================================================
|
|
636
|
+
// Utility Methods
|
|
637
|
+
// =========================================================================
|
|
638
|
+
/**
|
|
639
|
+
* Extract text content from ACP content blocks
|
|
640
|
+
*/
|
|
641
|
+
extractTextContent(content) {
|
|
642
|
+
return content
|
|
643
|
+
.filter((block) => block.type === 'text')
|
|
644
|
+
.map((block) => block.text)
|
|
645
|
+
.join('\n');
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Convert Node.js readable stream to Web ReadableStream
|
|
649
|
+
*/
|
|
650
|
+
nodeToWebReadable(nodeStream) {
|
|
651
|
+
return new ReadableStream({
|
|
652
|
+
start(controller) {
|
|
653
|
+
nodeStream.on('data', (chunk) => {
|
|
654
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
655
|
+
});
|
|
656
|
+
nodeStream.on('end', () => {
|
|
657
|
+
controller.close();
|
|
658
|
+
});
|
|
659
|
+
nodeStream.on('error', (err) => {
|
|
660
|
+
controller.error(err);
|
|
661
|
+
});
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Convert Node.js writable stream to Web WritableStream
|
|
667
|
+
*/
|
|
668
|
+
nodeToWebWritable(nodeStream) {
|
|
669
|
+
return new WritableStream({
|
|
670
|
+
write(chunk) {
|
|
671
|
+
return new Promise((resolve, reject) => {
|
|
672
|
+
nodeStream.write(Buffer.from(chunk), (err) => {
|
|
673
|
+
if (err)
|
|
674
|
+
reject(err);
|
|
675
|
+
else
|
|
676
|
+
resolve();
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
},
|
|
680
|
+
close() {
|
|
681
|
+
return new Promise((resolve) => {
|
|
682
|
+
nodeStream.end(() => resolve());
|
|
683
|
+
});
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Sleep utility
|
|
689
|
+
*/
|
|
690
|
+
sleep(ms) {
|
|
691
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Debug logging
|
|
695
|
+
*/
|
|
696
|
+
debug(...args) {
|
|
697
|
+
if (this.config.debug) {
|
|
698
|
+
console.error('[RelayACPAgent]', ...args);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
//# sourceMappingURL=acp-agent.js.map
|