gopherhole_openclaw_a2a 0.3.14 → 0.4.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/src/connection.ts DELETED
@@ -1,533 +0,0 @@
1
- /**
2
- * A2A Connection Manager
3
- * Uses @gopherhole/sdk for GopherHole hub connectivity
4
- */
5
-
6
- import { GopherHole, Message, getTaskResponseText } from '@gopherhole/sdk';
7
- import { v4 as uuidv4 } from 'uuid';
8
- import type {
9
- A2AMessage,
10
- A2AResponse,
11
- A2AChannelConfig,
12
- } from './types.js';
13
-
14
- export type MessageHandler = (agentId: string, message: A2AMessage) => Promise<void>;
15
-
16
- export class A2AConnectionManager {
17
- private gopherhole: GopherHole | null = null;
18
- private messageHandler: MessageHandler | null = null;
19
- private config: A2AChannelConfig;
20
- private agentId: string;
21
- private connected = false;
22
-
23
- constructor(config: A2AChannelConfig) {
24
- this.config = config;
25
- this.agentId = config.agentId ?? 'openclaw';
26
- }
27
-
28
- setMessageHandler(handler: MessageHandler): void {
29
- this.messageHandler = handler;
30
- }
31
-
32
- async start(): Promise<void> {
33
- // Connect to GopherHole if configured (flat config: enabled + apiKey)
34
- if (this.config.enabled && this.config.apiKey) {
35
- await this.connectToGopherHole();
36
- }
37
- }
38
-
39
- private async connectToGopherHole(): Promise<void> {
40
- const hubUrl = this.config.bridgeUrl || 'wss://hub.gopherhole.ai/ws';
41
- const timeoutMs = this.config.requestTimeoutMs ?? 180000;
42
-
43
- this.gopherhole = new GopherHole({
44
- apiKey: this.config.apiKey!,
45
- hubUrl,
46
- autoReconnect: true,
47
- reconnectDelay: this.config.reconnectIntervalMs ?? 5000,
48
- maxReconnectDelay: 300000, // 5 min cap on backoff
49
- // maxReconnectAttempts defaults to 0 (infinite) in SDK
50
- requestTimeout: timeoutMs,
51
- messageTimeout: timeoutMs,
52
- agentCard: this.config.agentCard ?? {
53
- name: this.config.agentName ?? 'OpenClaw',
54
- description: 'Personal AI assistant with tools, web search, browser control, and various skills',
55
- version: '0.1.0',
56
- skills: [
57
- {
58
- id: 'chat',
59
- name: 'Chat',
60
- description: 'General conversation and Q&A',
61
- tags: ['conversation', 'assistant'],
62
- inputModes: ['text/plain'],
63
- outputModes: ['text/plain', 'text/markdown'],
64
- },
65
- {
66
- id: 'web-search',
67
- name: 'Web Search',
68
- description: 'Search the web and summarize results',
69
- tags: ['search', 'research', 'web'],
70
- inputModes: ['text/plain'],
71
- outputModes: ['text/plain', 'text/markdown'],
72
- },
73
- {
74
- id: 'coding',
75
- name: 'Coding',
76
- description: 'Write, review, and debug code',
77
- tags: ['code', 'programming', 'development'],
78
- inputModes: ['text/plain'],
79
- outputModes: ['text/plain', 'text/markdown'],
80
- },
81
- {
82
- id: 'files',
83
- name: 'File Operations',
84
- description: 'Read, write, and manage files',
85
- tags: ['files', 'documents'],
86
- inputModes: ['text/plain', 'application/pdf', 'image/*'],
87
- outputModes: ['text/plain', 'text/markdown'],
88
- },
89
- ],
90
- },
91
- });
92
-
93
- // Set up event handlers
94
- this.gopherhole.on('connect', () => {
95
- this.connected = true;
96
- console.log('[a2a] Connected to GopherHole Hub via SDK');
97
- });
98
-
99
- this.gopherhole.on('disconnect', (reason) => {
100
- this.connected = false;
101
- console.log(`[a2a] Disconnected from GopherHole: ${reason}`);
102
- });
103
-
104
- this.gopherhole.on('reconnecting', ({ attempt, delayMs }) => {
105
- console.log(`[a2a] Reconnecting to GopherHole (attempt ${attempt}, waiting ${delayMs}ms)...`);
106
- });
107
-
108
- this.gopherhole.on('error', (error) => {
109
- console.error('[a2a] GopherHole SDK error:', error.message);
110
- });
111
-
112
- this.gopherhole.on('message', (message: Message) => {
113
- this.handleIncomingMessage(message);
114
- });
115
-
116
- // Handle system messages (rate limits, budget alerts, announcements)
117
- this.gopherhole.on('system', (message) => {
118
- this.handleSystemMessage(message);
119
- });
120
-
121
- // Connect
122
- try {
123
- await this.gopherhole.connect();
124
- console.log(`[a2a] GopherHole SDK connected, agent ID: ${this.gopherhole.id}`);
125
- } catch (err) {
126
- console.error('[a2a] Failed to connect to GopherHole:', (err as Error).message);
127
- throw err;
128
- }
129
- }
130
-
131
- private handleIncomingMessage(message: Message): void {
132
- if (!this.messageHandler) return;
133
-
134
- console.log(`[a2a] Received message from ${message.from}, taskId=${message.taskId}`);
135
- console.log(`[a2a] Raw message payload:`, JSON.stringify(message.payload, null, 2).slice(0, 500));
136
-
137
- // Validate taskId - critical for response routing
138
- if (!message.taskId) {
139
- console.error(`[a2a] WARNING: No taskId in incoming message! Response relay will fail.`);
140
- console.error(`[a2a] Full message object:`, JSON.stringify(message, null, 2));
141
- }
142
-
143
- // Convert SDK message to our A2AMessage format
144
- const a2aMsg: A2AMessage = {
145
- type: 'message',
146
- taskId: message.taskId || `gph-${Date.now()}`,
147
- from: message.from,
148
- content: {
149
- parts: message.payload.parts.map(p => ({
150
- kind: p.kind,
151
- text: p.text,
152
- data: p.data,
153
- mimeType: p.mimeType,
154
- })),
155
- },
156
- };
157
-
158
- console.log(`[a2a] Dispatching to messageHandler with taskId=${a2aMsg.taskId}`);
159
-
160
- this.messageHandler('gopherhole', a2aMsg).catch((err) => {
161
- console.error('[a2a] Error handling incoming message:', err);
162
- });
163
- }
164
-
165
- /**
166
- * Handle system messages from GopherHole Hub
167
- * These include spending alerts, account alerts, notices, maintenance, etc.
168
- */
169
- private handleSystemMessage(message: Message): void {
170
- const kind = message.metadata?.kind;
171
- const text = message.payload.parts.find(p => p.kind === 'text')?.text || '';
172
- const data = message.metadata?.data;
173
-
174
- // Log all system messages
175
- console.log(`[a2a] System message (${kind || 'unknown'}): ${text}`);
176
-
177
- // Handle specific message kinds
178
- switch (kind) {
179
- case 'spending_alert':
180
- console.warn(`[a2a] 💰 Spending alert: ${text}`);
181
- if (data) {
182
- console.warn(`[a2a] Spending data:`, JSON.stringify(data));
183
- }
184
- break;
185
-
186
- case 'account_alert':
187
- console.warn(`[a2a] ⚠️ Account alert: ${text}`);
188
- break;
189
-
190
- case 'system_notice':
191
- console.log(`[a2a] 📢 System notice: ${text}`);
192
- break;
193
-
194
- case 'maintenance':
195
- console.warn(`[a2a] 🔧 Maintenance notice: ${text}`);
196
- break;
197
-
198
- default:
199
- // Log but don't warn for unknown types
200
- if (kind) {
201
- console.log(`[a2a] System message "${kind}": ${text}`);
202
- }
203
- }
204
- }
205
-
206
- async stop(): Promise<void> {
207
- if (this.gopherhole) {
208
- this.gopherhole.disconnect();
209
- this.gopherhole = null;
210
- }
211
- this.connected = false;
212
- }
213
-
214
- /**
215
- * Send a message to another agent via GopherHole and wait for response
216
- */
217
- async sendMessage(
218
- targetAgentId: string,
219
- text: string,
220
- _contextId?: string
221
- ): Promise<A2AResponse> {
222
- return this.sendPartsViaGopherHole(targetAgentId, [{ kind: 'text', text }]);
223
- }
224
-
225
- /**
226
- * Send a multi-part message via GopherHole hub
227
- * Supports text, images, and other MIME types
228
- */
229
- async sendPartsViaGopherHole(
230
- targetAgentId: string,
231
- parts: Array<{ kind: string; text?: string; data?: string; mimeType?: string }>,
232
- contextId?: string
233
- ): Promise<A2AResponse> {
234
- if (!this.gopherhole || !this.connected) {
235
- throw new Error('GopherHole not connected');
236
- }
237
-
238
- console.log(`[a2a] Sending to ${targetAgentId} via SDK, parts=${parts.length}`);
239
-
240
- try {
241
- // Use SDK's send method with polling for completion
242
- const task = await this.gopherhole.send(
243
- targetAgentId,
244
- {
245
- role: 'agent',
246
- parts: parts.map(p => ({
247
- kind: p.kind as 'text' | 'file' | 'data',
248
- text: p.text,
249
- data: p.data,
250
- mimeType: p.mimeType,
251
- })),
252
- },
253
- { contextId }
254
- );
255
-
256
- // Wait for task completion
257
- const completedTask = await this.gopherhole.waitForTask(task.id, {
258
- pollIntervalMs: 1000,
259
- maxWaitMs: this.config.requestTimeoutMs ?? 180000,
260
- });
261
-
262
- if (completedTask.status.state === 'failed') {
263
- throw new Error(completedTask.status.message ?? 'Task failed');
264
- }
265
-
266
- // Extract response text using SDK helper
267
- const responseText = getTaskResponseText(completedTask);
268
-
269
- console.log(`[a2a] Got response from ${targetAgentId}: ${responseText.slice(0, 100)}...`);
270
-
271
- return {
272
- text: responseText,
273
- status: completedTask.status.state,
274
- from: targetAgentId,
275
- };
276
- } catch (err) {
277
- console.error(`[a2a] Failed to send to ${targetAgentId}:`, (err as Error).message);
278
- throw err;
279
- }
280
- }
281
-
282
- /**
283
- * Send a response to an incoming message via GopherHole
284
- * Uses SDK's respond() method to complete the original task
285
- */
286
- sendResponseViaGopherHole(
287
- _targetAgentId: string,
288
- taskId: string,
289
- text: string,
290
- _contextId?: string
291
- ): void {
292
- if (!this.gopherhole || !this.connected) {
293
- console.error('[a2a] Cannot send response - GopherHole not connected');
294
- return;
295
- }
296
-
297
- // Validate taskId
298
- if (!taskId) {
299
- console.error('[a2a] Cannot respond - taskId is null/undefined!');
300
- return;
301
- }
302
-
303
- if (taskId.startsWith('gph-')) {
304
- console.error(`[a2a] Cannot respond - taskId "${taskId}" is a fallback ID (not a real task). Response will be lost!`);
305
- return;
306
- }
307
-
308
- console.log(`[a2a] Responding to taskId=${taskId}: "${text.slice(0, 200)}..." (total ${text.length} chars)`);
309
-
310
- try {
311
- // Use SDK's respond method to complete the task
312
- this.gopherhole.respond(taskId, text);
313
- console.log(`[a2a] respond() called successfully for taskId=${taskId}`);
314
- } catch (err) {
315
- console.error('[a2a] Failed to send response:', (err as Error).message);
316
- console.error('[a2a] Error details:', err);
317
- }
318
- }
319
-
320
- /**
321
- * Legacy alias for sendPartsViaGopherHole with text-only
322
- */
323
- async sendViaGopherHole(
324
- targetAgentId: string,
325
- text: string,
326
- contextId?: string
327
- ): Promise<A2AResponse> {
328
- return this.sendPartsViaGopherHole(targetAgentId, [{ kind: 'text', text }], contextId);
329
- }
330
-
331
- /**
332
- * Legacy sendResponse (routes to GopherHole)
333
- */
334
- sendResponse(agentId: string, taskId: string, text: string, contextId?: string): void {
335
- this.sendResponseViaGopherHole(agentId, taskId, text, contextId);
336
- }
337
-
338
- /**
339
- * Check if GopherHole is connected
340
- */
341
- isGopherHoleConnected(): boolean {
342
- return this.connected && this.gopherhole?.connected === true;
343
- }
344
-
345
- /**
346
- * Make an A2A JSON-RPC call
347
- */
348
- private async a2aRpc<T>(method: string, params?: Record<string, unknown>): Promise<T | null> {
349
- if (!this.config.apiKey) {
350
- return null;
351
- }
352
-
353
- const hubUrl = this.config.bridgeUrl || 'wss://hub.gopherhole.ai/ws';
354
- const apiBase = hubUrl.replace('wss://', 'https://').replace('/ws', '');
355
-
356
- try {
357
- const response = await fetch(`${apiBase}/a2a`, {
358
- method: 'POST',
359
- headers: {
360
- 'Authorization': `Bearer ${this.config.apiKey}`,
361
- 'Content-Type': 'application/json',
362
- },
363
- body: JSON.stringify({
364
- jsonrpc: '2.0',
365
- method,
366
- params: params || {},
367
- id: Date.now(),
368
- }),
369
- });
370
-
371
- if (!response.ok) {
372
- console.error(`[a2a] RPC failed: ${response.status}`);
373
- return null;
374
- }
375
-
376
- const data = await response.json() as { result?: T; error?: { message: string } };
377
- if (data.error) {
378
- console.error(`[a2a] RPC error: ${data.error.message}`);
379
- return null;
380
- }
381
-
382
- return data.result || null;
383
- } catch (err) {
384
- console.error('[a2a] RPC error:', (err as Error).message);
385
- return null;
386
- }
387
- }
388
-
389
- /**
390
- * List available agents from GopherHole (agents you have access to)
391
- */
392
- async listAvailableAgents(options?: { query?: string; public?: boolean }): Promise<Array<{
393
- id: string;
394
- name: string;
395
- description?: string;
396
- verified?: boolean;
397
- accessType: 'same-tenant' | 'public' | 'granted';
398
- }>> {
399
- const result = await this.a2aRpc<{ agents: Array<{
400
- id: string;
401
- name: string;
402
- description?: string;
403
- verified?: boolean;
404
- accessType: string;
405
- }> }>('x-gopherhole/agents.available', options);
406
-
407
- if (!result?.agents) {
408
- return [];
409
- }
410
-
411
- return result.agents.map(a => ({
412
- id: a.id,
413
- name: a.name,
414
- description: a.description,
415
- verified: a.verified,
416
- accessType: a.accessType as 'same-tenant' | 'public' | 'granted',
417
- }));
418
- }
419
-
420
- /**
421
- * Discover public agents in the marketplace
422
- */
423
- async discoverAgents(options?: {
424
- query?: string;
425
- category?: string;
426
- tag?: string;
427
- skillTag?: string;
428
- contentMode?: string;
429
- sort?: string;
430
- owner?: string; // Filter by organization/tenant name
431
- verified?: boolean; // Only show agents from verified organizations
432
- limit?: number;
433
- offset?: number;
434
- scope?: string;
435
- }): Promise<Array<{
436
- id: string;
437
- name: string;
438
- description?: string;
439
- verified?: boolean;
440
- tenantName?: string;
441
- avgRating?: number;
442
- }>> {
443
- const result = await this.a2aRpc<{ agents: Array<{
444
- id: string;
445
- name: string;
446
- description?: string;
447
- verified?: boolean;
448
- tenantName?: string;
449
- avgRating?: number;
450
- }> }>('x-gopherhole/agents.discover', options);
451
-
452
- return result?.agents || [];
453
- }
454
-
455
- /**
456
- * Discover agents near a geographic location
457
- */
458
- async discoverNearby(options: {
459
- lat: number;
460
- lng: number;
461
- radius?: number;
462
- tag?: string;
463
- category?: string;
464
- limit?: number;
465
- offset?: number;
466
- }): Promise<Array<{
467
- id: string;
468
- name: string;
469
- description?: string;
470
- verified?: boolean;
471
- tenantName?: string;
472
- avgRating?: number;
473
- location?: {
474
- name: string;
475
- lat: number;
476
- lng: number;
477
- country: string;
478
- };
479
- distance?: number;
480
- }>> {
481
- const result = await this.a2aRpc<{ agents: Array<{
482
- id: string;
483
- name: string;
484
- description?: string;
485
- verified?: boolean;
486
- tenantName?: string;
487
- avgRating?: number;
488
- location?: {
489
- name: string;
490
- lat: number;
491
- lng: number;
492
- country: string;
493
- };
494
- distance?: number;
495
- }> }>('x-gopherhole/agents.discover.nearby', options);
496
-
497
- return result?.agents || [];
498
- }
499
-
500
- /**
501
- * List connection status (for backward compatibility)
502
- */
503
- listAgents(): Array<{ id: string; name: string; connected: boolean }> {
504
- const agents: Array<{ id: string; name: string; connected: boolean }> = [];
505
-
506
- if (this.gopherhole) {
507
- agents.push({
508
- id: 'gopherhole',
509
- name: 'GopherHole Hub',
510
- connected: this.connected,
511
- });
512
- }
513
-
514
- return agents;
515
- }
516
-
517
- /**
518
- * Check if an agent is connected
519
- */
520
- isConnected(agentId: string): boolean {
521
- if (agentId === 'gopherhole') {
522
- return this.isGopherHoleConnected();
523
- }
524
- return false;
525
- }
526
-
527
- /**
528
- * Get the underlying GopherHole SDK instance (for advanced usage)
529
- */
530
- getSDK(): GopherHole | null {
531
- return this.gopherhole;
532
- }
533
- }