gopherhole_openclaw_a2a 0.1.2
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 +67 -0
- package/SKILL.md +84 -0
- package/clawdbot.plugin.json +9 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +82 -0
- package/dist/src/channel.d.ts +68 -0
- package/dist/src/channel.js +271 -0
- package/dist/src/connection.d.ts +62 -0
- package/dist/src/connection.js +513 -0
- package/dist/src/gateway-client.d.ts +18 -0
- package/dist/src/gateway-client.js +256 -0
- package/dist/src/types.d.ts +73 -0
- package/dist/src/types.js +5 -0
- package/index.ts +100 -0
- package/package.json +46 -0
- package/src/channel.ts +377 -0
- package/src/connection.ts +605 -0
- package/src/gateway-client.ts +317 -0
- package/src/types.ts +73 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Connection Manager
|
|
3
|
+
* Handles WebSocket connections to other agents and the bridge
|
|
4
|
+
*/
|
|
5
|
+
import WebSocket from 'ws';
|
|
6
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
7
|
+
export class A2AConnectionManager {
|
|
8
|
+
connections = new Map();
|
|
9
|
+
pendingRequests = new Map();
|
|
10
|
+
reconnectTimers = new Map();
|
|
11
|
+
messageHandler = null;
|
|
12
|
+
config;
|
|
13
|
+
agentId;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.agentId = config.agentId ?? 'clawdbot';
|
|
17
|
+
}
|
|
18
|
+
setMessageHandler(handler) {
|
|
19
|
+
this.messageHandler = handler;
|
|
20
|
+
}
|
|
21
|
+
async start() {
|
|
22
|
+
// Connect to direct agents
|
|
23
|
+
if (this.config.agents) {
|
|
24
|
+
for (const agent of this.config.agents) {
|
|
25
|
+
await this.connectToAgent(agent.id, agent.url, agent.name);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Connect to bridge if configured
|
|
29
|
+
if (this.config.bridgeUrl) {
|
|
30
|
+
await this.connectToAgent('bridge', this.config.bridgeUrl, 'A2A Bridge');
|
|
31
|
+
}
|
|
32
|
+
// Connect to GopherHole if configured
|
|
33
|
+
if (this.config.gopherhole?.enabled && this.config.gopherhole?.apiKey) {
|
|
34
|
+
await this.connectToGopherHole();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async connectToGopherHole() {
|
|
38
|
+
const gphConfig = this.config.gopherhole;
|
|
39
|
+
const hubUrl = gphConfig.hubUrl || 'wss://gopherhole.helixdata.workers.dev/ws';
|
|
40
|
+
const conn = {
|
|
41
|
+
id: 'gopherhole',
|
|
42
|
+
name: 'GopherHole Hub',
|
|
43
|
+
url: hubUrl,
|
|
44
|
+
ws: null,
|
|
45
|
+
connected: false,
|
|
46
|
+
reconnectAttempts: 0,
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
await this.establishGopherHoleConnection(conn, gphConfig.apiKey);
|
|
50
|
+
this.connections.set('gopherhole', conn);
|
|
51
|
+
console.log(`[a2a] Connected to GopherHole Hub`);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error(`[a2a] Failed to connect to GopherHole:`, err.message);
|
|
55
|
+
this.connections.set('gopherhole', conn);
|
|
56
|
+
this.scheduleReconnect('gopherhole');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async establishGopherHoleConnection(conn, apiKey) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const ws = new WebSocket(conn.url);
|
|
62
|
+
const timeout = setTimeout(() => {
|
|
63
|
+
ws.terminate();
|
|
64
|
+
reject(new Error('GopherHole connection timeout'));
|
|
65
|
+
}, 10000);
|
|
66
|
+
ws.on('open', () => {
|
|
67
|
+
console.log('[a2a] GopherHole connected, authenticating...');
|
|
68
|
+
ws.send(JSON.stringify({ type: 'auth', token: apiKey }));
|
|
69
|
+
});
|
|
70
|
+
ws.on('message', (data) => {
|
|
71
|
+
try {
|
|
72
|
+
const msg = JSON.parse(data.toString());
|
|
73
|
+
if (msg.type === 'auth_ok' || msg.type === 'welcome') {
|
|
74
|
+
clearTimeout(timeout);
|
|
75
|
+
conn.ws = ws;
|
|
76
|
+
conn.connected = true;
|
|
77
|
+
conn.reconnectAttempts = 0;
|
|
78
|
+
conn.lastPingAt = Date.now();
|
|
79
|
+
console.log(`[a2a] GopherHole authenticated as ${msg.agentId}`);
|
|
80
|
+
resolve();
|
|
81
|
+
}
|
|
82
|
+
else if (msg.type === 'auth_error') {
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
reject(new Error(msg.error || 'GopherHole auth failed'));
|
|
85
|
+
}
|
|
86
|
+
else if (msg.type === 'message') {
|
|
87
|
+
// Incoming message from another agent via GopherHole
|
|
88
|
+
console.log(`[a2a] Received GopherHole message: taskId=${msg.taskId}, from=${msg.from}`);
|
|
89
|
+
const a2aMsg = {
|
|
90
|
+
type: 'message',
|
|
91
|
+
taskId: msg.taskId || `gph-${Date.now()}`,
|
|
92
|
+
from: msg.from,
|
|
93
|
+
content: msg.payload,
|
|
94
|
+
};
|
|
95
|
+
this.handleMessage('gopherhole', a2aMsg);
|
|
96
|
+
}
|
|
97
|
+
else if (msg.jsonrpc === '2.0' && msg.result) {
|
|
98
|
+
// JSON-RPC response - task status (may be immediate success/failure)
|
|
99
|
+
const requestId = msg.id;
|
|
100
|
+
const taskId = msg.result?.id;
|
|
101
|
+
const state = msg.result?.status?.state;
|
|
102
|
+
console.log(`[a2a] GopherHole JSON-RPC response: requestId=${requestId}, taskId=${taskId}, state=${state}`);
|
|
103
|
+
// Handle terminal states immediately (use requestId - that's what we stored)
|
|
104
|
+
if (state === 'completed' || state === 'failed') {
|
|
105
|
+
this.resolveGopherHoleTask(requestId, msg.result);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// For 'working'/'submitted', map taskId for future task_update events
|
|
109
|
+
if (taskId && requestId && taskId !== requestId) {
|
|
110
|
+
const pending = this.pendingRequests.get(requestId);
|
|
111
|
+
if (pending) {
|
|
112
|
+
this.pendingRequests.set(taskId, pending);
|
|
113
|
+
this.pendingRequests.delete(requestId);
|
|
114
|
+
console.log(`[a2a] Mapped taskId ${taskId} from requestId ${requestId}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (msg.jsonrpc === '2.0' && msg.error) {
|
|
119
|
+
// JSON-RPC error response
|
|
120
|
+
const requestId = msg.id;
|
|
121
|
+
console.log(`[a2a] GopherHole JSON-RPC error: id=${requestId}, error=${msg.error?.message}`);
|
|
122
|
+
const pending = this.pendingRequests.get(requestId);
|
|
123
|
+
if (pending) {
|
|
124
|
+
clearTimeout(pending.timeout);
|
|
125
|
+
this.pendingRequests.delete(requestId);
|
|
126
|
+
pending.reject(new Error(msg.error?.message || 'RPC error'));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (msg.type === 'task_update') {
|
|
130
|
+
// Task update event - nested under msg.task
|
|
131
|
+
const task = msg.task || msg;
|
|
132
|
+
const taskId = task.id || task.taskId;
|
|
133
|
+
const state = task.status?.state;
|
|
134
|
+
console.log(`[a2a] GopherHole task_update: taskId=${taskId}, state=${state}`);
|
|
135
|
+
if (state === 'completed' || state === 'failed') {
|
|
136
|
+
this.resolveGopherHoleTask(taskId, task);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (msg.type === 'error') {
|
|
140
|
+
// Error response (e.g., AGENT_NOT_FOUND)
|
|
141
|
+
const taskId = msg.taskId;
|
|
142
|
+
console.log(`[a2a] GopherHole error: code=${msg.code}, message=${msg.message}, taskId=${taskId}`);
|
|
143
|
+
if (taskId) {
|
|
144
|
+
const pending = this.pendingRequests.get(taskId);
|
|
145
|
+
if (pending) {
|
|
146
|
+
clearTimeout(pending.timeout);
|
|
147
|
+
this.pendingRequests.delete(taskId);
|
|
148
|
+
pending.reject(new Error(msg.message || msg.code || 'Unknown error'));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Unhandled message type - log for debugging
|
|
154
|
+
console.log(`[a2a] Unhandled GopherHole message: ${JSON.stringify(msg).slice(0, 500)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
console.error(`[a2a] Failed to parse GopherHole message:`, err);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
ws.on('close', (code, reason) => {
|
|
162
|
+
conn.connected = false;
|
|
163
|
+
conn.ws = null;
|
|
164
|
+
console.log(`[a2a] Disconnected from GopherHole: ${code} ${reason?.toString()}`);
|
|
165
|
+
this.scheduleReconnect('gopherhole');
|
|
166
|
+
});
|
|
167
|
+
ws.on('error', (err) => {
|
|
168
|
+
clearTimeout(timeout);
|
|
169
|
+
console.error(`[a2a] GopherHole WebSocket error:`, err.message);
|
|
170
|
+
if (!conn.connected) {
|
|
171
|
+
reject(err);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async stop() {
|
|
177
|
+
// Clear all reconnect timers
|
|
178
|
+
for (const timer of this.reconnectTimers.values()) {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
}
|
|
181
|
+
this.reconnectTimers.clear();
|
|
182
|
+
// Clear pending requests
|
|
183
|
+
for (const pending of this.pendingRequests.values()) {
|
|
184
|
+
clearTimeout(pending.timeout);
|
|
185
|
+
pending.reject(new Error('Connection closing'));
|
|
186
|
+
}
|
|
187
|
+
this.pendingRequests.clear();
|
|
188
|
+
// Close all connections
|
|
189
|
+
for (const conn of this.connections.values()) {
|
|
190
|
+
if (conn.ws && conn.ws.readyState === WebSocket.OPEN) {
|
|
191
|
+
conn.ws.close(1000, 'Shutting down');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
this.connections.clear();
|
|
195
|
+
}
|
|
196
|
+
async connectToAgent(id, url, name) {
|
|
197
|
+
const conn = {
|
|
198
|
+
id,
|
|
199
|
+
name: name ?? id,
|
|
200
|
+
url,
|
|
201
|
+
ws: null,
|
|
202
|
+
connected: false,
|
|
203
|
+
reconnectAttempts: 0,
|
|
204
|
+
};
|
|
205
|
+
try {
|
|
206
|
+
await this.establishConnection(conn);
|
|
207
|
+
this.connections.set(id, conn);
|
|
208
|
+
console.log(`[a2a] Connected to agent: ${conn.name} (${id})`);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
console.error(`[a2a] Failed to connect to ${id}:`, err.message);
|
|
212
|
+
this.connections.set(id, conn);
|
|
213
|
+
this.scheduleReconnect(id);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async establishConnection(conn) {
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
const ws = new WebSocket(conn.url);
|
|
219
|
+
const timeout = setTimeout(() => {
|
|
220
|
+
ws.terminate();
|
|
221
|
+
reject(new Error('Connection timeout'));
|
|
222
|
+
}, 10000);
|
|
223
|
+
ws.on('open', () => {
|
|
224
|
+
clearTimeout(timeout);
|
|
225
|
+
conn.ws = ws;
|
|
226
|
+
conn.connected = true;
|
|
227
|
+
conn.reconnectAttempts = 0;
|
|
228
|
+
conn.lastPingAt = Date.now();
|
|
229
|
+
// Send agent card announcement
|
|
230
|
+
this.sendAgentAnnounce(conn);
|
|
231
|
+
resolve();
|
|
232
|
+
});
|
|
233
|
+
ws.on('message', (data) => {
|
|
234
|
+
try {
|
|
235
|
+
const message = JSON.parse(data.toString());
|
|
236
|
+
this.handleMessage(conn.id, message);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
console.error(`[a2a] Failed to parse message from ${conn.id}:`, err);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
ws.on('close', (code, reason) => {
|
|
243
|
+
conn.connected = false;
|
|
244
|
+
conn.ws = null;
|
|
245
|
+
console.log(`[a2a] Disconnected from ${conn.id}: ${code} ${reason?.toString()}`);
|
|
246
|
+
this.scheduleReconnect(conn.id);
|
|
247
|
+
});
|
|
248
|
+
ws.on('error', (err) => {
|
|
249
|
+
clearTimeout(timeout);
|
|
250
|
+
console.error(`[a2a] WebSocket error for ${conn.id}:`, err.message);
|
|
251
|
+
reject(err);
|
|
252
|
+
});
|
|
253
|
+
ws.on('ping', () => {
|
|
254
|
+
conn.lastPingAt = Date.now();
|
|
255
|
+
ws.pong();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
sendAgentAnnounce(conn) {
|
|
260
|
+
if (!conn.ws || !conn.connected)
|
|
261
|
+
return;
|
|
262
|
+
// Announce ourselves to the agent/bridge
|
|
263
|
+
const announce = {
|
|
264
|
+
type: 'announce',
|
|
265
|
+
agent: {
|
|
266
|
+
id: this.agentId,
|
|
267
|
+
name: this.config.agentName ?? 'Clawdbot',
|
|
268
|
+
description: 'Clawdbot AI assistant',
|
|
269
|
+
skills: ['chat', 'tasks', 'tools'],
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
conn.ws.send(JSON.stringify(announce));
|
|
273
|
+
}
|
|
274
|
+
scheduleReconnect(agentId) {
|
|
275
|
+
if (this.reconnectTimers.has(agentId))
|
|
276
|
+
return;
|
|
277
|
+
const conn = this.connections.get(agentId);
|
|
278
|
+
if (!conn)
|
|
279
|
+
return;
|
|
280
|
+
// Exponential backoff: 5s, 10s, 20s, 40s, max 60s
|
|
281
|
+
const baseDelay = this.config.reconnectIntervalMs ?? 5000;
|
|
282
|
+
const delay = Math.min(baseDelay * Math.pow(2, conn.reconnectAttempts), 60000);
|
|
283
|
+
conn.reconnectAttempts++;
|
|
284
|
+
console.log(`[a2a] Scheduling reconnect to ${agentId} in ${delay}ms`);
|
|
285
|
+
const timer = setTimeout(async () => {
|
|
286
|
+
this.reconnectTimers.delete(agentId);
|
|
287
|
+
if (!conn.connected) {
|
|
288
|
+
try {
|
|
289
|
+
// Use GopherHole auth flow for gopherhole connection
|
|
290
|
+
if (agentId === 'gopherhole' && this.config.gopherhole?.apiKey) {
|
|
291
|
+
await this.establishGopherHoleConnection(conn, this.config.gopherhole.apiKey);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
await this.establishConnection(conn);
|
|
295
|
+
}
|
|
296
|
+
console.log(`[a2a] Reconnected to ${agentId}`);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
this.scheduleReconnect(agentId);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}, delay);
|
|
303
|
+
this.reconnectTimers.set(agentId, timer);
|
|
304
|
+
}
|
|
305
|
+
handleMessage(agentId, message) {
|
|
306
|
+
// Handle responses to our requests
|
|
307
|
+
if (message.type === 'response' || message.status === 'completed' || message.status === 'failed') {
|
|
308
|
+
const pending = this.pendingRequests.get(message.taskId);
|
|
309
|
+
if (pending) {
|
|
310
|
+
clearTimeout(pending.timeout);
|
|
311
|
+
this.pendingRequests.delete(message.taskId);
|
|
312
|
+
if (message.status === 'failed' || message.error) {
|
|
313
|
+
pending.reject(new Error(message.error ?? 'Task failed'));
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
const text = message.content?.parts
|
|
317
|
+
?.filter((p) => p.kind === 'text')
|
|
318
|
+
.map((p) => p.text)
|
|
319
|
+
.join('\n') ?? '';
|
|
320
|
+
pending.resolve({
|
|
321
|
+
text,
|
|
322
|
+
status: message.status ?? 'completed',
|
|
323
|
+
from: message.from,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Route to message handler for incoming messages
|
|
330
|
+
if (this.messageHandler) {
|
|
331
|
+
this.messageHandler(agentId, message).catch((err) => {
|
|
332
|
+
console.error(`[a2a] Error handling message from ${agentId}:`, err);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Resolve a GopherHole task response - extract text from artifacts
|
|
338
|
+
*/
|
|
339
|
+
resolveGopherHoleTask(taskId, taskData) {
|
|
340
|
+
const pending = this.pendingRequests.get(taskId);
|
|
341
|
+
if (!pending) {
|
|
342
|
+
console.log(`[a2a] No pending request for taskId ${taskId}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
clearTimeout(pending.timeout);
|
|
346
|
+
this.pendingRequests.delete(taskId);
|
|
347
|
+
const status = taskData.status;
|
|
348
|
+
if (status?.state === 'failed') {
|
|
349
|
+
pending.reject(new Error(status.message ?? 'Task failed'));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Extract text from artifacts (GopherHole puts responses in artifacts, not history!)
|
|
353
|
+
let text = '';
|
|
354
|
+
const artifacts = taskData.artifacts;
|
|
355
|
+
if (artifacts?.length) {
|
|
356
|
+
// Get text from all artifacts
|
|
357
|
+
text = artifacts
|
|
358
|
+
.flatMap((artifact) => artifact.parts ?? [])
|
|
359
|
+
.filter((part) => part.kind === 'text' && part.text)
|
|
360
|
+
.map((part) => part.text)
|
|
361
|
+
.join('\n');
|
|
362
|
+
}
|
|
363
|
+
console.log(`[a2a] Resolved task ${taskId}: ${text.slice(0, 100)}...`);
|
|
364
|
+
pending.resolve({
|
|
365
|
+
text,
|
|
366
|
+
status: status?.state ?? 'completed',
|
|
367
|
+
from: 'gopherhole',
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Send a message to another agent and wait for response
|
|
372
|
+
*/
|
|
373
|
+
async sendMessage(agentId, text, contextId) {
|
|
374
|
+
const conn = this.connections.get(agentId);
|
|
375
|
+
if (!conn) {
|
|
376
|
+
throw new Error(`Agent ${agentId} not found`);
|
|
377
|
+
}
|
|
378
|
+
if (!conn.connected || !conn.ws) {
|
|
379
|
+
throw new Error(`Agent ${agentId} not connected`);
|
|
380
|
+
}
|
|
381
|
+
const taskId = uuidv4();
|
|
382
|
+
const timeoutMs = this.config.requestTimeoutMs ?? 60000; // 60s default
|
|
383
|
+
return new Promise((resolve, reject) => {
|
|
384
|
+
const timeout = setTimeout(() => {
|
|
385
|
+
this.pendingRequests.delete(taskId);
|
|
386
|
+
reject(new Error(`Request timeout after ${timeoutMs / 1000}s - agent may be offline`));
|
|
387
|
+
}, timeoutMs);
|
|
388
|
+
this.pendingRequests.set(taskId, {
|
|
389
|
+
taskId,
|
|
390
|
+
resolve,
|
|
391
|
+
reject,
|
|
392
|
+
timeout,
|
|
393
|
+
startedAt: Date.now(),
|
|
394
|
+
});
|
|
395
|
+
const msg = {
|
|
396
|
+
type: 'message',
|
|
397
|
+
taskId,
|
|
398
|
+
contextId,
|
|
399
|
+
from: this.agentId,
|
|
400
|
+
content: {
|
|
401
|
+
parts: [{ kind: 'text', text }],
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
conn.ws.send(JSON.stringify(msg));
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Send a response to an incoming message
|
|
409
|
+
*/
|
|
410
|
+
sendResponse(agentId, taskId, text, contextId) {
|
|
411
|
+
const conn = this.connections.get(agentId);
|
|
412
|
+
if (!conn?.ws || !conn.connected) {
|
|
413
|
+
console.warn(`[a2a] Cannot send response - ${agentId} not connected`);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const response = {
|
|
417
|
+
type: 'response',
|
|
418
|
+
taskId,
|
|
419
|
+
contextId,
|
|
420
|
+
from: this.agentId,
|
|
421
|
+
content: {
|
|
422
|
+
parts: [{ kind: 'text', text }],
|
|
423
|
+
},
|
|
424
|
+
status: 'completed',
|
|
425
|
+
};
|
|
426
|
+
conn.ws.send(JSON.stringify(response));
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Send a response to an agent via GopherHole (for replying to incoming messages)
|
|
430
|
+
*/
|
|
431
|
+
sendResponseViaGopherHole(targetAgentId, taskId, text, contextId) {
|
|
432
|
+
const gphConn = this.connections.get('gopherhole');
|
|
433
|
+
if (!gphConn?.connected || !gphConn.ws) {
|
|
434
|
+
console.warn(`[a2a] Cannot send GopherHole response - not connected`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
// GopherHole task_response format (completes the original task)
|
|
438
|
+
const msg = {
|
|
439
|
+
type: 'task_response',
|
|
440
|
+
taskId,
|
|
441
|
+
to: targetAgentId,
|
|
442
|
+
status: { state: 'completed' },
|
|
443
|
+
artifact: {
|
|
444
|
+
artifactId: `response-${Date.now()}`,
|
|
445
|
+
mimeType: 'text/plain',
|
|
446
|
+
parts: [{ kind: 'text', text }],
|
|
447
|
+
},
|
|
448
|
+
lastChunk: true,
|
|
449
|
+
};
|
|
450
|
+
console.log(`[a2a] Sending response to ${targetAgentId} via GopherHole: taskId=${taskId}, msg=${JSON.stringify(msg)}`);
|
|
451
|
+
gphConn.ws.send(JSON.stringify(msg));
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Send a message to a remote agent via GopherHole
|
|
455
|
+
* Note: targetAgentId must be the actual agent ID (e.g., "agent-70153299")
|
|
456
|
+
*/
|
|
457
|
+
async sendViaGopherHole(targetAgentId, text, contextId) {
|
|
458
|
+
const gphConn = this.connections.get('gopherhole');
|
|
459
|
+
if (!gphConn?.connected || !gphConn.ws) {
|
|
460
|
+
throw new Error('GopherHole not connected');
|
|
461
|
+
}
|
|
462
|
+
const taskId = uuidv4();
|
|
463
|
+
const timeoutMs = this.config.requestTimeoutMs ?? 60000; // 60s default
|
|
464
|
+
return new Promise((resolve, reject) => {
|
|
465
|
+
const timeout = setTimeout(() => {
|
|
466
|
+
this.pendingRequests.delete(taskId);
|
|
467
|
+
reject(new Error(`GopherHole request timeout after ${timeoutMs / 1000}s - target agent may be offline`));
|
|
468
|
+
}, timeoutMs);
|
|
469
|
+
this.pendingRequests.set(taskId, {
|
|
470
|
+
taskId,
|
|
471
|
+
resolve,
|
|
472
|
+
reject,
|
|
473
|
+
timeout,
|
|
474
|
+
startedAt: Date.now(),
|
|
475
|
+
});
|
|
476
|
+
// GopherHole WebSocket message format
|
|
477
|
+
const msg = {
|
|
478
|
+
type: 'message',
|
|
479
|
+
id: taskId,
|
|
480
|
+
to: targetAgentId,
|
|
481
|
+
payload: {
|
|
482
|
+
parts: [{ kind: 'text', text }],
|
|
483
|
+
...(contextId ? { contextId } : {}),
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
console.log(`[a2a] Sending to ${targetAgentId} via GopherHole: taskId=${taskId}`);
|
|
487
|
+
gphConn.ws.send(JSON.stringify(msg));
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Check if GopherHole is connected
|
|
492
|
+
*/
|
|
493
|
+
isGopherHoleConnected() {
|
|
494
|
+
return this.connections.get('gopherhole')?.connected ?? false;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* List connected agents
|
|
498
|
+
*/
|
|
499
|
+
listAgents() {
|
|
500
|
+
return Array.from(this.connections.values()).map((c) => ({
|
|
501
|
+
id: c.id,
|
|
502
|
+
name: c.name,
|
|
503
|
+
connected: c.connected,
|
|
504
|
+
}));
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Check if an agent is connected
|
|
508
|
+
*/
|
|
509
|
+
isConnected(agentId) {
|
|
510
|
+
const conn = this.connections.get(agentId);
|
|
511
|
+
return conn?.connected ?? false;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway WebSocket Client
|
|
3
|
+
* Sends messages to the Clawdbot gateway via its WebSocket protocol
|
|
4
|
+
*
|
|
5
|
+
* chat.send is non-blocking - it returns a runId immediately and
|
|
6
|
+
* streams the response via chat events. We accumulate deltas and
|
|
7
|
+
* resolve when we get state: "final".
|
|
8
|
+
*/
|
|
9
|
+
export declare function connectToGateway(port?: number): Promise<void>;
|
|
10
|
+
export declare function callGateway(method: string, params: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* Send a chat message and wait for the full response.
|
|
13
|
+
* chat.send is non-blocking - returns runId immediately, response streams via events.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sendChatMessage(sessionKey: string, message: string, timeoutMs?: number): Promise<{
|
|
16
|
+
text: string;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function disconnectFromGateway(): void;
|