gopherhole_openclaw_a2a 0.2.8 → 0.3.1
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/dist/src/channel.d.ts +1 -1
- package/dist/src/channel.js +18 -29
- package/dist/src/connection.d.ts +11 -1
- package/dist/src/connection.js +42 -8
- package/dist/src/types.d.ts +17 -21
- package/package.json +1 -1
- package/src/channel.ts +38 -57
- package/src/connection.ts +57 -8
- package/src/types.ts +21 -21
package/dist/src/channel.d.ts
CHANGED
package/dist/src/channel.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A2A Channel Plugin for
|
|
2
|
+
* A2A Channel Plugin for OpenClaw
|
|
3
3
|
* Enables communication with other AI agents via A2A protocol
|
|
4
4
|
*/
|
|
5
5
|
// Use minimal type imports - mostly self-contained
|
|
@@ -25,10 +25,9 @@ function resolveA2AAccount(opts) {
|
|
|
25
25
|
accountId,
|
|
26
26
|
name: config.agentName ?? 'A2A',
|
|
27
27
|
enabled: config.enabled ?? false,
|
|
28
|
-
configured: !!(config.bridgeUrl
|
|
29
|
-
agentId: config.agentId ?? '
|
|
28
|
+
configured: !!(config.bridgeUrl && config.apiKey),
|
|
29
|
+
agentId: config.agentId ?? 'openclaw',
|
|
30
30
|
bridgeUrl: config.bridgeUrl ?? null,
|
|
31
|
-
agents: config.agents ?? [],
|
|
32
31
|
config,
|
|
33
32
|
};
|
|
34
33
|
}
|
|
@@ -39,9 +38,9 @@ const meta = {
|
|
|
39
38
|
detailLabel: 'A2A Protocol',
|
|
40
39
|
docsPath: '/channels/a2a',
|
|
41
40
|
docsLabel: 'a2a',
|
|
42
|
-
blurb: 'Communicate with other AI agents via A2A protocol.',
|
|
41
|
+
blurb: 'Communicate with other AI agents via GopherHole A2A protocol.',
|
|
43
42
|
systemImage: 'bubble.left.and.bubble.right',
|
|
44
|
-
aliases: ['agent2agent'],
|
|
43
|
+
aliases: ['agent2agent', 'gopherhole'],
|
|
45
44
|
order: 200,
|
|
46
45
|
};
|
|
47
46
|
export const a2aPlugin = {
|
|
@@ -98,8 +97,8 @@ export const a2aPlugin = {
|
|
|
98
97
|
messaging: {
|
|
99
98
|
normalizeTarget: (target) => target?.trim() ?? '',
|
|
100
99
|
targetResolver: {
|
|
101
|
-
looksLikeId: (id) => /^[a-z0-9_
|
|
102
|
-
hint: '<agentId>',
|
|
100
|
+
looksLikeId: (id) => /^[a-z0-9_@-]+$/i.test(id),
|
|
101
|
+
hint: '<agentId> (e.g. @memory, @echo)',
|
|
103
102
|
},
|
|
104
103
|
formatTargetDisplay: ({ target }) => target ?? '',
|
|
105
104
|
},
|
|
@@ -108,7 +107,7 @@ export const a2aPlugin = {
|
|
|
108
107
|
applyAccountName: ({ cfg }) => cfg,
|
|
109
108
|
validateInput: ({ input }) => {
|
|
110
109
|
if (!input.httpUrl && !input.customArgs) {
|
|
111
|
-
return 'A2A requires --http-url (bridge URL) or
|
|
110
|
+
return 'A2A requires --http-url (bridge URL) or bridgeUrl + apiKey in config.';
|
|
112
111
|
}
|
|
113
112
|
return null;
|
|
114
113
|
},
|
|
@@ -179,16 +178,18 @@ export const a2aPlugin = {
|
|
|
179
178
|
lastError: snapshot.lastError ?? null,
|
|
180
179
|
}),
|
|
181
180
|
probeAccount: async () => ({ ok: connectionManager !== null }),
|
|
182
|
-
buildAccountSnapshot: ({ account, runtime }) => {
|
|
183
|
-
const
|
|
181
|
+
buildAccountSnapshot: async ({ account, runtime }) => {
|
|
182
|
+
const connectionStatus = connectionManager?.listAgents() ?? [];
|
|
183
|
+
const availableAgents = await connectionManager?.listAvailableAgents() ?? [];
|
|
184
184
|
return {
|
|
185
185
|
accountId: account.accountId,
|
|
186
186
|
name: account.name,
|
|
187
187
|
enabled: account.enabled,
|
|
188
188
|
configured: account.configured,
|
|
189
189
|
running: runtime?.running ?? false,
|
|
190
|
-
connected:
|
|
191
|
-
|
|
190
|
+
connected: connectionStatus.some((a) => a.connected),
|
|
191
|
+
hubStatus: connectionStatus,
|
|
192
|
+
availableAgents,
|
|
192
193
|
lastStartAt: runtime?.lastStartAt ?? null,
|
|
193
194
|
lastStopAt: runtime?.lastStopAt ?? null,
|
|
194
195
|
lastError: runtime?.lastError ?? null,
|
|
@@ -211,7 +212,7 @@ export const a2aPlugin = {
|
|
|
211
212
|
.join('\n') ?? '';
|
|
212
213
|
if (!text)
|
|
213
214
|
return;
|
|
214
|
-
// Route to
|
|
215
|
+
// Route to OpenClaw's reply pipeline via gateway JSON-RPC
|
|
215
216
|
try {
|
|
216
217
|
ctx.log?.info(`[a2a] Routing message from ${message.from}: "${text.slice(0, 100)}..."`);
|
|
217
218
|
// Use chat.send to route the message through the agent
|
|
@@ -219,26 +220,14 @@ export const a2aPlugin = {
|
|
|
219
220
|
const sessionKey = `agent:main:a2a:${message.from}`;
|
|
220
221
|
const response = await sendChatMessage(sessionKey, text);
|
|
221
222
|
ctx.log?.info(`[a2a] chat.send returned: ${response ? `text=${response.text?.slice(0, 50)}...` : 'null'}`);
|
|
222
|
-
// Send response back to the agent
|
|
223
|
+
// Send response back to the agent via GopherHole
|
|
223
224
|
if (response?.text) {
|
|
224
|
-
|
|
225
|
-
if (agentId === 'gopherhole' && message.from) {
|
|
226
|
-
connectionManager?.sendResponseViaGopherHole(message.from, message.taskId, response.text, message.contextId);
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
connectionManager?.sendResponse(agentId, message.taskId, response.text, message.contextId);
|
|
230
|
-
}
|
|
225
|
+
connectionManager?.sendResponseViaGopherHole(message.from, message.taskId, response.text, message.contextId);
|
|
231
226
|
}
|
|
232
227
|
}
|
|
233
228
|
catch (err) {
|
|
234
229
|
ctx.log?.error(`[a2a] Error handling message:`, err);
|
|
235
|
-
|
|
236
|
-
if (agentId === 'gopherhole' && message.from) {
|
|
237
|
-
connectionManager?.sendResponseViaGopherHole(message.from, message.taskId, `Error: ${err.message}`, message.contextId);
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
connectionManager?.sendResponse(agentId, message.taskId, `Error: ${err.message}`, message.contextId);
|
|
241
|
-
}
|
|
230
|
+
connectionManager?.sendResponseViaGopherHole(message.from, message.taskId, `Error: ${err.message}`, message.contextId);
|
|
242
231
|
}
|
|
243
232
|
}
|
|
244
233
|
});
|
package/dist/src/connection.d.ts
CHANGED
|
@@ -49,7 +49,17 @@ export declare class A2AConnectionManager {
|
|
|
49
49
|
*/
|
|
50
50
|
isGopherHoleConnected(): boolean;
|
|
51
51
|
/**
|
|
52
|
-
* List
|
|
52
|
+
* List available agents from GopherHole
|
|
53
|
+
* Fetches same-tenant agents + agents with approved access + public agents
|
|
54
|
+
*/
|
|
55
|
+
listAvailableAgents(): Promise<Array<{
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
description?: string;
|
|
59
|
+
accessType: 'same-tenant' | 'public' | 'granted';
|
|
60
|
+
}>>;
|
|
61
|
+
/**
|
|
62
|
+
* List connection status (for backward compatibility)
|
|
53
63
|
*/
|
|
54
64
|
listAgents(): Array<{
|
|
55
65
|
id: string;
|
package/dist/src/connection.js
CHANGED
|
@@ -11,30 +11,29 @@ export class A2AConnectionManager {
|
|
|
11
11
|
connected = false;
|
|
12
12
|
constructor(config) {
|
|
13
13
|
this.config = config;
|
|
14
|
-
this.agentId = config.agentId ?? '
|
|
14
|
+
this.agentId = config.agentId ?? 'openclaw';
|
|
15
15
|
}
|
|
16
16
|
setMessageHandler(handler) {
|
|
17
17
|
this.messageHandler = handler;
|
|
18
18
|
}
|
|
19
19
|
async start() {
|
|
20
|
-
// Connect to GopherHole if configured
|
|
21
|
-
if (this.config.
|
|
20
|
+
// Connect to GopherHole if configured (flat config: enabled + apiKey)
|
|
21
|
+
if (this.config.enabled && this.config.apiKey) {
|
|
22
22
|
await this.connectToGopherHole();
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
async connectToGopherHole() {
|
|
26
|
-
const
|
|
27
|
-
const hubUrl = gphConfig.hubUrl || 'wss://gopherhole.ai/ws';
|
|
26
|
+
const hubUrl = this.config.bridgeUrl || 'wss://gopherhole.ai/ws';
|
|
28
27
|
const timeoutMs = this.config.requestTimeoutMs ?? 180000;
|
|
29
28
|
this.gopherhole = new GopherHole({
|
|
30
|
-
apiKey:
|
|
29
|
+
apiKey: this.config.apiKey,
|
|
31
30
|
hubUrl,
|
|
32
31
|
autoReconnect: true,
|
|
33
32
|
reconnectDelay: this.config.reconnectIntervalMs ?? 5000,
|
|
34
33
|
maxReconnectAttempts: 20,
|
|
35
34
|
requestTimeout: timeoutMs,
|
|
36
35
|
messageTimeout: timeoutMs,
|
|
37
|
-
agentCard:
|
|
36
|
+
agentCard: this.config.agentCard ?? {
|
|
38
37
|
name: this.config.agentName ?? 'OpenClaw',
|
|
39
38
|
description: 'Personal AI assistant with tools, web search, browser control, and various skills',
|
|
40
39
|
version: '0.1.0',
|
|
@@ -213,7 +212,42 @@ export class A2AConnectionManager {
|
|
|
213
212
|
return this.connected && this.gopherhole?.connected === true;
|
|
214
213
|
}
|
|
215
214
|
/**
|
|
216
|
-
* List
|
|
215
|
+
* List available agents from GopherHole
|
|
216
|
+
* Fetches same-tenant agents + agents with approved access + public agents
|
|
217
|
+
*/
|
|
218
|
+
async listAvailableAgents() {
|
|
219
|
+
if (!this.config.apiKey) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
const hubUrl = this.config.bridgeUrl || 'wss://gopherhole.ai/ws';
|
|
223
|
+
// Convert wss:// to https:// for API calls
|
|
224
|
+
const apiBase = hubUrl.replace('wss://', 'https://').replace('/ws', '');
|
|
225
|
+
try {
|
|
226
|
+
const response = await fetch(`${apiBase}/api/agents/available`, {
|
|
227
|
+
headers: {
|
|
228
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
229
|
+
'Content-Type': 'application/json',
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
if (!response.ok) {
|
|
233
|
+
console.error(`[a2a] Failed to fetch agents: ${response.status}`);
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
const data = await response.json();
|
|
237
|
+
return data.agents.map(a => ({
|
|
238
|
+
id: a.id,
|
|
239
|
+
name: a.name,
|
|
240
|
+
description: a.description,
|
|
241
|
+
accessType: a.access_type,
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
console.error('[a2a] Error fetching available agents:', err.message);
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* List connection status (for backward compatibility)
|
|
217
251
|
*/
|
|
218
252
|
listAgents() {
|
|
219
253
|
const agents = [];
|
package/dist/src/types.d.ts
CHANGED
|
@@ -39,26 +39,27 @@ export interface A2AResponse {
|
|
|
39
39
|
status: string;
|
|
40
40
|
from?: string;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* A2A Channel Config (flat structure)
|
|
44
|
+
*
|
|
45
|
+
* Example:
|
|
46
|
+
* {
|
|
47
|
+
* "channels": {
|
|
48
|
+
* "a2a": {
|
|
49
|
+
* "enabled": true,
|
|
50
|
+
* "bridgeUrl": "wss://gopherhole.ai/ws",
|
|
51
|
+
* "apiKey": "gph_your_api_key"
|
|
52
|
+
* }
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
*/
|
|
42
56
|
export interface A2AChannelConfig {
|
|
43
57
|
enabled?: boolean;
|
|
58
|
+
bridgeUrl?: string;
|
|
59
|
+
apiKey?: string;
|
|
44
60
|
agentId?: string;
|
|
45
61
|
agentName?: string;
|
|
46
|
-
|
|
47
|
-
agents?: Array<{
|
|
48
|
-
id: string;
|
|
49
|
-
url: string;
|
|
50
|
-
name?: string;
|
|
51
|
-
}>;
|
|
52
|
-
gopherhole?: {
|
|
53
|
-
enabled?: boolean;
|
|
54
|
-
apiKey: string;
|
|
55
|
-
hubUrl?: string;
|
|
56
|
-
requestTimeoutMs?: number;
|
|
57
|
-
agentCard?: A2AAgentCard;
|
|
58
|
-
};
|
|
59
|
-
auth?: {
|
|
60
|
-
token?: string;
|
|
61
|
-
};
|
|
62
|
+
agentCard?: A2AAgentCard;
|
|
62
63
|
reconnectIntervalMs?: number;
|
|
63
64
|
requestTimeoutMs?: number;
|
|
64
65
|
}
|
|
@@ -69,10 +70,5 @@ export interface ResolvedA2AAccount {
|
|
|
69
70
|
configured: boolean;
|
|
70
71
|
agentId: string;
|
|
71
72
|
bridgeUrl: string | null;
|
|
72
|
-
agents: Array<{
|
|
73
|
-
id: string;
|
|
74
|
-
url: string;
|
|
75
|
-
name?: string;
|
|
76
|
-
}>;
|
|
77
73
|
config: A2AChannelConfig;
|
|
78
74
|
}
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A2A Channel Plugin for
|
|
2
|
+
* A2A Channel Plugin for OpenClaw
|
|
3
3
|
* Enables communication with other AI agents via A2A protocol
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
} from './types.js';
|
|
19
19
|
|
|
20
20
|
// Minimal runtime interface - what we actually need
|
|
21
|
-
interface
|
|
21
|
+
interface OpenClawRuntime {
|
|
22
22
|
handleInbound(params: {
|
|
23
23
|
channel: string;
|
|
24
24
|
chatId: string;
|
|
@@ -32,14 +32,14 @@ interface ClawdbotRuntime {
|
|
|
32
32
|
|
|
33
33
|
// Runtime state
|
|
34
34
|
let connectionManager: A2AConnectionManager | null = null;
|
|
35
|
-
let currentRuntime:
|
|
35
|
+
let currentRuntime: OpenClawRuntime | null = null;
|
|
36
36
|
|
|
37
37
|
export function setA2ARuntime(runtime: unknown): void {
|
|
38
|
-
currentRuntime = runtime as
|
|
38
|
+
currentRuntime = runtime as OpenClawRuntime;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
-
type
|
|
42
|
+
type OpenClawConfig = any;
|
|
43
43
|
|
|
44
44
|
// Minimal channel plugin interfaces (self-contained)
|
|
45
45
|
interface ChannelAccountSnapshot {
|
|
@@ -85,12 +85,12 @@ type ChannelPlugin<T = any> = {
|
|
|
85
85
|
};
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
-
function resolveA2AConfig(cfg:
|
|
88
|
+
function resolveA2AConfig(cfg: OpenClawConfig): A2AChannelConfig {
|
|
89
89
|
return cfg?.channels?.a2a ?? {};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
function resolveA2AAccount(opts: {
|
|
93
|
-
cfg:
|
|
93
|
+
cfg: OpenClawConfig;
|
|
94
94
|
accountId?: string;
|
|
95
95
|
}): ResolvedA2AAccount {
|
|
96
96
|
const config = resolveA2AConfig(opts.cfg);
|
|
@@ -100,10 +100,9 @@ function resolveA2AAccount(opts: {
|
|
|
100
100
|
accountId,
|
|
101
101
|
name: config.agentName ?? 'A2A',
|
|
102
102
|
enabled: config.enabled ?? false,
|
|
103
|
-
configured: !!(config.bridgeUrl
|
|
104
|
-
agentId: config.agentId ?? '
|
|
103
|
+
configured: !!(config.bridgeUrl && config.apiKey),
|
|
104
|
+
agentId: config.agentId ?? 'openclaw',
|
|
105
105
|
bridgeUrl: config.bridgeUrl ?? null,
|
|
106
|
-
agents: config.agents ?? [],
|
|
107
106
|
config,
|
|
108
107
|
};
|
|
109
108
|
}
|
|
@@ -115,9 +114,9 @@ const meta = {
|
|
|
115
114
|
detailLabel: 'A2A Protocol',
|
|
116
115
|
docsPath: '/channels/a2a',
|
|
117
116
|
docsLabel: 'a2a',
|
|
118
|
-
blurb: 'Communicate with other AI agents via A2A protocol.',
|
|
117
|
+
blurb: 'Communicate with other AI agents via GopherHole A2A protocol.',
|
|
119
118
|
systemImage: 'bubble.left.and.bubble.right',
|
|
120
|
-
aliases: ['agent2agent'],
|
|
119
|
+
aliases: ['agent2agent', 'gopherhole'],
|
|
121
120
|
order: 200,
|
|
122
121
|
};
|
|
123
122
|
|
|
@@ -136,10 +135,10 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
|
|
|
136
135
|
config: {
|
|
137
136
|
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
|
138
137
|
resolveAccount: (cfg, accountId) =>
|
|
139
|
-
resolveA2AAccount({ cfg: cfg as
|
|
138
|
+
resolveA2AAccount({ cfg: cfg as OpenClawConfig, accountId }),
|
|
140
139
|
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
141
140
|
setAccountEnabled: ({ cfg, enabled }) => {
|
|
142
|
-
const next = cfg as
|
|
141
|
+
const next = cfg as OpenClawConfig;
|
|
143
142
|
return {
|
|
144
143
|
...next,
|
|
145
144
|
channels: {
|
|
@@ -149,9 +148,9 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
|
|
|
149
148
|
enabled,
|
|
150
149
|
},
|
|
151
150
|
},
|
|
152
|
-
} as
|
|
151
|
+
} as OpenClawConfig;
|
|
153
152
|
},
|
|
154
|
-
deleteAccount: ({ cfg }) => cfg as
|
|
153
|
+
deleteAccount: ({ cfg }) => cfg as OpenClawConfig,
|
|
155
154
|
isConfigured: (account) => account.configured,
|
|
156
155
|
describeAccount: (account): ChannelAccountSnapshot => ({
|
|
157
156
|
accountId: account.accountId,
|
|
@@ -176,22 +175,22 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
|
|
|
176
175
|
messaging: {
|
|
177
176
|
normalizeTarget: (target) => target?.trim() ?? '',
|
|
178
177
|
targetResolver: {
|
|
179
|
-
looksLikeId: (id) => /^[a-z0-9_
|
|
180
|
-
hint: '<agentId>',
|
|
178
|
+
looksLikeId: (id) => /^[a-z0-9_@-]+$/i.test(id),
|
|
179
|
+
hint: '<agentId> (e.g. @memory, @echo)',
|
|
181
180
|
},
|
|
182
181
|
formatTargetDisplay: ({ target }) => target ?? '',
|
|
183
182
|
},
|
|
184
183
|
setup: {
|
|
185
184
|
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
186
|
-
applyAccountName: ({ cfg }) => cfg as
|
|
185
|
+
applyAccountName: ({ cfg }) => cfg as OpenClawConfig,
|
|
187
186
|
validateInput: ({ input }) => {
|
|
188
187
|
if (!input.httpUrl && !input.customArgs) {
|
|
189
|
-
return 'A2A requires --http-url (bridge URL) or
|
|
188
|
+
return 'A2A requires --http-url (bridge URL) or bridgeUrl + apiKey in config.';
|
|
190
189
|
}
|
|
191
190
|
return null;
|
|
192
191
|
},
|
|
193
192
|
applyAccountConfig: ({ cfg, input }) => {
|
|
194
|
-
const next = cfg as
|
|
193
|
+
const next = cfg as OpenClawConfig;
|
|
195
194
|
return {
|
|
196
195
|
...next,
|
|
197
196
|
channels: {
|
|
@@ -202,7 +201,7 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
|
|
|
202
201
|
...(input.httpUrl ? { bridgeUrl: input.httpUrl } : {}),
|
|
203
202
|
},
|
|
204
203
|
},
|
|
205
|
-
} as
|
|
204
|
+
} as OpenClawConfig;
|
|
206
205
|
},
|
|
207
206
|
},
|
|
208
207
|
outbound: {
|
|
@@ -256,16 +255,18 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
|
|
|
256
255
|
lastError: snapshot.lastError ?? null,
|
|
257
256
|
}),
|
|
258
257
|
probeAccount: async () => ({ ok: connectionManager !== null }),
|
|
259
|
-
buildAccountSnapshot: ({ account, runtime }) => {
|
|
260
|
-
const
|
|
258
|
+
buildAccountSnapshot: async ({ account, runtime }) => {
|
|
259
|
+
const connectionStatus = connectionManager?.listAgents() ?? [];
|
|
260
|
+
const availableAgents = await connectionManager?.listAvailableAgents() ?? [];
|
|
261
261
|
return {
|
|
262
262
|
accountId: account.accountId,
|
|
263
263
|
name: account.name,
|
|
264
264
|
enabled: account.enabled,
|
|
265
265
|
configured: account.configured,
|
|
266
266
|
running: runtime?.running ?? false,
|
|
267
|
-
connected:
|
|
268
|
-
|
|
267
|
+
connected: connectionStatus.some((a) => a.connected),
|
|
268
|
+
hubStatus: connectionStatus,
|
|
269
|
+
availableAgents,
|
|
269
270
|
lastStartAt: runtime?.lastStartAt ?? null,
|
|
270
271
|
lastStopAt: runtime?.lastStopAt ?? null,
|
|
271
272
|
lastError: runtime?.lastError ?? null,
|
|
@@ -292,7 +293,7 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
|
|
|
292
293
|
|
|
293
294
|
if (!text) return;
|
|
294
295
|
|
|
295
|
-
// Route to
|
|
296
|
+
// Route to OpenClaw's reply pipeline via gateway JSON-RPC
|
|
296
297
|
try {
|
|
297
298
|
ctx.log?.info(`[a2a] Routing message from ${message.from}: "${text.slice(0, 100)}..."`);
|
|
298
299
|
|
|
@@ -303,43 +304,23 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
|
|
|
303
304
|
|
|
304
305
|
ctx.log?.info(`[a2a] chat.send returned: ${response ? `text=${response.text?.slice(0, 50)}...` : 'null'}`);
|
|
305
306
|
|
|
306
|
-
// Send response back to the agent
|
|
307
|
+
// Send response back to the agent via GopherHole
|
|
307
308
|
if (response?.text) {
|
|
308
|
-
// If message came via GopherHole, route response back through it
|
|
309
|
-
if (agentId === 'gopherhole' && message.from) {
|
|
310
|
-
connectionManager?.sendResponseViaGopherHole(
|
|
311
|
-
message.from,
|
|
312
|
-
message.taskId,
|
|
313
|
-
response.text,
|
|
314
|
-
message.contextId
|
|
315
|
-
);
|
|
316
|
-
} else {
|
|
317
|
-
connectionManager?.sendResponse(
|
|
318
|
-
agentId,
|
|
319
|
-
message.taskId,
|
|
320
|
-
response.text,
|
|
321
|
-
message.contextId
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
} catch (err) {
|
|
326
|
-
ctx.log?.error(`[a2a] Error handling message:`, err);
|
|
327
|
-
// If message came via GopherHole, route error back through it
|
|
328
|
-
if (agentId === 'gopherhole' && message.from) {
|
|
329
309
|
connectionManager?.sendResponseViaGopherHole(
|
|
330
310
|
message.from,
|
|
331
311
|
message.taskId,
|
|
332
|
-
|
|
333
|
-
message.contextId
|
|
334
|
-
);
|
|
335
|
-
} else {
|
|
336
|
-
connectionManager?.sendResponse(
|
|
337
|
-
agentId,
|
|
338
|
-
message.taskId,
|
|
339
|
-
`Error: ${(err as Error).message}`,
|
|
312
|
+
response.text,
|
|
340
313
|
message.contextId
|
|
341
314
|
);
|
|
342
315
|
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
ctx.log?.error(`[a2a] Error handling message:`, err);
|
|
318
|
+
connectionManager?.sendResponseViaGopherHole(
|
|
319
|
+
message.from,
|
|
320
|
+
message.taskId,
|
|
321
|
+
`Error: ${(err as Error).message}`,
|
|
322
|
+
message.contextId
|
|
323
|
+
);
|
|
343
324
|
}
|
|
344
325
|
}
|
|
345
326
|
});
|
package/src/connection.ts
CHANGED
|
@@ -22,7 +22,7 @@ export class A2AConnectionManager {
|
|
|
22
22
|
|
|
23
23
|
constructor(config: A2AChannelConfig) {
|
|
24
24
|
this.config = config;
|
|
25
|
-
this.agentId = config.agentId ?? '
|
|
25
|
+
this.agentId = config.agentId ?? 'openclaw';
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
setMessageHandler(handler: MessageHandler): void {
|
|
@@ -30,26 +30,25 @@ export class A2AConnectionManager {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
async start(): Promise<void> {
|
|
33
|
-
// Connect to GopherHole if configured
|
|
34
|
-
if (this.config.
|
|
33
|
+
// Connect to GopherHole if configured (flat config: enabled + apiKey)
|
|
34
|
+
if (this.config.enabled && this.config.apiKey) {
|
|
35
35
|
await this.connectToGopherHole();
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
private async connectToGopherHole(): Promise<void> {
|
|
40
|
-
const
|
|
41
|
-
const hubUrl = gphConfig.hubUrl || 'wss://gopherhole.ai/ws';
|
|
40
|
+
const hubUrl = this.config.bridgeUrl || 'wss://gopherhole.ai/ws';
|
|
42
41
|
const timeoutMs = this.config.requestTimeoutMs ?? 180000;
|
|
43
42
|
|
|
44
43
|
this.gopherhole = new GopherHole({
|
|
45
|
-
apiKey:
|
|
44
|
+
apiKey: this.config.apiKey!,
|
|
46
45
|
hubUrl,
|
|
47
46
|
autoReconnect: true,
|
|
48
47
|
reconnectDelay: this.config.reconnectIntervalMs ?? 5000,
|
|
49
48
|
maxReconnectAttempts: 20,
|
|
50
49
|
requestTimeout: timeoutMs,
|
|
51
50
|
messageTimeout: timeoutMs,
|
|
52
|
-
agentCard:
|
|
51
|
+
agentCard: this.config.agentCard ?? {
|
|
53
52
|
name: this.config.agentName ?? 'OpenClaw',
|
|
54
53
|
description: 'Personal AI assistant with tools, web search, browser control, and various skills',
|
|
55
54
|
version: '0.1.0',
|
|
@@ -271,7 +270,57 @@ export class A2AConnectionManager {
|
|
|
271
270
|
}
|
|
272
271
|
|
|
273
272
|
/**
|
|
274
|
-
* List
|
|
273
|
+
* List available agents from GopherHole
|
|
274
|
+
* Fetches same-tenant agents + agents with approved access + public agents
|
|
275
|
+
*/
|
|
276
|
+
async listAvailableAgents(): Promise<Array<{
|
|
277
|
+
id: string;
|
|
278
|
+
name: string;
|
|
279
|
+
description?: string;
|
|
280
|
+
accessType: 'same-tenant' | 'public' | 'granted';
|
|
281
|
+
}>> {
|
|
282
|
+
if (!this.config.apiKey) {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const hubUrl = this.config.bridgeUrl || 'wss://gopherhole.ai/ws';
|
|
287
|
+
// Convert wss:// to https:// for API calls
|
|
288
|
+
const apiBase = hubUrl.replace('wss://', 'https://').replace('/ws', '');
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const response = await fetch(`${apiBase}/api/agents/available`, {
|
|
292
|
+
headers: {
|
|
293
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
294
|
+
'Content-Type': 'application/json',
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (!response.ok) {
|
|
299
|
+
console.error(`[a2a] Failed to fetch agents: ${response.status}`);
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const data = await response.json() as { agents: Array<{
|
|
304
|
+
id: string;
|
|
305
|
+
name: string;
|
|
306
|
+
description?: string;
|
|
307
|
+
access_type: string;
|
|
308
|
+
}> };
|
|
309
|
+
|
|
310
|
+
return data.agents.map(a => ({
|
|
311
|
+
id: a.id,
|
|
312
|
+
name: a.name,
|
|
313
|
+
description: a.description,
|
|
314
|
+
accessType: a.access_type as 'same-tenant' | 'public' | 'granted',
|
|
315
|
+
}));
|
|
316
|
+
} catch (err) {
|
|
317
|
+
console.error('[a2a] Error fetching available agents:', (err as Error).message);
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* List connection status (for backward compatibility)
|
|
275
324
|
*/
|
|
276
325
|
listAgents(): Array<{ id: string; name: string; connected: boolean }> {
|
|
277
326
|
const agents: Array<{ id: string; name: string; connected: boolean }> = [];
|
package/src/types.ts
CHANGED
|
@@ -39,28 +39,29 @@ export interface A2AResponse {
|
|
|
39
39
|
from?: string;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* A2A Channel Config (flat structure)
|
|
44
|
+
*
|
|
45
|
+
* Example:
|
|
46
|
+
* {
|
|
47
|
+
* "channels": {
|
|
48
|
+
* "a2a": {
|
|
49
|
+
* "enabled": true,
|
|
50
|
+
* "bridgeUrl": "wss://gopherhole.ai/ws",
|
|
51
|
+
* "apiKey": "gph_your_api_key"
|
|
52
|
+
* }
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
*/
|
|
42
56
|
export interface A2AChannelConfig {
|
|
43
57
|
enabled?: boolean;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}>;
|
|
52
|
-
gopherhole?: { // GopherHole Agent Hub connection
|
|
53
|
-
enabled?: boolean;
|
|
54
|
-
apiKey: string;
|
|
55
|
-
hubUrl?: string; // Default: wss://gopherhole.ai/ws
|
|
56
|
-
requestTimeoutMs?: number;
|
|
57
|
-
agentCard?: A2AAgentCard; // Custom agent card (overrides defaults)
|
|
58
|
-
};
|
|
59
|
-
auth?: {
|
|
60
|
-
token?: string;
|
|
61
|
-
};
|
|
62
|
-
reconnectIntervalMs?: number;
|
|
63
|
-
requestTimeoutMs?: number;
|
|
58
|
+
bridgeUrl?: string; // WebSocket URL (default: wss://gopherhole.ai/ws)
|
|
59
|
+
apiKey?: string; // GopherHole API key (gph_...)
|
|
60
|
+
agentId?: string; // Our agent ID (default: "openclaw")
|
|
61
|
+
agentName?: string; // Display name for agent card
|
|
62
|
+
agentCard?: A2AAgentCard; // Custom agent card (overrides defaults)
|
|
63
|
+
reconnectIntervalMs?: number; // Reconnect delay (default: 5000)
|
|
64
|
+
requestTimeoutMs?: number; // Request timeout (default: 180000)
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export interface ResolvedA2AAccount {
|
|
@@ -70,6 +71,5 @@ export interface ResolvedA2AAccount {
|
|
|
70
71
|
configured: boolean;
|
|
71
72
|
agentId: string;
|
|
72
73
|
bridgeUrl: string | null;
|
|
73
|
-
agents: Array<{ id: string; url: string; name?: string }>;
|
|
74
74
|
config: A2AChannelConfig;
|
|
75
75
|
}
|