a2acalling 0.1.5 → 0.1.7

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 CHANGED
@@ -24,7 +24,7 @@ a2acalling/
24
24
  │ │ ├── tokens.js # Token storage & validation
25
25
  │ │ └── client.js # Outbound A2A client
26
26
  │ └── routes/
27
- │ └── federation.js # Express routes for /api/federation
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-federation.json`
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/federation`
57
- 2. **Agent tool**: Add `federation_call` tool using `A2AClient`
58
- 3. **Commands**: Wire `/federation` commands to CLI functions
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/federation/invoke` from remote agents
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: /federation create --name "Alice" --expires 7d
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/federation/invoke
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/federation.js` - Express router (mount at `/api/federation`)
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 federation protocol?"
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 federation work"],
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 serve [options] # Start federation server
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/federation/status` | Check federation support |
191
- | `GET` | `/api/federation/ping` | Health check with auth |
192
- | `POST` | `/api/federation/invoke` | Call the agent |
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/federation', createRoutes({
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
- Federation is **cooperative AND adversarial**. Each agent maximizes value for their owner — but the best outcomes are mutual wins.
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 federation 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 federation tokens, managing remote agent access, or calling other OpenClaw agents."
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/federation",
21
- "tools": ["federation_call"],
20
+ "routes": "/api/a2a",
21
+ "tools": ["a2a_call"],
22
22
  },
23
23
  }
24
24
  ---
25
25
 
26
- # A2A Federation
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: `/federation create`, "create a federation token", "let another agent call me"
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 federated agent:
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 federation_call tool
75
- federation_call({
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 a federation call, the agent operates within the token's permission scope:
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
- 🤝 Federation call received
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 a federation token
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(`✅ Federation token created\n`);
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 federation tokens.');
170
+ console.log('No active A2A tokens.');
171
171
  return;
172
172
  }
173
173
 
174
- console.log('Active federation tokens:\n');
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(`Federation status for ${url}:\n`);
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 federation server on port ${port}...`);
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/federation/ping',
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 a federation token
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 federation status
867
+ status <url> Get A2A status
868
868
 
869
869
  Server:
870
- server Start the federation server
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
- # Federation Protocol v0 Reference
1
+ # A2A Protocol v0 Reference
2
2
 
3
3
  ## Overview
4
4
 
5
- Federation enables OpenClaw agents to call each other across instances with scoped permissions and owner notification.
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/federation/`
19
+ All endpoints are prefixed with `/api/a2a/`
20
20
 
21
21
  ### GET /status
22
22
 
23
- Check if federation is enabled.
23
+ Check if A2A is enabled.
24
24
 
25
25
  Response:
26
26
  ```json
