a2acalling 0.1.4 → 0.1.6
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/AGENTS.md +5 -5
- package/CLAUDE.md +4 -4
- package/README.md +34 -8
- package/SKILL.md +10 -10
- package/bin/cli.js +10 -10
- package/docs/protocol.md +52 -14
- package/package.json +3 -3
- package/scripts/install-openclaw.js +6 -6
- package/src/index.js +2 -2
- package/src/lib/client.js +79 -5
- package/src/lib/conversations.js +1 -1
- package/src/lib/openclaw-integration.js +4 -4
- package/src/lib/summarizer.js +1 -1
- package/src/lib/tokens.js +4 -4
- package/src/routes/{federation.js → a2a.js} +18 -18
- package/src/server.js +143 -163
package/AGENTS.md
CHANGED
|
@@ -24,7 +24,7 @@ a2acalling/
|
|
|
24
24
|
│ │ ├── tokens.js # Token storage & validation
|
|
25
25
|
│ │ └── client.js # Outbound A2A client
|
|
26
26
|
│ └── routes/
|
|
27
|
-
│ └──
|
|
27
|
+
│ └── a2a.js # Express routes for /api/a2a
|
|
28
28
|
├── docs/
|
|
29
29
|
│ └── protocol.md # Protocol specification
|
|
30
30
|
└── .env # Secrets (gitignored)
|
|
@@ -47,15 +47,15 @@ node -e "const a2a = require('./src'); console.log(a2a.version)"
|
|
|
47
47
|
2. **Permission presets**: `chat-only` (default), `tools-read`, `tools-write`
|
|
48
48
|
3. **Disclosure levels**: `public`, `minimal` (default), `none`
|
|
49
49
|
4. **Rate limits**: 10/min, 100/hr, 1000/day per token
|
|
50
|
-
5. **Storage**: JSON file at `~/.config/openclaw/a2a
|
|
50
|
+
5. **Storage**: JSON file at `~/.config/openclaw/a2a.json`
|
|
51
51
|
|
|
52
52
|
## Integration Points
|
|
53
53
|
|
|
54
54
|
This package is designed to integrate with OpenClaw:
|
|
55
55
|
|
|
56
|
-
1. **Gateway routes**: Mount `createRoutes()` at `/api/
|
|
57
|
-
2. **Agent tool**: Add `
|
|
58
|
-
3. **Commands**: Wire `/
|
|
56
|
+
1. **Gateway routes**: Mount `createRoutes()` at `/api/a2a`
|
|
57
|
+
2. **Agent tool**: Add `a2a_call` tool using `A2AClient`
|
|
58
|
+
3. **Commands**: Wire `/a2a` commands to CLI functions
|
|
59
59
|
|
|
60
60
|
## Commit Convention
|
|
61
61
|
|
package/CLAUDE.md
CHANGED
|
@@ -16,19 +16,19 @@ git remote set-url origin https://${GH_TOKEN}@github.com/onthegonow/A2A_for_Open
|
|
|
16
16
|
## What This Does
|
|
17
17
|
|
|
18
18
|
1. **Token Management** - Create expiring tokens with permissions (chat-only/tools-read/tools-write)
|
|
19
|
-
2. **Inbound Calls** - Express routes handle `/api/
|
|
19
|
+
2. **Inbound Calls** - Express routes handle `/api/a2a/invoke` from remote agents
|
|
20
20
|
3. **Outbound Calls** - `A2AClient` calls remote agents via their invite URLs
|
|
21
21
|
4. **Owner Notifications** - Configurable alerts when your agent gets called
|
|
22
22
|
|
|
23
23
|
## Token Flow
|
|
24
24
|
|
|
25
25
|
```
|
|
26
|
-
User: /
|
|
26
|
+
User: /a2a create --name "Alice" --expires 7d
|
|
27
27
|
Bot: ✅ a2a://myhost.com/fed_abc123
|
|
28
28
|
|
|
29
29
|
User shares URL with Alice...
|
|
30
30
|
|
|
31
|
-
Alice's agent: POST /api/
|
|
31
|
+
Alice's agent: POST /api/a2a/invoke
|
|
32
32
|
Authorization: Bearer fed_abc123
|
|
33
33
|
{"message": "Hey, can you help?"}
|
|
34
34
|
|
|
@@ -40,7 +40,7 @@ You get notified (if configured).
|
|
|
40
40
|
|
|
41
41
|
- `src/lib/tokens.js` - All token CRUD + validation
|
|
42
42
|
- `src/lib/client.js` - `A2AClient` for outbound calls
|
|
43
|
-
- `src/routes/
|
|
43
|
+
- `src/routes/a2a.js` - Express router (mount at `/api/a2a`)
|
|
44
44
|
- `docs/protocol.md` - Full protocol spec
|
|
45
45
|
|
|
46
46
|
## Testing
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ a2a create --name "My Agent" --owner "Your Name" --tier friends
|
|
|
40
40
|
a2a add "a2a://their-host.com/fed_xyz789" "Alice's Agent"
|
|
41
41
|
|
|
42
42
|
# Make a call
|
|
43
|
-
a2a call "Alice's Agent" "Hey! Want to collaborate on the
|
|
43
|
+
a2a call "Alice's Agent" "Hey! Want to collaborate on the a2a protocol?"
|
|
44
44
|
|
|
45
45
|
# Or call directly
|
|
46
46
|
a2a call "a2a://their-host.com/fed_xyz789" "Hello!"
|
|
@@ -120,7 +120,7 @@ Every call generates an owner-context summary that tracks the exchange:
|
|
|
120
120
|
{
|
|
121
121
|
"exchange": {
|
|
122
122
|
"weGot": ["learned about their developer tools project"],
|
|
123
|
-
"weGave": ["shared our A2A
|
|
123
|
+
"weGave": ["shared our A2A work"],
|
|
124
124
|
"balance": "even",
|
|
125
125
|
"fair": true
|
|
126
126
|
},
|
|
@@ -171,7 +171,7 @@ a2a ping <target> # Check if agent is available
|
|
|
171
171
|
### Server
|
|
172
172
|
|
|
173
173
|
```bash
|
|
174
|
-
a2a
|
|
174
|
+
a2a server [options] # Start A2A server
|
|
175
175
|
--port, -p <port> # Port (default: 3001)
|
|
176
176
|
```
|
|
177
177
|
|
|
@@ -187,9 +187,10 @@ a2a://<hostname>:<port>/<token>
|
|
|
187
187
|
|
|
188
188
|
| Method | Path | Description |
|
|
189
189
|
|--------|------|-------------|
|
|
190
|
-
| `GET` | `/api/
|
|
191
|
-
| `GET` | `/api/
|
|
192
|
-
| `POST` | `/api/
|
|
190
|
+
| `GET` | `/api/a2a/status` | Check A2A support |
|
|
191
|
+
| `GET` | `/api/a2a/ping` | Health check with auth |
|
|
192
|
+
| `POST` | `/api/a2a/invoke` | Call the agent |
|
|
193
|
+
| `POST` | `/api/a2a/end` | End a conversation and return summary data |
|
|
193
194
|
|
|
194
195
|
### Invoke Request
|
|
195
196
|
|
|
@@ -214,6 +215,25 @@ a2a://<hostname>:<port>/<token>
|
|
|
214
215
|
}
|
|
215
216
|
```
|
|
216
217
|
|
|
218
|
+
### End Conversation Request
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"conversation_id": "conv_123"
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### End Conversation Response
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"success": true,
|
|
231
|
+
"conversation_id": "conv_123",
|
|
232
|
+
"status": "concluded",
|
|
233
|
+
"summary": "Optional call summary"
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
217
237
|
## 🔌 Library Usage
|
|
218
238
|
|
|
219
239
|
### Making Calls (Client)
|
|
@@ -237,6 +257,12 @@ const followUp = await client.call(
|
|
|
237
257
|
'Thanks! One more question...',
|
|
238
258
|
{ conversationId: response.conversation_id }
|
|
239
259
|
);
|
|
260
|
+
|
|
261
|
+
// Explicitly end the call when done
|
|
262
|
+
const ended = await client.end(
|
|
263
|
+
'a2a://their-host.com/fed_token123',
|
|
264
|
+
response.conversation_id
|
|
265
|
+
);
|
|
240
266
|
```
|
|
241
267
|
|
|
242
268
|
### Receiving Calls (Server)
|
|
@@ -248,7 +274,7 @@ const express = require('express');
|
|
|
248
274
|
const app = express();
|
|
249
275
|
app.use(express.json());
|
|
250
276
|
|
|
251
|
-
app.use('/api/
|
|
277
|
+
app.use('/api/a2a', createRoutes({
|
|
252
278
|
tokenStore: new TokenStore(),
|
|
253
279
|
|
|
254
280
|
async handleMessage(message, context) {
|
|
@@ -284,7 +310,7 @@ app.listen(3001);
|
|
|
284
310
|
|
|
285
311
|
## 🤝 Philosophy
|
|
286
312
|
|
|
287
|
-
|
|
313
|
+
A2A is **cooperative AND adversarial**. Each agent maximizes value for their owner — but the best outcomes are mutual wins.
|
|
288
314
|
|
|
289
315
|
Your agent should:
|
|
290
316
|
1. **Protect your interests** — track what you're giving vs. getting
|
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: a2a
|
|
3
|
-
description: "Agent-to-agent
|
|
3
|
+
description: "Agent-to-agent A2A for OpenClaw. Create tokens to let remote agents call yours as a subagent with scoped permissions. Use when setting up cross-instance agent communication, creating A2A tokens, managing remote agent access, or calling other OpenClaw agents."
|
|
4
4
|
metadata:
|
|
5
5
|
{
|
|
6
6
|
"openclaw":
|
|
@@ -17,13 +17,13 @@ metadata:
|
|
|
17
17
|
"label": "Install A2A Calling (npm)",
|
|
18
18
|
},
|
|
19
19
|
],
|
|
20
|
-
"routes": "/api/
|
|
21
|
-
"tools": ["
|
|
20
|
+
"routes": "/api/a2a",
|
|
21
|
+
"tools": ["a2a_call"],
|
|
22
22
|
},
|
|
23
23
|
}
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
# A2A
|
|
26
|
+
# A2A
|
|
27
27
|
|
|
28
28
|
Enable agent-to-agent communication across OpenClaw instances.
|
|
29
29
|
|
|
@@ -31,7 +31,7 @@ Enable agent-to-agent communication across OpenClaw instances.
|
|
|
31
31
|
|
|
32
32
|
### Create Token
|
|
33
33
|
|
|
34
|
-
User says: `/
|
|
34
|
+
User says: `/a2a create`, "create an A2A token", "let another agent call me"
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
a2a create --name "NAME" --expires DURATION --permissions LEVEL
|
|
@@ -68,11 +68,11 @@ a2a add "a2a://host/token" "Agent Name"
|
|
|
68
68
|
|
|
69
69
|
## Calling Remote Agents
|
|
70
70
|
|
|
71
|
-
When task delegation to a known remote agent would help, or user asks to contact a
|
|
71
|
+
When task delegation to a known remote agent would help, or user asks to contact a A2A agent:
|
|
72
72
|
|
|
73
73
|
```javascript
|
|
74
|
-
// Use
|
|
75
|
-
|
|
74
|
+
// Use a2a_call tool
|
|
75
|
+
a2a_call({
|
|
76
76
|
endpoint: "a2a://host/token",
|
|
77
77
|
message: "Your question here",
|
|
78
78
|
conversation_id: "optional-for-continuity"
|
|
@@ -81,7 +81,7 @@ federation_call({
|
|
|
81
81
|
|
|
82
82
|
## Handling Incoming Calls
|
|
83
83
|
|
|
84
|
-
When receiving
|
|
84
|
+
When receiving an A2A call, the agent operates within the token's permission scope:
|
|
85
85
|
|
|
86
86
|
| Permission | Allowed |
|
|
87
87
|
|------------|---------|
|
|
@@ -99,7 +99,7 @@ Apply disclosure level:
|
|
|
99
99
|
When `notify: all`, send to owner:
|
|
100
100
|
|
|
101
101
|
```
|
|
102
|
-
🤝
|
|
102
|
+
🤝 A2A call received
|
|
103
103
|
|
|
104
104
|
From: [Caller] ([host])
|
|
105
105
|
Token: "[name]" (expires [date])
|
package/bin/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* A2A Calling CLI
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* a2a create [options] Create
|
|
6
|
+
* a2a create [options] Create an A2A token
|
|
7
7
|
* a2a list List active tokens
|
|
8
8
|
* a2a revoke <id> Revoke a token
|
|
9
9
|
* a2a add <url> [name] Add a remote agent
|
|
@@ -122,7 +122,7 @@ const commands = {
|
|
|
122
122
|
console.log(`✅ Token created (link failed: ${linkResult.error})\n`);
|
|
123
123
|
}
|
|
124
124
|
} else {
|
|
125
|
-
console.log(`✅
|
|
125
|
+
console.log(`✅ A2A token created\n`);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
console.log(`Name: ${record.name}`);
|
|
@@ -167,11 +167,11 @@ a2a call "${agentName}" "Hello!"
|
|
|
167
167
|
list: () => {
|
|
168
168
|
const tokens = store.list();
|
|
169
169
|
if (tokens.length === 0) {
|
|
170
|
-
console.log('No active
|
|
170
|
+
console.log('No active A2A tokens.');
|
|
171
171
|
return;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
console.log('Active
|
|
174
|
+
console.log('Active A2A tokens:\n');
|
|
175
175
|
for (const t of tokens) {
|
|
176
176
|
const expired = t.expires_at && new Date(t.expires_at) < new Date();
|
|
177
177
|
const status = expired ? '⚠️ EXPIRED' : '✅ Active';
|
|
@@ -715,7 +715,7 @@ a2a call "${agentName}" "Hello!"
|
|
|
715
715
|
const client = new A2AClient();
|
|
716
716
|
try {
|
|
717
717
|
const status = await client.status(url);
|
|
718
|
-
console.log(`
|
|
718
|
+
console.log(`A2A status for ${url}:\n`);
|
|
719
719
|
console.log(JSON.stringify(status, null, 2));
|
|
720
720
|
} catch (err) {
|
|
721
721
|
console.error(`❌ Failed to get status: ${err.message}`);
|
|
@@ -726,7 +726,7 @@ a2a call "${agentName}" "Hello!"
|
|
|
726
726
|
server: (args) => {
|
|
727
727
|
const port = args.flags.port || args.flags.p || process.env.PORT || 3001;
|
|
728
728
|
process.env.PORT = port;
|
|
729
|
-
console.log(`Starting A2A
|
|
729
|
+
console.log(`Starting A2A server on port ${port}...`);
|
|
730
730
|
require('../src/server.js');
|
|
731
731
|
},
|
|
732
732
|
|
|
@@ -747,7 +747,7 @@ a2a call "${agentName}" "Hello!"
|
|
|
747
747
|
const req = http.request({
|
|
748
748
|
hostname: serverHost === 'localhost' ? '127.0.0.1' : serverHost,
|
|
749
749
|
port: serverPort,
|
|
750
|
-
path: '/api/
|
|
750
|
+
path: '/api/a2a/ping',
|
|
751
751
|
timeout: 2000
|
|
752
752
|
}, (res) => {
|
|
753
753
|
resolve(res.statusCode === 200);
|
|
@@ -822,7 +822,7 @@ ${inviteUrl}
|
|
|
822
822
|
Usage: a2a <command> [options]
|
|
823
823
|
|
|
824
824
|
Commands:
|
|
825
|
-
create Create
|
|
825
|
+
create Create an A2A token
|
|
826
826
|
--name, -n Token/agent name
|
|
827
827
|
--owner, -o Owner name (human behind the agent)
|
|
828
828
|
--expires, -e Expiration (1h, 1d, 7d, 30d, never)
|
|
@@ -864,10 +864,10 @@ Conversations:
|
|
|
864
864
|
Calling:
|
|
865
865
|
call <contact|url> <msg> Call a remote agent
|
|
866
866
|
ping <url> Check if agent is reachable
|
|
867
|
-
status <url> Get
|
|
867
|
+
status <url> Get A2A status
|
|
868
868
|
|
|
869
869
|
Server:
|
|
870
|
-
server Start the
|
|
870
|
+
server Start the A2A server
|
|
871
871
|
--port, -p Port to listen on (default: 3001)
|
|
872
872
|
|
|
873
873
|
quickstart One-command setup: check server + create invite
|
package/docs/protocol.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# A2A Protocol v0 Reference
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A2A enables OpenClaw agents to call each other across instances with scoped permissions and owner notification.
|
|
6
6
|
|
|
7
7
|
## Token Format
|
|
8
8
|
|
|
@@ -16,16 +16,16 @@ Token structure: `fed_<base64url(24 random bytes)>`
|
|
|
16
16
|
|
|
17
17
|
## API Endpoints
|
|
18
18
|
|
|
19
|
-
All endpoints are prefixed with `/api/
|
|
19
|
+
All endpoints are prefixed with `/api/a2a/`
|
|
20
20
|
|
|
21
21
|
### GET /status
|
|
22
22
|
|
|
23
|
-
Check if
|
|
23
|
+
Check if A2A is enabled.
|
|
24
24
|
|
|
25
25
|
Response:
|
|
26
26
|
```json
|
|
27
27
|
{
|
|
28
|
-
"
|
|
28
|
+
"a2a": true,
|
|
29
29
|
"version": "0.1.0",
|
|
30
30
|
"capabilities": ["invoke", "multi-turn"],
|
|
31
31
|
"rate_limits": {
|
|
@@ -93,6 +93,40 @@ Error responses:
|
|
|
93
93
|
{"success": false, "error": "missing_message", "message": "..."}
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
### POST /end
|
|
97
|
+
|
|
98
|
+
Explicitly end a conversation and trigger conclusion/summarization.
|
|
99
|
+
|
|
100
|
+
Headers:
|
|
101
|
+
```
|
|
102
|
+
Authorization: Bearer fed_abc123xyz
|
|
103
|
+
Content-Type: application/json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Request body:
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"conversation_id": "conv_123456"
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Success response:
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"success": true,
|
|
117
|
+
"conversation_id": "conv_123456",
|
|
118
|
+
"status": "concluded",
|
|
119
|
+
"summary": "Optional summary text"
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Error responses:
|
|
124
|
+
```json
|
|
125
|
+
{"success": false, "error": "unauthorized", "message": "..."}
|
|
126
|
+
{"success": false, "error": "missing_conversation_id", "message": "..."}
|
|
127
|
+
{"success": false, "error": "internal_error", "message": "..."}
|
|
128
|
+
```
|
|
129
|
+
|
|
96
130
|
## Permission Scopes
|
|
97
131
|
|
|
98
132
|
| Scope | Tools | Files | Memory | Actions |
|
|
@@ -111,7 +145,7 @@ Error responses:
|
|
|
111
145
|
|
|
112
146
|
## Token Storage Schema
|
|
113
147
|
|
|
114
|
-
Stored in `~/.config/openclaw/
|
|
148
|
+
Stored in `~/.config/openclaw/a2a.json`:
|
|
115
149
|
|
|
116
150
|
```json
|
|
117
151
|
{
|
|
@@ -156,7 +190,7 @@ Limits reset on natural boundaries (minute, hour, day UTC).
|
|
|
156
190
|
## Security Considerations
|
|
157
191
|
|
|
158
192
|
1. **Token hashing**: Tokens stored as SHA-256 hashes server-side
|
|
159
|
-
2. **TLS required**: All
|
|
193
|
+
2. **TLS required**: All A2A calls should use HTTPS
|
|
160
194
|
3. **No credential forwarding**: Tokens are never forwarded to other agents
|
|
161
195
|
4. **Audit logging**: All invocations are logged with caller info
|
|
162
196
|
5. **Auto-revocation**: Tokens may auto-revoke after repeated errors
|
|
@@ -172,13 +206,17 @@ To continue a conversation, include `conversation_id` from the previous response
|
|
|
172
206
|
}
|
|
173
207
|
```
|
|
174
208
|
|
|
209
|
+
When finished, either:
|
|
210
|
+
- Call `POST /end` with the same `conversation_id` for explicit conclusion, or
|
|
211
|
+
- Let the receiver auto-conclude on idle timeout/max duration (if enabled).
|
|
212
|
+
|
|
175
213
|
Conversations expire after 1 hour of inactivity.
|
|
176
214
|
|
|
177
215
|
## Owner Notifications
|
|
178
216
|
|
|
179
217
|
When `notify: all`:
|
|
180
218
|
```
|
|
181
|
-
🤝
|
|
219
|
+
🤝 A2A call received
|
|
182
220
|
|
|
183
221
|
From: Alice's Agent (alice.example.com)
|
|
184
222
|
Token: "Work collab" (expires 2026-02-18)
|
|
@@ -199,16 +237,16 @@ Owner can reply to inject into the conversation.
|
|
|
199
237
|
|
|
200
238
|
Add to gateway routes:
|
|
201
239
|
```javascript
|
|
202
|
-
const
|
|
203
|
-
app.use('/api/
|
|
240
|
+
const a2a = require('./skills/a2a/scripts/server');
|
|
241
|
+
app.use('/api/a2a', a2a);
|
|
204
242
|
```
|
|
205
243
|
|
|
206
244
|
### Agent Context
|
|
207
245
|
|
|
208
|
-
When handling
|
|
246
|
+
When handling an A2A call, inject context:
|
|
209
247
|
```json
|
|
210
248
|
{
|
|
211
|
-
"
|
|
249
|
+
"a2a": {
|
|
212
250
|
"active": true,
|
|
213
251
|
"caller": "Alice's Agent",
|
|
214
252
|
"permissions": "chat-only",
|
|
@@ -217,10 +255,10 @@ When handling a federation call, inject context:
|
|
|
217
255
|
}
|
|
218
256
|
```
|
|
219
257
|
|
|
220
|
-
### New Tool:
|
|
258
|
+
### New Tool: a2a_call
|
|
221
259
|
|
|
222
260
|
```typescript
|
|
223
|
-
|
|
261
|
+
a2a_call({
|
|
224
262
|
endpoint: string, // a2a:// URL
|
|
225
263
|
message: string, // Message to send
|
|
226
264
|
conversation_id?: string // For multi-turn
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "a2acalling",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Agent-to-agent calling for OpenClaw -
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"a2a": "bin/cli.js",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"openclaw",
|
|
16
16
|
"claudebot",
|
|
17
17
|
"agent",
|
|
18
|
-
"federation",
|
|
19
18
|
"a2a",
|
|
19
|
+
"calling",
|
|
20
20
|
"ai"
|
|
21
21
|
],
|
|
22
22
|
"author": "OpenClaw Contributors",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* This script:
|
|
6
6
|
* 1. Installs the a2a skill to the user's OpenClaw skills directory
|
|
7
7
|
* 2. Adds /a2a as a custom command in OpenClaw config
|
|
8
|
-
* 3. Sets up the
|
|
8
|
+
* 3. Sets up the A2A server as a systemd service (optional)
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
11
|
* npx a2acalling install
|
|
@@ -47,10 +47,10 @@ function error(msg) { console.error(`${red('[a2a]')} ${msg}`); }
|
|
|
47
47
|
// Skill content
|
|
48
48
|
const SKILL_MD = `---
|
|
49
49
|
name: a2a
|
|
50
|
-
description: "Agent-to-Agent
|
|
50
|
+
description: "Agent-to-Agent a2a. Handle /a2a commands to create tokens, manage connections, and call remote agents. Triggers on: /a2a, a2a, agent token, a2a invite."
|
|
51
51
|
---
|
|
52
52
|
|
|
53
|
-
# A2A
|
|
53
|
+
# A2A
|
|
54
54
|
|
|
55
55
|
Handle agent-to-agent communication with Telegram inline buttons + \`a2a\` CLI.
|
|
56
56
|
|
|
@@ -88,7 +88,7 @@ message({
|
|
|
88
88
|
channel: "telegram",
|
|
89
89
|
target: "CHAT_ID",
|
|
90
90
|
threadId: "TOPIC_ID", // REQUIRED for forum topics!
|
|
91
|
-
message: "🤝 **A2A
|
|
91
|
+
message: "🤝 **A2A**\\n\\nWhat would you like to do?",
|
|
92
92
|
buttons: [
|
|
93
93
|
[{ text: "📝 Create Invite", callback_data: "/a2a invite" }, { text: "📋 List Tokens", callback_data: "/a2a list" }],
|
|
94
94
|
[{ text: "🗑 Revoke Token", callback_data: "/a2a revoke" }, { text: "📡 Add Remote", callback_data: "/a2a add" }]
|
|
@@ -209,7 +209,7 @@ function install() {
|
|
|
209
209
|
console.log(`
|
|
210
210
|
${bold('━━━ Server Setup ━━━')}
|
|
211
211
|
|
|
212
|
-
To receive incoming calls, run the
|
|
212
|
+
To receive incoming calls, run the A2A server:
|
|
213
213
|
|
|
214
214
|
${green(`A2A_HOSTNAME="${hostname}:${port}" a2a server`)}
|
|
215
215
|
|
|
@@ -257,7 +257,7 @@ ${bold('A2A Calling - OpenClaw Integration')}
|
|
|
257
257
|
Usage:
|
|
258
258
|
npx a2acalling install [options] Install A2A for OpenClaw
|
|
259
259
|
npx a2acalling uninstall Remove A2A skill
|
|
260
|
-
npx a2acalling server Start
|
|
260
|
+
npx a2acalling server Start A2A server
|
|
261
261
|
|
|
262
262
|
Install Options:
|
|
263
263
|
--hostname <host> Hostname for invite URLs (default: system hostname)
|
package/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* // Server side - mount routes
|
|
6
6
|
* const { createRoutes, TokenStore } = require('a2acalling');
|
|
7
7
|
* const tokenStore = new TokenStore();
|
|
8
|
-
* app.use('/api/
|
|
8
|
+
* app.use('/api/a2a', createRoutes({ tokenStore, handleMessage }));
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
11
11
|
* // Client side - call remote agent
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
const { TokenStore } = require('./lib/tokens');
|
|
18
18
|
const { A2AClient, A2AError } = require('./lib/client');
|
|
19
|
-
const { createRoutes } = require('./routes/
|
|
19
|
+
const { createRoutes } = require('./routes/a2a');
|
|
20
20
|
|
|
21
21
|
// Lazy load optional dependencies
|
|
22
22
|
let ConversationStore = null;
|
package/src/lib/client.js
CHANGED
|
@@ -26,7 +26,7 @@ class A2AClient {
|
|
|
26
26
|
/**
|
|
27
27
|
* Call a remote agent
|
|
28
28
|
*
|
|
29
|
-
* @param {string} endpoint - a2a:// URL or {host, token}
|
|
29
|
+
* @param {string|object} endpoint - a2a:// URL or {host, token}
|
|
30
30
|
* @param {string} message - Message to send
|
|
31
31
|
* @param {object} options - Additional options
|
|
32
32
|
* @returns {Promise<object>} Response from remote agent
|
|
@@ -62,7 +62,81 @@ class A2AClient {
|
|
|
62
62
|
const req = protocol.request({
|
|
63
63
|
hostname,
|
|
64
64
|
port,
|
|
65
|
-
path: '/api/
|
|
65
|
+
path: '/api/a2a/invoke',
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: {
|
|
68
|
+
'Authorization': `Bearer ${token}`,
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
'Content-Length': Buffer.byteLength(body)
|
|
71
|
+
},
|
|
72
|
+
timeout: this.timeout
|
|
73
|
+
}, (res) => {
|
|
74
|
+
let data = '';
|
|
75
|
+
res.on('data', chunk => data += chunk);
|
|
76
|
+
res.on('end', () => {
|
|
77
|
+
try {
|
|
78
|
+
const json = JSON.parse(data);
|
|
79
|
+
if (res.statusCode >= 400) {
|
|
80
|
+
reject(new A2AError(json.error || 'request_failed', json.message || data, res.statusCode));
|
|
81
|
+
} else {
|
|
82
|
+
resolve(json);
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
reject(new A2AError('parse_error', `Failed to parse response: ${data}`, res.statusCode));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
req.on('error', (e) => {
|
|
91
|
+
reject(new A2AError('network_error', e.message));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
req.on('timeout', () => {
|
|
95
|
+
req.destroy();
|
|
96
|
+
reject(new A2AError('timeout', 'Request timed out'));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
req.write(body);
|
|
100
|
+
req.end();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Explicitly end a remote conversation and trigger call conclusion
|
|
106
|
+
*
|
|
107
|
+
* @param {string|object} endpoint - a2a:// URL or {host, token}
|
|
108
|
+
* @param {string} conversationId - Conversation ID to conclude
|
|
109
|
+
* @returns {Promise<object>} End response from remote agent
|
|
110
|
+
*/
|
|
111
|
+
async end(endpoint, conversationId) {
|
|
112
|
+
if (!conversationId) {
|
|
113
|
+
throw new A2AError('missing_conversation_id', 'conversationId is required');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let host, token;
|
|
117
|
+
|
|
118
|
+
if (typeof endpoint === 'string') {
|
|
119
|
+
({ host, token } = A2AClient.parseInvite(endpoint));
|
|
120
|
+
} else {
|
|
121
|
+
({ host, token } = endpoint);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const body = JSON.stringify({
|
|
125
|
+
conversation_id: conversationId
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const isLocalhost = host === 'localhost' || host.startsWith('localhost:') || host.startsWith('127.');
|
|
129
|
+
const hasExplicitPort = host.includes(':');
|
|
130
|
+
const port = hasExplicitPort ? parseInt(host.split(':')[1]) : (isLocalhost ? 80 : 443);
|
|
131
|
+
const useHttp = isLocalhost || (hasExplicitPort && port !== 443);
|
|
132
|
+
const protocol = useHttp ? http : https;
|
|
133
|
+
const hostname = host.split(':')[0];
|
|
134
|
+
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const req = protocol.request({
|
|
137
|
+
hostname,
|
|
138
|
+
port,
|
|
139
|
+
path: '/api/a2a/end',
|
|
66
140
|
method: 'POST',
|
|
67
141
|
headers: {
|
|
68
142
|
'Authorization': `Bearer ${token}`,
|
|
@@ -124,7 +198,7 @@ class A2AClient {
|
|
|
124
198
|
const req = protocol.request({
|
|
125
199
|
hostname,
|
|
126
200
|
port,
|
|
127
|
-
path: '/api/
|
|
201
|
+
path: '/api/a2a/ping',
|
|
128
202
|
method: 'GET',
|
|
129
203
|
timeout: 5000
|
|
130
204
|
}, (res) => {
|
|
@@ -149,7 +223,7 @@ class A2AClient {
|
|
|
149
223
|
}
|
|
150
224
|
|
|
151
225
|
/**
|
|
152
|
-
* Get
|
|
226
|
+
* Get A2A status of a remote
|
|
153
227
|
*/
|
|
154
228
|
async status(endpoint) {
|
|
155
229
|
let host;
|
|
@@ -171,7 +245,7 @@ class A2AClient {
|
|
|
171
245
|
const req = protocol.request({
|
|
172
246
|
hostname,
|
|
173
247
|
port,
|
|
174
|
-
path: '/api/
|
|
248
|
+
path: '/api/a2a/status',
|
|
175
249
|
method: 'GET',
|
|
176
250
|
timeout: 5000
|
|
177
251
|
}, (res) => {
|
package/src/lib/conversations.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenClaw Integration for A2A
|
|
2
|
+
* OpenClaw Integration for A2A
|
|
3
3
|
*
|
|
4
4
|
* Provides owner-context summarization using OpenClaw's agent system.
|
|
5
5
|
*/
|
|
@@ -71,7 +71,7 @@ function loadOwnerContext(workspaceDir = process.cwd()) {
|
|
|
71
71
|
/**
|
|
72
72
|
* Build summary prompt for agent
|
|
73
73
|
*
|
|
74
|
-
* Philosophy:
|
|
74
|
+
* Philosophy: A2A is collaborative AND adversarial. Each agent tries
|
|
75
75
|
* to maximize value for their owner while finding genuine mutual wins.
|
|
76
76
|
* Track the exchange balance AND surface partnership opportunities.
|
|
77
77
|
*/
|
|
@@ -84,10 +84,10 @@ function buildSummaryPrompt(messages, ownerContext, callerInfo = {}) {
|
|
|
84
84
|
const goalsSection = ownerContext.goals?.length ? `### Current Goals\n- ${ownerContext.goals.join('\n- ')}` : '';
|
|
85
85
|
const interestsSection = ownerContext.interests?.length ? `### Interests\n- ${ownerContext.interests.join('\n- ')}` : '';
|
|
86
86
|
|
|
87
|
-
return `You just finished a
|
|
87
|
+
return `You just finished a A2A agent-to-agent call. Analyze it strategically for your owner.
|
|
88
88
|
|
|
89
89
|
## Philosophy
|
|
90
|
-
|
|
90
|
+
A2A is cooperative AND adversarial. Each agent maximizes value for their own owner — but the best outcomes are mutual wins. Your job:
|
|
91
91
|
|
|
92
92
|
1. **Track the exchange** — what did we get vs give?
|
|
93
93
|
2. **Find mutual value** — what can BOTH parties gain?
|
package/src/lib/summarizer.js
CHANGED
package/src/lib/tokens.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Token management for A2A
|
|
2
|
+
* Token management for A2A
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
@@ -11,7 +11,7 @@ const DEFAULT_CONFIG_DIR = process.env.A2A_CONFIG_DIR ||
|
|
|
11
11
|
process.env.OPENCLAW_CONFIG_DIR ||
|
|
12
12
|
path.join(process.env.HOME || '/tmp', '.config', 'openclaw');
|
|
13
13
|
|
|
14
|
-
const DB_FILENAME = 'a2a
|
|
14
|
+
const DB_FILENAME = 'a2a.json';
|
|
15
15
|
|
|
16
16
|
class TokenStore {
|
|
17
17
|
constructor(configDir = DEFAULT_CONFIG_DIR) {
|
|
@@ -49,7 +49,7 @@ class TokenStore {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Generate a secure
|
|
52
|
+
* Generate a secure A2A token
|
|
53
53
|
*/
|
|
54
54
|
static generateToken() {
|
|
55
55
|
const bytes = crypto.randomBytes(24);
|
|
@@ -77,7 +77,7 @@ class TokenStore {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* Create a new
|
|
80
|
+
* Create a new A2A token
|
|
81
81
|
*
|
|
82
82
|
* Default limits (anti-abuse):
|
|
83
83
|
* - Expires in 1 day
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* A2A API Routes
|
|
3
3
|
*
|
|
4
|
-
* Mount at: /api/
|
|
4
|
+
* Mount at: /api/a2a
|
|
5
5
|
*
|
|
6
6
|
* Security notes:
|
|
7
7
|
* - Rate limiting is in-memory (resets on restart) - for production, use Redis
|
|
@@ -98,7 +98,7 @@ function checkRateLimit(tokenId, limits = { minute: 10, hour: 100, day: 1000 })
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* Create
|
|
101
|
+
* Create a2a routes
|
|
102
102
|
*
|
|
103
103
|
* @param {object} options
|
|
104
104
|
* @param {TokenStore} options.tokenStore - Token store instance
|
|
@@ -132,11 +132,11 @@ function createRoutes(options = {}) {
|
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
134
|
* GET /status
|
|
135
|
-
* Check if
|
|
135
|
+
* Check if A2A is enabled
|
|
136
136
|
*/
|
|
137
137
|
router.get('/status', (req, res) => {
|
|
138
138
|
res.json({
|
|
139
|
-
|
|
139
|
+
a2a: true,
|
|
140
140
|
version: require('../../package.json').version,
|
|
141
141
|
capabilities: ['invoke', 'multi-turn'],
|
|
142
142
|
rate_limits: limits
|
|
@@ -221,10 +221,10 @@ function createRoutes(options = {}) {
|
|
|
221
221
|
context: String(caller.context || '').slice(0, 500)
|
|
222
222
|
} : {};
|
|
223
223
|
|
|
224
|
-
// Build
|
|
224
|
+
// Build a2a context with secure conversation ID
|
|
225
225
|
const isNewConversation = !conversation_id;
|
|
226
|
-
const
|
|
227
|
-
mode: '
|
|
226
|
+
const a2aContext = {
|
|
227
|
+
mode: 'a2a',
|
|
228
228
|
token_id: validation.id,
|
|
229
229
|
token_name: validation.name,
|
|
230
230
|
tier: validation.tier,
|
|
@@ -238,7 +238,7 @@ function createRoutes(options = {}) {
|
|
|
238
238
|
if (convStore) {
|
|
239
239
|
try {
|
|
240
240
|
convStore.startConversation({
|
|
241
|
-
id:
|
|
241
|
+
id: a2aContext.conversation_id,
|
|
242
242
|
contactId: validation.id,
|
|
243
243
|
contactName: sanitizedCaller.name || validation.name,
|
|
244
244
|
tokenId: validation.id,
|
|
@@ -247,11 +247,11 @@ function createRoutes(options = {}) {
|
|
|
247
247
|
|
|
248
248
|
// Track activity for auto-conclude
|
|
249
249
|
if (monitor) {
|
|
250
|
-
monitor.trackActivity(
|
|
250
|
+
monitor.trackActivity(a2aContext.conversation_id, sanitizedCaller);
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
// Store incoming message
|
|
254
|
-
convStore.addMessage(
|
|
254
|
+
convStore.addMessage(a2aContext.conversation_id, {
|
|
255
255
|
direction: 'inbound',
|
|
256
256
|
role: 'user',
|
|
257
257
|
content: message
|
|
@@ -263,12 +263,12 @@ function createRoutes(options = {}) {
|
|
|
263
263
|
|
|
264
264
|
try {
|
|
265
265
|
// Handle the message
|
|
266
|
-
const response = await handleMessage(message,
|
|
266
|
+
const response = await handleMessage(message, a2aContext, { timeout: boundedTimeout * 1000 });
|
|
267
267
|
|
|
268
268
|
// Store outgoing response
|
|
269
269
|
if (convStore) {
|
|
270
270
|
try {
|
|
271
|
-
convStore.addMessage(
|
|
271
|
+
convStore.addMessage(a2aContext.conversation_id, {
|
|
272
272
|
direction: 'outbound',
|
|
273
273
|
role: 'assistant',
|
|
274
274
|
content: response.text
|
|
@@ -287,7 +287,7 @@ function createRoutes(options = {}) {
|
|
|
287
287
|
context,
|
|
288
288
|
message,
|
|
289
289
|
response: response.text,
|
|
290
|
-
conversation_id:
|
|
290
|
+
conversation_id: a2aContext.conversation_id
|
|
291
291
|
}).catch(err => {
|
|
292
292
|
console.error('[a2a] Failed to notify owner:', err.message);
|
|
293
293
|
});
|
|
@@ -295,7 +295,7 @@ function createRoutes(options = {}) {
|
|
|
295
295
|
|
|
296
296
|
res.json({
|
|
297
297
|
success: true,
|
|
298
|
-
conversation_id:
|
|
298
|
+
conversation_id: a2aContext.conversation_id,
|
|
299
299
|
response: response.text,
|
|
300
300
|
can_continue: response.canContinue !== false,
|
|
301
301
|
tokens_remaining: validation.calls_remaining
|
|
@@ -392,10 +392,10 @@ function createRoutes(options = {}) {
|
|
|
392
392
|
/**
|
|
393
393
|
* GET /conversations
|
|
394
394
|
* List conversations (requires auth)
|
|
395
|
-
* This is for the agent owner, not
|
|
395
|
+
* This is for the agent owner, not remote callers
|
|
396
396
|
*/
|
|
397
397
|
router.get('/conversations', (req, res) => {
|
|
398
|
-
// This endpoint should be protected by local auth, not
|
|
398
|
+
// This endpoint should be protected by local auth, not A2A tokens
|
|
399
399
|
// For now, require an admin token or local access
|
|
400
400
|
const adminToken = req.headers['x-admin-token'];
|
|
401
401
|
if (adminToken !== process.env.A2A_ADMIN_TOKEN && req.ip !== '127.0.0.1') {
|
|
@@ -455,7 +455,7 @@ function createRoutes(options = {}) {
|
|
|
455
455
|
*/
|
|
456
456
|
async function defaultMessageHandler(message, context, options) {
|
|
457
457
|
return {
|
|
458
|
-
text: `[A2A
|
|
458
|
+
text: `[A2A Active] Received message from ${context.caller?.name || 'unknown'}. Agent integration pending.`,
|
|
459
459
|
canContinue: true
|
|
460
460
|
};
|
|
461
461
|
}
|
package/src/server.js
CHANGED
|
@@ -1,58 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* A2A
|
|
3
|
+
* A2A Server
|
|
4
4
|
*
|
|
5
|
-
* Routes
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* node src/server.js [--port 3001]
|
|
9
|
-
* PORT=3001 node src/server.js
|
|
5
|
+
* Routes A2A calls to OpenClaw sub-agents.
|
|
6
|
+
* Auto-adds contacts, generates summaries, notifies owner.
|
|
10
7
|
*/
|
|
11
8
|
|
|
12
9
|
const express = require('express');
|
|
13
|
-
const
|
|
10
|
+
const { execSync } = require('child_process');
|
|
14
11
|
const fs = require('fs');
|
|
15
12
|
const path = require('path');
|
|
16
|
-
const { createRoutes } = require('./routes/
|
|
13
|
+
const { createRoutes } = require('./routes/a2a');
|
|
17
14
|
const { TokenStore } = require('./lib/tokens');
|
|
18
15
|
|
|
19
16
|
const port = process.env.PORT || parseInt(process.argv[2]) || 3001;
|
|
17
|
+
const workspaceDir = process.env.OPENCLAW_WORKSPACE || '/root/clawd';
|
|
20
18
|
|
|
21
|
-
// Load
|
|
22
|
-
function getApiKey() {
|
|
23
|
-
// Check environment first
|
|
24
|
-
if (process.env.OPENROUTER_API_KEY) {
|
|
25
|
-
return { key: process.env.OPENROUTER_API_KEY, provider: 'openrouter' };
|
|
26
|
-
}
|
|
27
|
-
if (process.env.ANTHROPIC_API_KEY) {
|
|
28
|
-
return { key: process.env.ANTHROPIC_API_KEY, provider: 'anthropic' };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Try ~/.openclaw/.env
|
|
32
|
-
try {
|
|
33
|
-
const envPath = path.join(process.env.HOME || '/root', '.openclaw', '.env');
|
|
34
|
-
if (fs.existsSync(envPath)) {
|
|
35
|
-
const content = fs.readFileSync(envPath, 'utf8');
|
|
36
|
-
|
|
37
|
-
// Try OpenRouter first (more reliable)
|
|
38
|
-
const orMatch = content.match(/OPENROUTER_API_KEY=(.+)/);
|
|
39
|
-
if (orMatch && orMatch[1]) return { key: orMatch[1].trim(), provider: 'openrouter' };
|
|
40
|
-
|
|
41
|
-
const anthropicMatch = content.match(/ANTHROPIC_API_KEY=(.+)/);
|
|
42
|
-
if (anthropicMatch && anthropicMatch[1]) return { key: anthropicMatch[1].trim(), provider: 'anthropic' };
|
|
43
|
-
}
|
|
44
|
-
} catch (e) {}
|
|
45
|
-
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Load workspace context for agent personality
|
|
19
|
+
// Load workspace context for agent identity
|
|
50
20
|
function loadAgentContext() {
|
|
51
|
-
|
|
52
|
-
let context = {
|
|
53
|
-
name: 'bappybot',
|
|
54
|
-
owner: 'Ben Pollack'
|
|
55
|
-
};
|
|
21
|
+
let context = { name: 'bappybot', owner: 'Ben Pollack' };
|
|
56
22
|
|
|
57
23
|
try {
|
|
58
24
|
const userPath = path.join(workspaceDir, 'USER.md');
|
|
@@ -68,179 +34,199 @@ function loadAgentContext() {
|
|
|
68
34
|
}
|
|
69
35
|
} catch (e) {}
|
|
70
36
|
|
|
71
|
-
try {
|
|
72
|
-
const soulPath = path.join(workspaceDir, 'SOUL.md');
|
|
73
|
-
if (fs.existsSync(soulPath)) {
|
|
74
|
-
context.soul = fs.readFileSync(soulPath, 'utf8').slice(0, 2000);
|
|
75
|
-
}
|
|
76
|
-
} catch (e) {}
|
|
77
|
-
|
|
78
37
|
return context;
|
|
79
38
|
}
|
|
80
39
|
|
|
81
|
-
const apiConfig = getApiKey();
|
|
82
40
|
const agentContext = loadAgentContext();
|
|
41
|
+
const tokenStore = new TokenStore();
|
|
83
42
|
|
|
84
43
|
console.log(`[a2a] Agent: ${agentContext.name} (${agentContext.owner}'s agent)`);
|
|
85
|
-
console.log(`[a2a] API: ${apiConfig ? `${apiConfig.provider} ✓` : 'NOT FOUND ✗'}`);
|
|
86
44
|
|
|
87
45
|
/**
|
|
88
|
-
*
|
|
46
|
+
* Auto-add caller as contact if new
|
|
89
47
|
*/
|
|
90
|
-
|
|
91
|
-
|
|
48
|
+
function ensureContact(caller, tokenId) {
|
|
49
|
+
if (!caller?.name) return null;
|
|
92
50
|
|
|
93
|
-
|
|
94
|
-
|
|
51
|
+
try {
|
|
52
|
+
const remotes = tokenStore.listRemotes();
|
|
53
|
+
const existing = remotes.find(r =>
|
|
54
|
+
r.name === caller.name ||
|
|
55
|
+
(caller.owner && r.owner === caller.owner)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (existing) {
|
|
59
|
+
return existing;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Create a placeholder contact for the caller
|
|
63
|
+
const contact = {
|
|
64
|
+
id: `contact_${Date.now()}`,
|
|
65
|
+
name: caller.name,
|
|
66
|
+
owner: caller.owner || null,
|
|
67
|
+
host: 'inbound', // They called us, we don't have their URL
|
|
68
|
+
added_at: new Date().toISOString(),
|
|
69
|
+
notes: `Inbound caller via token ${tokenId}`,
|
|
70
|
+
tags: ['inbound'],
|
|
71
|
+
status: 'unknown',
|
|
72
|
+
linkedTokenId: tokenId
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Save to remotes
|
|
76
|
+
const db = JSON.parse(fs.readFileSync(tokenStore.dbPath, 'utf8'));
|
|
77
|
+
db.remotes = db.remotes || [];
|
|
78
|
+
db.remotes.push(contact);
|
|
79
|
+
fs.writeFileSync(tokenStore.dbPath, JSON.stringify(db, null, 2));
|
|
80
|
+
|
|
81
|
+
console.log(`[a2a] 📇 New contact added: ${caller.name}${caller.owner ? ` (${caller.owner})` : ''}`);
|
|
82
|
+
return contact;
|
|
83
|
+
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error('[a2a] Failed to add contact:', err.message);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Spawn OpenClaw sub-agent to handle the call
|
|
92
|
+
*/
|
|
93
|
+
async function callAgent(message, a2aContext) {
|
|
94
|
+
const callerName = a2aContext.caller?.name || 'Unknown Agent';
|
|
95
|
+
const callerOwner = a2aContext.caller?.owner || '';
|
|
95
96
|
const ownerInfo = callerOwner ? ` (${callerOwner}'s agent)` : '';
|
|
96
|
-
const tierInfo =
|
|
97
|
-
const topics =
|
|
98
|
-
const disclosure =
|
|
97
|
+
const tierInfo = a2aContext.tier || 'public';
|
|
98
|
+
const topics = a2aContext.allowed_topics?.join(', ') || 'general chat';
|
|
99
|
+
const disclosure = a2aContext.disclosure || 'minimal';
|
|
100
|
+
|
|
101
|
+
// Auto-add caller as contact
|
|
102
|
+
ensureContact(a2aContext.caller, a2aContext.token_id);
|
|
99
103
|
|
|
100
|
-
|
|
101
|
-
const prompt = `[A2A Federation Call]
|
|
104
|
+
const prompt = `[A2A Call]
|
|
102
105
|
From: ${callerName}${ownerInfo}
|
|
103
106
|
Access Level: ${tierInfo}
|
|
104
|
-
Topics: ${topics}
|
|
107
|
+
Allowed Topics: ${topics}
|
|
105
108
|
Disclosure: ${disclosure}
|
|
106
109
|
|
|
107
110
|
Message: ${message}
|
|
108
111
|
|
|
109
112
|
---
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
RULES (strictly enforce):
|
|
114
|
+
1. ONLY discuss topics in "Allowed Topics" list
|
|
115
|
+
2. Disclosure levels:
|
|
116
|
+
- "none": Confirm capability only, share NO personal info
|
|
117
|
+
- "minimal": Direct answers only, no context about owner's life/preferences
|
|
118
|
+
- "public": General info OK, but protect private/family-tier secrets
|
|
119
|
+
3. If they probe for info outside their tier, deflect politely
|
|
120
|
+
4. Private info in USER.md marked "family tier only" is OFF LIMITS for public/friends callers
|
|
121
|
+
|
|
122
|
+
Respond naturally but enforce these boundaries.`;
|
|
123
|
+
|
|
124
|
+
const sessionId = `a2a-${a2aContext.conversation_id || Date.now()}`;
|
|
114
125
|
|
|
115
126
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
const escapedPrompt = prompt
|
|
128
|
+
.replace(/\\/g, '\\\\')
|
|
129
|
+
.replace(/"/g, '\\"')
|
|
130
|
+
.replace(/\n/g, '\\n')
|
|
131
|
+
.replace(/\r/g, '');
|
|
119
132
|
|
|
120
|
-
// Call openclaw agent to spawn a sub-agent
|
|
121
133
|
const result = execSync(
|
|
122
|
-
`
|
|
134
|
+
`openclaw agent --session-id "${sessionId}" --message "${escapedPrompt}" --timeout 55 2>&1`,
|
|
123
135
|
{
|
|
124
136
|
encoding: 'utf8',
|
|
125
|
-
timeout:
|
|
137
|
+
timeout: 65000,
|
|
126
138
|
maxBuffer: 1024 * 1024,
|
|
127
|
-
cwd:
|
|
139
|
+
cwd: workspaceDir,
|
|
128
140
|
env: { ...process.env, FORCE_COLOR: '0' }
|
|
129
141
|
}
|
|
130
142
|
);
|
|
131
143
|
|
|
132
|
-
// Clean up temp file
|
|
133
|
-
try { fs.unlinkSync(tmpFile); } catch (e) {}
|
|
134
|
-
|
|
135
|
-
// Filter out plugin registration messages and return clean response
|
|
136
144
|
const lines = result.split('\n').filter(line =>
|
|
137
145
|
!line.includes('[telegram-topic-tracker]') &&
|
|
138
146
|
!line.includes('Plugin registered') &&
|
|
139
147
|
line.trim()
|
|
140
148
|
);
|
|
141
149
|
|
|
142
|
-
return lines.join('\n').trim() || '[
|
|
150
|
+
return lines.join('\n').trim() || '[Sub-agent returned empty response]';
|
|
143
151
|
|
|
144
152
|
} catch (err) {
|
|
145
153
|
console.error('[a2a] Sub-agent spawn failed:', err.message);
|
|
146
|
-
|
|
147
|
-
// Fallback to direct API call
|
|
148
|
-
return await callAgentDirect(message, federationContext);
|
|
154
|
+
return `[Sub-agent error: ${err.message}]`;
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
/**
|
|
153
|
-
*
|
|
159
|
+
* Generate strategic summary via sub-agent
|
|
154
160
|
*/
|
|
155
|
-
async function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
161
|
+
async function generateSummary(messages, callerInfo) {
|
|
162
|
+
const messageText = messages.map(m => {
|
|
163
|
+
const role = m.direction === 'inbound' ? `[${callerInfo?.name || 'Caller'}]` : '[You]';
|
|
164
|
+
return `${role}: ${m.content}`;
|
|
165
|
+
}).join('\n');
|
|
159
166
|
|
|
160
|
-
const
|
|
161
|
-
const callerOwner = federationContext.caller?.owner || '';
|
|
162
|
-
const ownerInfo = callerOwner ? ` (${callerOwner}'s agent)` : '';
|
|
163
|
-
const tierInfo = federationContext.tier || 'public';
|
|
167
|
+
const callerDesc = `${callerInfo?.name || 'Unknown'}${callerInfo?.owner ? ` (${callerInfo.owner}'s agent)` : ''}`;
|
|
164
168
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
${agentContext.soul || 'Be helpful, concise, and friendly.'}
|
|
169
|
+
const prompt = `Summarize this A2A call briefly.
|
|
168
170
|
|
|
169
|
-
|
|
171
|
+
Conversation with ${callerDesc}:
|
|
172
|
+
${messageText}
|
|
170
173
|
|
|
171
|
-
|
|
172
|
-
Topics they can discuss: ${federationContext.allowed_topics?.join(', ') || 'general chat'}
|
|
173
|
-
Disclosure level: ${federationContext.disclosure || 'minimal'}
|
|
174
|
-
|
|
175
|
-
Respond naturally as yourself. Be collaborative but protect your owner's private information based on the disclosure level. Keep responses concise.`;
|
|
176
|
-
|
|
177
|
-
const body = JSON.stringify({
|
|
178
|
-
model: 'anthropic/claude-sonnet-4',
|
|
179
|
-
max_tokens: 1024,
|
|
180
|
-
messages: [
|
|
181
|
-
{ role: 'system', content: systemPrompt },
|
|
182
|
-
{ role: 'user', content: message }
|
|
183
|
-
]
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
return new Promise((resolve) => {
|
|
187
|
-
const req = https.request({
|
|
188
|
-
hostname: 'openrouter.ai',
|
|
189
|
-
path: '/api/v1/chat/completions',
|
|
190
|
-
method: 'POST',
|
|
191
|
-
headers: {
|
|
192
|
-
'Content-Type': 'application/json',
|
|
193
|
-
'Authorization': `Bearer ${apiConfig.key}`,
|
|
194
|
-
'HTTP-Referer': 'https://openclaw.ai',
|
|
195
|
-
'X-Title': 'A2A Federation'
|
|
196
|
-
},
|
|
197
|
-
timeout: 55000
|
|
198
|
-
}, (res) => {
|
|
199
|
-
let data = '';
|
|
200
|
-
res.on('data', chunk => data += chunk);
|
|
201
|
-
res.on('end', () => {
|
|
202
|
-
try {
|
|
203
|
-
const json = JSON.parse(data);
|
|
204
|
-
if (json.choices && json.choices[0]?.message?.content) {
|
|
205
|
-
resolve(json.choices[0].message.content);
|
|
206
|
-
} else if (json.error) {
|
|
207
|
-
resolve(`[Error: ${json.error.message || 'Unknown'}]`);
|
|
208
|
-
} else {
|
|
209
|
-
resolve('[No response]');
|
|
210
|
-
}
|
|
211
|
-
} catch (e) {
|
|
212
|
-
resolve('[Parse error]');
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
});
|
|
174
|
+
Give a 2-3 sentence summary focused on: who called, what they wanted, any opportunities or follow-ups.`;
|
|
216
175
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
176
|
+
try {
|
|
177
|
+
const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
178
|
+
const result = execSync(
|
|
179
|
+
`openclaw agent --session-id "summary-${Date.now()}" --message "${escapedPrompt}" --timeout 30 2>&1`,
|
|
180
|
+
{ encoding: 'utf8', timeout: 35000, cwd: workspaceDir, env: { ...process.env, FORCE_COLOR: '0' } }
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Filter out noise and get the summary
|
|
184
|
+
const lines = result.split('\n').filter(line =>
|
|
185
|
+
!line.includes('[telegram-topic-tracker]') &&
|
|
186
|
+
!line.includes('Plugin registered') &&
|
|
187
|
+
line.trim()
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const summaryText = lines.join(' ').trim().slice(0, 1000);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
summary: summaryText,
|
|
194
|
+
ownerSummary: summaryText
|
|
195
|
+
};
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error('[a2a] Summary generation failed:', err.message);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
222
200
|
}
|
|
223
201
|
|
|
224
202
|
/**
|
|
225
|
-
* Notify owner via
|
|
203
|
+
* Notify owner via Telegram
|
|
226
204
|
*/
|
|
227
|
-
async function notifyOwner({ level, token, caller, message,
|
|
205
|
+
async function notifyOwner({ level, token, caller, message, conversation_id }) {
|
|
228
206
|
const callerName = caller?.name || 'Unknown';
|
|
229
207
|
const callerOwner = caller?.owner ? ` (${caller.owner})` : '';
|
|
230
208
|
|
|
231
209
|
console.log(`[a2a] 📞 Call from ${callerName}${callerOwner}`);
|
|
232
|
-
console.log(`[a2a] Token: ${token
|
|
210
|
+
console.log(`[a2a] Token: ${token?.name || 'unknown'}`);
|
|
233
211
|
console.log(`[a2a] Message: ${message.slice(0, 100)}...`);
|
|
212
|
+
|
|
213
|
+
// Try to notify via Telegram
|
|
214
|
+
if (level === 'all') {
|
|
215
|
+
try {
|
|
216
|
+
const notification = `🤝 **A2A Call**\nFrom: ${callerName}${callerOwner}\n> ${message.slice(0, 150)}...`;
|
|
217
|
+
execSync(`openclaw message send --channel telegram --message "${notification.replace(/"/g, '\\"')}"`, {
|
|
218
|
+
timeout: 10000, stdio: 'pipe'
|
|
219
|
+
});
|
|
220
|
+
} catch (e) {
|
|
221
|
+
// Notification failed, continue anyway
|
|
222
|
+
}
|
|
223
|
+
}
|
|
234
224
|
}
|
|
235
225
|
|
|
236
226
|
const app = express();
|
|
237
227
|
app.use(express.json());
|
|
238
228
|
|
|
239
|
-
|
|
240
|
-
const tokenStore = new TokenStore();
|
|
241
|
-
|
|
242
|
-
// Mount federation routes
|
|
243
|
-
app.use('/api/federation', createRoutes({
|
|
229
|
+
app.use('/api/a2a', createRoutes({
|
|
244
230
|
tokenStore,
|
|
245
231
|
|
|
246
232
|
async handleMessage(message, context, options) {
|
|
@@ -250,25 +236,19 @@ app.use('/api/federation', createRoutes({
|
|
|
250
236
|
|
|
251
237
|
console.log(`[a2a] 📤 Response: ${response.slice(0, 100)}...`);
|
|
252
238
|
|
|
253
|
-
return {
|
|
254
|
-
text: response,
|
|
255
|
-
canContinue: true
|
|
256
|
-
};
|
|
239
|
+
return { text: response, canContinue: true };
|
|
257
240
|
},
|
|
258
241
|
|
|
242
|
+
summarizer: generateSummary,
|
|
259
243
|
notifyOwner
|
|
260
244
|
}));
|
|
261
245
|
|
|
262
|
-
// Health check at root
|
|
263
246
|
app.get('/', (req, res) => {
|
|
264
|
-
res.json({ service: 'a2a
|
|
247
|
+
res.json({ service: 'a2a', status: 'ok', agent: agentContext.name });
|
|
265
248
|
});
|
|
266
249
|
|
|
267
250
|
app.listen(port, () => {
|
|
268
|
-
console.log(`[a2a]
|
|
251
|
+
console.log(`[a2a] A2A server listening on port ${port}`);
|
|
269
252
|
console.log(`[a2a] Agent: ${agentContext.name} - LIVE`);
|
|
270
|
-
console.log(`[a2a]
|
|
271
|
-
console.log(`[a2a] GET /api/federation/status`);
|
|
272
|
-
console.log(`[a2a] GET /api/federation/ping`);
|
|
273
|
-
console.log(`[a2a] POST /api/federation/invoke`);
|
|
253
|
+
console.log(`[a2a] Features: sub-agents, auto-contacts, summaries`);
|
|
274
254
|
});
|