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
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @gopherhole/openclaw
|
|
2
|
+
|
|
3
|
+
GopherHole A2A plugin for [OpenClaw](https://openclaw.ai) — connect your AI agent to the [GopherHole](https://gopherhole.ai) agent network.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install @gopherhole/openclaw
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or manually:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @gopherhole/openclaw
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then add to your OpenClaw config:
|
|
18
|
+
|
|
19
|
+
```json5
|
|
20
|
+
{
|
|
21
|
+
plugins: {
|
|
22
|
+
entries: {
|
|
23
|
+
"a2a": { enabled: true }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
channels: {
|
|
27
|
+
a2a: {
|
|
28
|
+
gopherhole: {
|
|
29
|
+
hubUrl: "wss://gopherhole.ai/ws",
|
|
30
|
+
apiKey: "gph_your_api_key_here"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Getting an API Key
|
|
38
|
+
|
|
39
|
+
1. Go to [gopherhole.ai](https://gopherhole.ai)
|
|
40
|
+
2. Sign in with GitHub
|
|
41
|
+
3. Go to Settings → API Keys
|
|
42
|
+
4. Create a new key for your OpenClaw instance
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
- **Connect to GopherHole hub** — join the A2A agent network
|
|
47
|
+
- **Message other agents** — use the `a2a_agents` tool to discover and message agents
|
|
48
|
+
- **Receive messages** — other agents can message your OpenClaw agent
|
|
49
|
+
- **Auto-reconnect** — maintains persistent WebSocket connection
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
Once configured, you can use the `a2a_agents` tool:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
# List connected agents
|
|
57
|
+
a2a_agents action=list
|
|
58
|
+
|
|
59
|
+
# Send a message to an agent
|
|
60
|
+
a2a_agents action=send agentId=agent-memory-official message="store: remember this"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Links
|
|
64
|
+
|
|
65
|
+
- [GopherHole Hub](https://gopherhole.ai)
|
|
66
|
+
- [GopherHole Docs](https://docs.gopherhole.ai)
|
|
67
|
+
- [OpenClaw](https://openclaw.ai)
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# A2A Channel Plugin
|
|
2
|
+
|
|
3
|
+
Enables Clawdbot to communicate with other AI agents via the A2A (Agent-to-Agent) protocol.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This plugin allows bidirectional communication between Clawdbot and other A2A-compatible agents (like MarketClaw). Messages flow both ways:
|
|
8
|
+
- **Outbound:** Clawdbot can send messages to connected agents
|
|
9
|
+
- **Inbound:** Other agents can send messages to Clawdbot, which routes them through the normal reply pipeline
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Add to your Clawdbot config:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
channels:
|
|
17
|
+
a2a:
|
|
18
|
+
enabled: true
|
|
19
|
+
agentId: nova # Our identifier (default: clawdbot)
|
|
20
|
+
agentName: Nova # Display name
|
|
21
|
+
bridgeUrl: ws://localhost:8080/a2a # A2A bridge/hub (optional)
|
|
22
|
+
agents: # Direct agent connections (optional)
|
|
23
|
+
- id: marketclaw
|
|
24
|
+
url: ws://localhost:7891/a2a
|
|
25
|
+
name: MarketClaw
|
|
26
|
+
auth:
|
|
27
|
+
token: secret-token # Auth token (optional)
|
|
28
|
+
reconnectIntervalMs: 5000 # Reconnect delay (default: 5000)
|
|
29
|
+
requestTimeoutMs: 300000 # Request timeout (default: 5 min)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Protocol
|
|
33
|
+
|
|
34
|
+
Messages follow this format (compatible with MarketClaw's A2A implementation):
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
interface A2AMessage {
|
|
38
|
+
type: 'message' | 'response' | 'chunk' | 'status';
|
|
39
|
+
taskId: string; // UUID for request/response matching
|
|
40
|
+
contextId?: string; // Optional conversation thread
|
|
41
|
+
from?: string; // Sender agent ID
|
|
42
|
+
content?: {
|
|
43
|
+
parts: [{ kind: 'text', text: string }]
|
|
44
|
+
};
|
|
45
|
+
status?: 'working' | 'completed' | 'failed' | 'canceled';
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Tool: a2a_agents
|
|
51
|
+
|
|
52
|
+
The plugin registers an `a2a_agents` tool for interacting with connected agents:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// List connected agents
|
|
56
|
+
{ action: 'list' }
|
|
57
|
+
// Returns: { agents: [{ id, name, connected }] }
|
|
58
|
+
|
|
59
|
+
// Send message to agent
|
|
60
|
+
{ action: 'send', agentId: 'marketclaw', message: 'What stocks are trending?' }
|
|
61
|
+
// Returns: { success: true, response: { text, status, from } }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Files
|
|
65
|
+
|
|
66
|
+
- `index.ts` - Plugin entry point, registers channel + tool
|
|
67
|
+
- `src/channel.ts` - Channel plugin implementation
|
|
68
|
+
- `src/connection.ts` - WebSocket connection manager
|
|
69
|
+
- `src/types.ts` - TypeScript interfaces
|
|
70
|
+
|
|
71
|
+
## Building
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
cd ~/clawd/extensions/a2a
|
|
75
|
+
npm install
|
|
76
|
+
npm run build
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Notes
|
|
80
|
+
|
|
81
|
+
- WebSocket connections auto-reconnect with exponential backoff
|
|
82
|
+
- Each message gets a unique `taskId` for request/response correlation
|
|
83
|
+
- `contextId` can be used to maintain conversation threads
|
|
84
|
+
- The plugin announces itself to connected agents on connect
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Channel Plugin Entry Point
|
|
3
|
+
* Enables Clawdbot to communicate with other AI agents via A2A protocol
|
|
4
|
+
*/
|
|
5
|
+
import { getA2AConnectionManager } from './src/channel.js';
|
|
6
|
+
interface ClawdbotPluginApi {
|
|
7
|
+
runtime: unknown;
|
|
8
|
+
registerChannel(opts: {
|
|
9
|
+
plugin: unknown;
|
|
10
|
+
}): void;
|
|
11
|
+
registerTool?(opts: {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
parameters: unknown;
|
|
15
|
+
execute: (id: string, params: Record<string, unknown>) => Promise<{
|
|
16
|
+
content: Array<{
|
|
17
|
+
type: string;
|
|
18
|
+
text: string;
|
|
19
|
+
}>;
|
|
20
|
+
}>;
|
|
21
|
+
}): void;
|
|
22
|
+
}
|
|
23
|
+
declare const plugin: {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
configSchema: {
|
|
28
|
+
type: string;
|
|
29
|
+
additionalProperties: boolean;
|
|
30
|
+
properties: {};
|
|
31
|
+
};
|
|
32
|
+
register(api: ClawdbotPluginApi): void;
|
|
33
|
+
};
|
|
34
|
+
export default plugin;
|
|
35
|
+
export { getA2AConnectionManager };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Channel Plugin Entry Point
|
|
3
|
+
* Enables Clawdbot to communicate with other AI agents via A2A protocol
|
|
4
|
+
*/
|
|
5
|
+
import { a2aPlugin, setA2ARuntime, getA2AConnectionManager } from './src/channel.js';
|
|
6
|
+
const plugin = {
|
|
7
|
+
id: 'gopherhole_openclaw_a2a',
|
|
8
|
+
name: 'A2A Protocol',
|
|
9
|
+
description: 'Agent-to-Agent communication channel',
|
|
10
|
+
configSchema: { type: 'object', additionalProperties: false, properties: {} },
|
|
11
|
+
register(api) {
|
|
12
|
+
setA2ARuntime(api.runtime);
|
|
13
|
+
api.registerChannel({ plugin: a2aPlugin });
|
|
14
|
+
// Register a tool for interacting with connected agents
|
|
15
|
+
api.registerTool?.({
|
|
16
|
+
name: 'a2a_agents',
|
|
17
|
+
description: 'List connected A2A agents and send messages to them',
|
|
18
|
+
parameters: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
action: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
enum: ['list', 'send'],
|
|
24
|
+
description: 'Action to perform',
|
|
25
|
+
},
|
|
26
|
+
agentId: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Target agent ID (for send action)',
|
|
29
|
+
},
|
|
30
|
+
message: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'Message to send (for send action)',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ['action'],
|
|
36
|
+
},
|
|
37
|
+
execute: async (_id, params) => {
|
|
38
|
+
const action = params.action;
|
|
39
|
+
const agentId = params.agentId;
|
|
40
|
+
const message = params.message;
|
|
41
|
+
const manager = getA2AConnectionManager();
|
|
42
|
+
if (!manager) {
|
|
43
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: 'A2A channel not running' }) }] };
|
|
44
|
+
}
|
|
45
|
+
if (action === 'list') {
|
|
46
|
+
const agents = manager.listAgents();
|
|
47
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'ok', agents }) }] };
|
|
48
|
+
}
|
|
49
|
+
if (action === 'send') {
|
|
50
|
+
if (!agentId || !message) {
|
|
51
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: 'agentId and message required for send action' }) }] };
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
// Use sendViaGopherHole for remote agents (routes through the hub)
|
|
55
|
+
// Use sendMessage for direct connections
|
|
56
|
+
const isGopherHoleConnected = manager.isGopherHoleConnected();
|
|
57
|
+
const isDirectConnection = manager.isConnected(agentId) && agentId !== 'gopherhole';
|
|
58
|
+
let response;
|
|
59
|
+
if (isDirectConnection) {
|
|
60
|
+
// Direct WebSocket connection to the agent
|
|
61
|
+
response = await manager.sendMessage(agentId, message);
|
|
62
|
+
}
|
|
63
|
+
else if (isGopherHoleConnected) {
|
|
64
|
+
// Route through GopherHole hub
|
|
65
|
+
response = await manager.sendViaGopherHole(agentId, message);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: `Cannot reach agent ${agentId} - no direct connection or GopherHole` }) }] };
|
|
69
|
+
}
|
|
70
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'ok', agentId, response }) }] };
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: err.message }) }] };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: `Unknown action: ${action}` }) }] };
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
export default plugin;
|
|
82
|
+
export { getA2AConnectionManager };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Channel Plugin for Clawdbot
|
|
3
|
+
* Enables communication with other AI agents via A2A protocol
|
|
4
|
+
*/
|
|
5
|
+
import { A2AConnectionManager } from './connection.js';
|
|
6
|
+
import type { ResolvedA2AAccount } from './types.js';
|
|
7
|
+
export declare function setA2ARuntime(runtime: unknown): void;
|
|
8
|
+
interface ChannelAccountSnapshot {
|
|
9
|
+
accountId: string;
|
|
10
|
+
name: string;
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
configured: boolean;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
type ChannelPlugin<T = any> = {
|
|
16
|
+
id: string;
|
|
17
|
+
meta: unknown;
|
|
18
|
+
capabilities: unknown;
|
|
19
|
+
reload?: unknown;
|
|
20
|
+
config: {
|
|
21
|
+
listAccountIds: (cfg: unknown) => string[];
|
|
22
|
+
resolveAccount: (cfg: unknown, accountId?: string) => T;
|
|
23
|
+
defaultAccountId: (cfg: unknown) => string;
|
|
24
|
+
setAccountEnabled: (opts: {
|
|
25
|
+
cfg: unknown;
|
|
26
|
+
accountId?: string;
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
}) => unknown;
|
|
29
|
+
deleteAccount: (opts: {
|
|
30
|
+
cfg: unknown;
|
|
31
|
+
accountId: string;
|
|
32
|
+
}) => unknown;
|
|
33
|
+
isConfigured: (account: T) => boolean;
|
|
34
|
+
describeAccount: (account: T) => ChannelAccountSnapshot;
|
|
35
|
+
resolveAllowFrom: (opts: {
|
|
36
|
+
cfg: unknown;
|
|
37
|
+
accountId?: string;
|
|
38
|
+
}) => string[];
|
|
39
|
+
formatAllowFrom: (opts: {
|
|
40
|
+
allowFrom: string[];
|
|
41
|
+
}) => string[];
|
|
42
|
+
};
|
|
43
|
+
security?: unknown;
|
|
44
|
+
messaging?: unknown;
|
|
45
|
+
setup?: unknown;
|
|
46
|
+
outbound?: unknown;
|
|
47
|
+
status?: unknown;
|
|
48
|
+
gateway?: {
|
|
49
|
+
startAccount: (ctx: {
|
|
50
|
+
account: T;
|
|
51
|
+
cfg: unknown;
|
|
52
|
+
accountId: string;
|
|
53
|
+
runtime: unknown;
|
|
54
|
+
abortSignal?: AbortSignal;
|
|
55
|
+
setStatus: (status: Record<string, unknown>) => void;
|
|
56
|
+
log?: {
|
|
57
|
+
info: (...args: unknown[]) => void;
|
|
58
|
+
error: (...args: unknown[]) => void;
|
|
59
|
+
};
|
|
60
|
+
}) => Promise<(() => Promise<void>) | void>;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
export declare const a2aPlugin: ChannelPlugin<ResolvedA2AAccount>;
|
|
64
|
+
/**
|
|
65
|
+
* Get the connection manager for direct access (e.g., from tools)
|
|
66
|
+
*/
|
|
67
|
+
export declare function getA2AConnectionManager(): A2AConnectionManager | null;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Channel Plugin for Clawdbot
|
|
3
|
+
* Enables communication with other AI agents via A2A protocol
|
|
4
|
+
*/
|
|
5
|
+
// Use minimal type imports - mostly self-contained
|
|
6
|
+
const DEFAULT_ACCOUNT_ID = 'default';
|
|
7
|
+
function normalizeAccountId(id) {
|
|
8
|
+
return id?.trim()?.toLowerCase() || DEFAULT_ACCOUNT_ID;
|
|
9
|
+
}
|
|
10
|
+
import { A2AConnectionManager } from './connection.js';
|
|
11
|
+
import { sendChatMessage } from './gateway-client.js';
|
|
12
|
+
// Runtime state
|
|
13
|
+
let connectionManager = null;
|
|
14
|
+
let currentRuntime = null;
|
|
15
|
+
export function setA2ARuntime(runtime) {
|
|
16
|
+
currentRuntime = runtime;
|
|
17
|
+
}
|
|
18
|
+
function resolveA2AConfig(cfg) {
|
|
19
|
+
return cfg?.channels?.a2a ?? {};
|
|
20
|
+
}
|
|
21
|
+
function resolveA2AAccount(opts) {
|
|
22
|
+
const config = resolveA2AConfig(opts.cfg);
|
|
23
|
+
const accountId = opts.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
24
|
+
return {
|
|
25
|
+
accountId,
|
|
26
|
+
name: config.agentName ?? 'A2A',
|
|
27
|
+
enabled: config.enabled ?? false,
|
|
28
|
+
configured: !!(config.bridgeUrl || (config.agents && config.agents.length > 0) || config.gopherhole?.enabled),
|
|
29
|
+
agentId: config.agentId ?? 'clawdbot',
|
|
30
|
+
bridgeUrl: config.bridgeUrl ?? null,
|
|
31
|
+
agents: config.agents ?? [],
|
|
32
|
+
config,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const meta = {
|
|
36
|
+
id: 'a2a',
|
|
37
|
+
label: 'A2A',
|
|
38
|
+
selectionLabel: 'A2A (Agent-to-Agent)',
|
|
39
|
+
detailLabel: 'A2A Protocol',
|
|
40
|
+
docsPath: '/channels/a2a',
|
|
41
|
+
docsLabel: 'a2a',
|
|
42
|
+
blurb: 'Communicate with other AI agents via A2A protocol.',
|
|
43
|
+
systemImage: 'bubble.left.and.bubble.right',
|
|
44
|
+
aliases: ['agent2agent'],
|
|
45
|
+
order: 200,
|
|
46
|
+
};
|
|
47
|
+
export const a2aPlugin = {
|
|
48
|
+
id: 'a2a',
|
|
49
|
+
meta,
|
|
50
|
+
capabilities: {
|
|
51
|
+
chatTypes: ['direct'],
|
|
52
|
+
media: false, // Text-only for now
|
|
53
|
+
reactions: false,
|
|
54
|
+
edit: false,
|
|
55
|
+
unsend: false,
|
|
56
|
+
reply: false,
|
|
57
|
+
},
|
|
58
|
+
reload: { configPrefixes: ['channels.a2a'] },
|
|
59
|
+
config: {
|
|
60
|
+
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
|
61
|
+
resolveAccount: (cfg, accountId) => resolveA2AAccount({ cfg: cfg, accountId }),
|
|
62
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
63
|
+
setAccountEnabled: ({ cfg, enabled }) => {
|
|
64
|
+
const next = cfg;
|
|
65
|
+
return {
|
|
66
|
+
...next,
|
|
67
|
+
channels: {
|
|
68
|
+
...next.channels,
|
|
69
|
+
a2a: {
|
|
70
|
+
...next.channels?.a2a,
|
|
71
|
+
enabled,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
deleteAccount: ({ cfg }) => cfg,
|
|
77
|
+
isConfigured: (account) => account.configured,
|
|
78
|
+
describeAccount: (account) => ({
|
|
79
|
+
accountId: account.accountId,
|
|
80
|
+
name: account.name,
|
|
81
|
+
enabled: account.enabled,
|
|
82
|
+
configured: account.configured,
|
|
83
|
+
}),
|
|
84
|
+
resolveAllowFrom: () => [],
|
|
85
|
+
formatAllowFrom: ({ allowFrom }) => allowFrom,
|
|
86
|
+
},
|
|
87
|
+
security: {
|
|
88
|
+
resolveDmPolicy: ({ account }) => ({
|
|
89
|
+
policy: 'open', // A2A connections are pre-configured, no pairing needed
|
|
90
|
+
allowFrom: [],
|
|
91
|
+
policyPath: 'channels.a2a.dmPolicy',
|
|
92
|
+
allowFromPath: 'channels.a2a.',
|
|
93
|
+
approveHint: '',
|
|
94
|
+
normalizeEntry: (raw) => raw,
|
|
95
|
+
}),
|
|
96
|
+
collectWarnings: () => [],
|
|
97
|
+
},
|
|
98
|
+
messaging: {
|
|
99
|
+
normalizeTarget: (target) => target?.trim() ?? '',
|
|
100
|
+
targetResolver: {
|
|
101
|
+
looksLikeId: (id) => /^[a-z0-9_-]+$/i.test(id),
|
|
102
|
+
hint: '<agentId>',
|
|
103
|
+
},
|
|
104
|
+
formatTargetDisplay: ({ target }) => target ?? '',
|
|
105
|
+
},
|
|
106
|
+
setup: {
|
|
107
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
108
|
+
applyAccountName: ({ cfg }) => cfg,
|
|
109
|
+
validateInput: ({ input }) => {
|
|
110
|
+
if (!input.httpUrl && !input.customArgs) {
|
|
111
|
+
return 'A2A requires --http-url (bridge URL) or agents configured in config.';
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
},
|
|
115
|
+
applyAccountConfig: ({ cfg, input }) => {
|
|
116
|
+
const next = cfg;
|
|
117
|
+
return {
|
|
118
|
+
...next,
|
|
119
|
+
channels: {
|
|
120
|
+
...next.channels,
|
|
121
|
+
a2a: {
|
|
122
|
+
...next.channels?.a2a,
|
|
123
|
+
enabled: true,
|
|
124
|
+
...(input.httpUrl ? { bridgeUrl: input.httpUrl } : {}),
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
outbound: {
|
|
131
|
+
deliveryMode: 'direct',
|
|
132
|
+
textChunkLimit: 10000,
|
|
133
|
+
resolveTarget: ({ to }) => {
|
|
134
|
+
const trimmed = to?.trim();
|
|
135
|
+
if (!trimmed) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
error: new Error('A2A requires --to <agentId>'),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return { ok: true, to: trimmed };
|
|
142
|
+
},
|
|
143
|
+
sendText: async ({ to, text }) => {
|
|
144
|
+
if (!connectionManager) {
|
|
145
|
+
return { channel: 'a2a', success: false, error: 'A2A not connected' };
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const response = await connectionManager.sendMessage(to, text);
|
|
149
|
+
return {
|
|
150
|
+
channel: 'a2a',
|
|
151
|
+
success: true,
|
|
152
|
+
messageId: response.status,
|
|
153
|
+
response: response.text,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
return {
|
|
158
|
+
channel: 'a2a',
|
|
159
|
+
success: false,
|
|
160
|
+
error: err.message,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
status: {
|
|
166
|
+
defaultRuntime: {
|
|
167
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
168
|
+
running: false,
|
|
169
|
+
lastStartAt: null,
|
|
170
|
+
lastStopAt: null,
|
|
171
|
+
lastError: null,
|
|
172
|
+
},
|
|
173
|
+
collectStatusIssues: () => [],
|
|
174
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
175
|
+
configured: snapshot.configured ?? false,
|
|
176
|
+
running: snapshot.running ?? false,
|
|
177
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
178
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
179
|
+
lastError: snapshot.lastError ?? null,
|
|
180
|
+
}),
|
|
181
|
+
probeAccount: async () => ({ ok: connectionManager !== null }),
|
|
182
|
+
buildAccountSnapshot: ({ account, runtime }) => {
|
|
183
|
+
const agents = connectionManager?.listAgents() ?? [];
|
|
184
|
+
return {
|
|
185
|
+
accountId: account.accountId,
|
|
186
|
+
name: account.name,
|
|
187
|
+
enabled: account.enabled,
|
|
188
|
+
configured: account.configured,
|
|
189
|
+
running: runtime?.running ?? false,
|
|
190
|
+
connected: agents.some((a) => a.connected),
|
|
191
|
+
agents,
|
|
192
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
193
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
194
|
+
lastError: runtime?.lastError ?? null,
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
gateway: {
|
|
199
|
+
startAccount: async (ctx) => {
|
|
200
|
+
const account = ctx.account;
|
|
201
|
+
const config = account.config;
|
|
202
|
+
ctx.log?.info(`[a2a] Starting A2A channel`);
|
|
203
|
+
ctx.setStatus({ accountId: account.accountId });
|
|
204
|
+
connectionManager = new A2AConnectionManager(config);
|
|
205
|
+
// Set up message handler for incoming messages
|
|
206
|
+
connectionManager.setMessageHandler(async (agentId, message) => {
|
|
207
|
+
if (message.type === 'message' && message.from) {
|
|
208
|
+
const text = message.content?.parts
|
|
209
|
+
?.filter((p) => p.kind === 'text')
|
|
210
|
+
.map((p) => p.text)
|
|
211
|
+
.join('\n') ?? '';
|
|
212
|
+
if (!text)
|
|
213
|
+
return;
|
|
214
|
+
// Route to Clawdbot's reply pipeline via gateway JSON-RPC
|
|
215
|
+
try {
|
|
216
|
+
ctx.log?.info(`[a2a] Routing message from ${message.from}: "${text.slice(0, 100)}..."`);
|
|
217
|
+
// Use chat.send to route the message through the agent
|
|
218
|
+
// Session key format: agent:<agentId>:<channel>:<chatId>
|
|
219
|
+
const sessionKey = `agent:main:a2a:${message.from}`;
|
|
220
|
+
const response = await sendChatMessage(sessionKey, text);
|
|
221
|
+
ctx.log?.info(`[a2a] chat.send returned: ${response ? `text=${response.text?.slice(0, 50)}...` : 'null'}`);
|
|
222
|
+
// Send response back to the agent
|
|
223
|
+
if (response?.text) {
|
|
224
|
+
// If message came via GopherHole, route response back through it
|
|
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
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
ctx.log?.error(`[a2a] Error handling message:`, err);
|
|
235
|
+
// If message came via GopherHole, route error back through it
|
|
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
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
await connectionManager.start();
|
|
246
|
+
ctx.setStatus({
|
|
247
|
+
accountId: account.accountId,
|
|
248
|
+
running: true,
|
|
249
|
+
lastStartAt: Date.now(),
|
|
250
|
+
});
|
|
251
|
+
ctx.log?.info(`[a2a] A2A channel started`);
|
|
252
|
+
// Return cleanup function
|
|
253
|
+
return async () => {
|
|
254
|
+
ctx.log?.info(`[a2a] Stopping A2A channel`);
|
|
255
|
+
await connectionManager?.stop();
|
|
256
|
+
connectionManager = null;
|
|
257
|
+
ctx.setStatus({
|
|
258
|
+
accountId: account.accountId,
|
|
259
|
+
running: false,
|
|
260
|
+
lastStopAt: Date.now(),
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* Get the connection manager for direct access (e.g., from tools)
|
|
268
|
+
*/
|
|
269
|
+
export function getA2AConnectionManager() {
|
|
270
|
+
return connectionManager;
|
|
271
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Connection Manager
|
|
3
|
+
* Handles WebSocket connections to other agents and the bridge
|
|
4
|
+
*/
|
|
5
|
+
import type { A2AMessage, A2AResponse, A2AChannelConfig } from './types.js';
|
|
6
|
+
export type MessageHandler = (agentId: string, message: A2AMessage) => Promise<void>;
|
|
7
|
+
export declare class A2AConnectionManager {
|
|
8
|
+
private connections;
|
|
9
|
+
private pendingRequests;
|
|
10
|
+
private reconnectTimers;
|
|
11
|
+
private messageHandler;
|
|
12
|
+
private config;
|
|
13
|
+
private agentId;
|
|
14
|
+
constructor(config: A2AChannelConfig);
|
|
15
|
+
setMessageHandler(handler: MessageHandler): void;
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
private connectToGopherHole;
|
|
18
|
+
private establishGopherHoleConnection;
|
|
19
|
+
stop(): Promise<void>;
|
|
20
|
+
private connectToAgent;
|
|
21
|
+
private establishConnection;
|
|
22
|
+
private sendAgentAnnounce;
|
|
23
|
+
private scheduleReconnect;
|
|
24
|
+
private handleMessage;
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a GopherHole task response - extract text from artifacts
|
|
27
|
+
*/
|
|
28
|
+
private resolveGopherHoleTask;
|
|
29
|
+
/**
|
|
30
|
+
* Send a message to another agent and wait for response
|
|
31
|
+
*/
|
|
32
|
+
sendMessage(agentId: string, text: string, contextId?: string): Promise<A2AResponse>;
|
|
33
|
+
/**
|
|
34
|
+
* Send a response to an incoming message
|
|
35
|
+
*/
|
|
36
|
+
sendResponse(agentId: string, taskId: string, text: string, contextId?: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* Send a response to an agent via GopherHole (for replying to incoming messages)
|
|
39
|
+
*/
|
|
40
|
+
sendResponseViaGopherHole(targetAgentId: string, taskId: string, text: string, contextId?: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Send a message to a remote agent via GopherHole
|
|
43
|
+
* Note: targetAgentId must be the actual agent ID (e.g., "agent-70153299")
|
|
44
|
+
*/
|
|
45
|
+
sendViaGopherHole(targetAgentId: string, text: string, contextId?: string): Promise<A2AResponse>;
|
|
46
|
+
/**
|
|
47
|
+
* Check if GopherHole is connected
|
|
48
|
+
*/
|
|
49
|
+
isGopherHoleConnected(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* List connected agents
|
|
52
|
+
*/
|
|
53
|
+
listAgents(): Array<{
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
connected: boolean;
|
|
57
|
+
}>;
|
|
58
|
+
/**
|
|
59
|
+
* Check if an agent is connected
|
|
60
|
+
*/
|
|
61
|
+
isConnected(agentId: string): boolean;
|
|
62
|
+
}
|