27
27
  {
28
- "federation": true,
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/federation.json`:
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 federation calls should use HTTPS
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
- 🤝 Federation call received
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 federation = require('./skills/federation/scripts/server');
203
- app.use('/api/federation', federation);
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 a federation call, inject context:
246
+ When handling an A2A call, inject context:
209
247
  ```json
210
248
  {
211
- "federation": {
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: federation_call
258
+ ### New Tool: a2a_call
221
259
 
222
260
  ```typescript
223
- federation_call({
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.5",
4
- "description": "Agent-to-agent calling for OpenClaw - federated agent communication",
3
+ "version": "0.1.7",
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 federation server as a systemd service (optional)
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 federation. Handle /a2a commands to create tokens, manage connections, and call remote agents. Triggers on: /a2a, federation, agent token, a2a invite."
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 Federation
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 Federation**\\n\\nWhat would you like to do?",
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 a2a server:
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 federation server
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/federation', createRoutes({ tokenStore, handleMessage }));
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/federation');
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/federation/invoke',
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/federation/ping',
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 federation status of a remote
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/federation/status',
248
+ path: '/api/a2a/status',
175
249
  method: 'GET',
176
250
  timeout: 5000
177
251
  }, (res) => {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Conversation storage and summarization for A2A federation
2
+ * Conversation storage and summarization for A2A
3
3
  *
4
4
  * Uses SQLite for local storage with auto-summarization on call conclusion.
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * OpenClaw Integration for A2A Federation
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: Federation is collaborative AND adversarial. Each agent tries
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 federated agent-to-agent call. Analyze it strategically for your owner.
87
+ return `You just finished a A2A agent-to-agent call. Analyze it strategically for your owner.
88
88
 
89
89
  ## Philosophy
90
- Federation is cooperative AND adversarial. Each agent maximizes value for their own owner — but the best outcomes are mutual wins. Your job:
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?
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Conversation summarizer for A2A federation
2
+ * Conversation summarizer for A2A
3
3
  *
4
4
  * Provides a default summarizer interface and a simple implementation.
5
5
  * OpenClaw installations can provide their own summarizer via config.
package/src/lib/tokens.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Token management for A2A federation
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-federation.json';
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 federation token
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 federation token
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
- * Federation API Routes
2
+ * A2A API Routes
3
3
  *
4
- * Mount at: /api/federation
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 federation routes
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 federation is enabled
135
+ * Check if A2A is enabled
136
136
  */
137
137
  router.get('/status', (req, res) => {
138
138
  res.json({
139
- federation: true,
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 federation context with secure conversation ID
224
+ // Build a2a context with secure conversation ID
225
225
  const isNewConversation = !conversation_id;
226
- const federationContext = {
227
- mode: 'federation',
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: federationContext.conversation_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(federationContext.conversation_id, sanitizedCaller);
250
+ monitor.trackActivity(a2aContext.conversation_id, sanitizedCaller);
251
251
  }
252
252
 
253
253
  // Store incoming message
254
- convStore.addMessage(federationContext.conversation_id, {
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, federationContext, { timeout: boundedTimeout * 1000 });
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(federationContext.conversation_id, {
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: federationContext.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: federationContext.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 federated callers
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 federation tokens
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 Federation Active] Received message from ${context.caller?.name || 'unknown'}. Agent integration pending.`,
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 Federation Server
3
+ * A2A Server
4
4
  *
5
- * Routes federation calls to an LLM agent.
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 https = require('https');
10
+ const { execSync } = require('child_process');
14
11
  const fs = require('fs');
15
12
  const path = require('path');
16
- const { createRoutes } = require('./routes/federation');
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 API key from various sources
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
- const workspaceDir = process.env.OPENCLAW_WORKSPACE || '/root/clawd';
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,186 +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
- * Call agent via OpenClaw sub-agent (full tool access)
46
+ * Auto-add caller as contact if new
89
47
  */
90
- async function callAgent(message, federationContext) {
91
- const { execSync } = require('child_process');
48
+ function ensureContact(caller, tokenId) {
49
+ if (!caller?.name) return null;
92
50
 
93
- const callerName = federationContext.caller?.name || 'Unknown Agent';
94
- const callerOwner = federationContext.caller?.owner || '';
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 = federationContext.tier || 'public';
97
- const topics = federationContext.allowed_topics?.join(', ') || 'general chat';
98
- const disclosure = federationContext.disclosure || 'minimal';
97
+ const tierInfo = a2aContext.tier || 'public';
98
+ const topics = a2aContext.allowed_topics?.join(', ') || 'general chat';
99
+ const disclosure = a2aContext.disclosure || 'minimal';
99
100
 
100
- // Build the federation context for the sub-agent
101
- const prompt = `[A2A Federation Call]
101
+ // Auto-add caller as contact
102
+ ensureContact(a2aContext.caller, a2aContext.token_id);
103
+
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
- Respond to this federated agent call. Be yourself - collaborative but protect private info based on disclosure level. Keep response concise (under 500 chars).`;
111
-
112
- // Use a unique session ID for this conversation
113
- const sessionId = `a2a-${federationContext.conversation_id || Date.now()}`;
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
- // Escape the prompt for shell (replace quotes and newlines)
117
127
  const escapedPrompt = prompt
118
128
  .replace(/\\/g, '\\\\')
119
129
  .replace(/"/g, '\\"')
120
130
  .replace(/\n/g, '\\n')
121
131
  .replace(/\r/g, '');
122
132
 
123
- // Call openclaw agent to spawn a sub-agent
124
133
  const result = execSync(
125
134
  `openclaw agent --session-id "${sessionId}" --message "${escapedPrompt}" --timeout 55 2>&1`,
126
135
  {
127
136
  encoding: 'utf8',
128
137
  timeout: 65000,
129
138
  maxBuffer: 1024 * 1024,
130
- cwd: process.env.OPENCLAW_WORKSPACE || '/root/clawd',
139
+ cwd: workspaceDir,
131
140
  env: { ...process.env, FORCE_COLOR: '0' }
132
141
  }
133
142
  );
134
143
 
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
- const response = lines.join('\n').trim();
143
-
144
- if (!response || response.includes('error') || response.includes('failed')) {
145
- console.error('[a2a] Sub-agent returned error, using fallback');
146
- return await callAgentDirect(message, federationContext);
147
- }
148
-
149
- return response;
150
+ return lines.join('\n').trim() || '[Sub-agent returned empty response]';
150
151
 
151
152
  } catch (err) {
152
153
  console.error('[a2a] Sub-agent spawn failed:', err.message);
153
-
154
- // Fallback to direct API call only if sub-agent completely fails
155
- return await callAgentDirect(message, federationContext);
154
+ return `[Sub-agent error: ${err.message}]`;
156
155
  }
157
156
  }
158
157
 
159
158
  /**
160
- * Fallback: Call LLM directly via OpenRouter
159
+ * Generate strategic summary via sub-agent
161
160
  */
162
- async function callAgentDirect(message, federationContext) {
163
- if (!apiConfig) {
164
- return '[Agent configuration error: No API key available]';
165
- }
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');
166
166
 
167
- const callerName = federationContext.caller?.name || 'Unknown Agent';
168
- const callerOwner = federationContext.caller?.owner || '';
169
- const ownerInfo = callerOwner ? ` (${callerOwner}'s agent)` : '';
170
- const tierInfo = federationContext.tier || 'public';
167
+ const callerDesc = `${callerInfo?.name || 'Unknown'}${callerInfo?.owner ? ` (${callerInfo.owner}'s agent)` : ''}`;
171
168
 
172
- const systemPrompt = `You are ${agentContext.name}, ${agentContext.owner}'s AI agent.
173
-
174
- ${agentContext.soul || 'Be helpful, concise, and friendly.'}
169
+ const prompt = `Summarize this A2A call briefly.
175
170
 
176
- You're receiving a federated call from another AI agent: ${callerName}${ownerInfo}.
171
+ Conversation with ${callerDesc}:
172
+ ${messageText}
177
173
 
178
- Their access level: ${tierInfo}
179
- Topics they can discuss: ${federationContext.allowed_topics?.join(', ') || 'general chat'}
180
- Disclosure level: ${federationContext.disclosure || 'minimal'}
174
+ Give a 2-3 sentence summary focused on: who called, what they wanted, any opportunities or follow-ups.`;
181
175
 
182
- Respond naturally as yourself. Be collaborative but protect your owner's private information based on the disclosure level. Keep responses concise.`;
183
-
184
- const body = JSON.stringify({
185
- model: 'anthropic/claude-sonnet-4',
186
- max_tokens: 1024,
187
- messages: [
188
- { role: 'system', content: systemPrompt },
189
- { role: 'user', content: message }
190
- ]
191
- });
192
-
193
- return new Promise((resolve) => {
194
- const req = https.request({
195
- hostname: 'openrouter.ai',
196
- path: '/api/v1/chat/completions',
197
- method: 'POST',
198
- headers: {
199
- 'Content-Type': 'application/json',
200
- 'Authorization': `Bearer ${apiConfig.key}`,
201
- 'HTTP-Referer': 'https://openclaw.ai',
202
- 'X-Title': 'A2A Federation'
203
- },
204
- timeout: 55000
205
- }, (res) => {
206
- let data = '';
207
- res.on('data', chunk => data += chunk);
208
- res.on('end', () => {
209
- try {
210
- const json = JSON.parse(data);
211
- if (json.choices && json.choices[0]?.message?.content) {
212
- resolve(json.choices[0].message.content);
213
- } else if (json.error) {
214
- resolve(`[Error: ${json.error.message || 'Unknown'}]`);
215
- } else {
216
- resolve('[No response]');
217
- }
218
- } catch (e) {
219
- resolve('[Parse error]');
220
- }
221
- });
222
- });
223
-
224
- req.on('error', () => resolve('[Agent unavailable]'));
225
- req.on('timeout', () => { req.destroy(); resolve('[Timeout]'); });
226
- req.write(body);
227
- req.end();
228
- });
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
+ }
229
200
  }
230
201
 
231
202
  /**
232
- * Notify owner via console (Telegram notification handled by OpenClaw)
203
+ * Notify owner via Telegram
233
204
  */
234
- async function notifyOwner({ level, token, caller, message, response, conversation_id }) {
205
+ async function notifyOwner({ level, token, caller, message, conversation_id }) {
235
206
  const callerName = caller?.name || 'Unknown';
236
207
  const callerOwner = caller?.owner ? ` (${caller.owner})` : '';
237
208
 
238
209
  console.log(`[a2a] 📞 Call from ${callerName}${callerOwner}`);
239
- console.log(`[a2a] Token: ${token.name}`);
210
+ console.log(`[a2a] Token: ${token?.name || 'unknown'}`);
240
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
+ }
241
224
  }
242
225
 
243
226
  const app = express();
244
227
  app.use(express.json());
245
228
 
246
- // Initialize token store
247
- const tokenStore = new TokenStore();
248
-
249
- // Mount federation routes
250
- app.use('/api/federation', createRoutes({
229
+ app.use('/api/a2a', createRoutes({
251
230
  tokenStore,
252
231
 
253
232
  async handleMessage(message, context, options) {
@@ -257,25 +236,19 @@ app.use('/api/federation', createRoutes({
257
236
 
258
237
  console.log(`[a2a] 📤 Response: ${response.slice(0, 100)}...`);
259
238
 
260
- return {
261
- text: response,
262
- canContinue: true
263
- };
239
+ return { text: response, canContinue: true };
264
240
  },
265
241
 
242
+ summarizer: generateSummary,
266
243
  notifyOwner
267
244
  }));
268
245
 
269
- // Health check at root
270
246
  app.get('/', (req, res) => {
271
- res.json({ service: 'a2a-federation', status: 'ok', agent: agentContext.name });
247
+ res.json({ service: 'a2a', status: 'ok', agent: agentContext.name });
272
248
  });
273
249
 
274
250
  app.listen(port, () => {
275
- console.log(`[a2a] Federation server listening on port ${port}`);
251
+ console.log(`[a2a] A2A server listening on port ${port}`);
276
252
  console.log(`[a2a] Agent: ${agentContext.name} - LIVE`);
277
- console.log(`[a2a] Endpoints:`);
278
- console.log(`[a2a] GET /api/federation/status`);
279
- console.log(`[a2a] GET /api/federation/ping`);
280
- console.log(`[a2a] POST /api/federation/invoke`);
253
+ console.log(`[a2a] Features: sub-agents, auto-contacts, summaries`);
281
254
  });