a2acalling 0.1.2 → 0.1.4

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/server.js +229 -11
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 🤝 A2A Calling
2
2
 
3
- **Agent-to-Agent calling with OpenClaw support. Let your people talk to my people!**
3
+ **Agent-to-Agent calling with OpenClaw support. "I'll have my people call your people!"**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/a2acalling.svg)](https://www.npmjs.com/package/a2acalling)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -304,4 +304,4 @@ MIT — go build something cool.
304
304
 
305
305
  ---
306
306
 
307
- *Let your people talk to my people.* 🤝
307
+ *I'll have my people call your people.* 🤝
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Agent-to-agent calling for OpenClaw - federated agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/server.js CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * A2A Federation Server
4
4
  *
5
- * Standalone server for testing or running alongside OpenClaw.
5
+ * Routes federation calls to an LLM agent.
6
6
  *
7
7
  * Usage:
8
8
  * node src/server.js [--port 3001]
@@ -10,11 +10,229 @@
10
10
  */
11
11
 
12
12
  const express = require('express');
13
+ const https = require('https');
14
+ const fs = require('fs');
15
+ const path = require('path');
13
16
  const { createRoutes } = require('./routes/federation');
14
17
  const { TokenStore } = require('./lib/tokens');
15
18
 
16
19
  const port = process.env.PORT || parseInt(process.argv[2]) || 3001;
17
20
 
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
50
+ function loadAgentContext() {
51
+ const workspaceDir = process.env.OPENCLAW_WORKSPACE || '/root/clawd';
52
+ let context = {
53
+ name: 'bappybot',
54
+ owner: 'Ben Pollack'
55
+ };
56
+
57
+ try {
58
+ const userPath = path.join(workspaceDir, 'USER.md');
59
+ if (fs.existsSync(userPath)) {
60
+ const content = fs.readFileSync(userPath, 'utf8');
61
+ const nameMatch = content.match(/\*\*Name:\*\*\s*([^\n]+)/);
62
+ if (nameMatch) {
63
+ const name = nameMatch[1].trim();
64
+ if (name && !name.includes('_') && !name.includes('(')) {
65
+ context.owner = name;
66
+ }
67
+ }
68
+ }
69
+ } catch (e) {}
70
+
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
+ return context;
79
+ }
80
+
81
+ const apiConfig = getApiKey();
82
+ const agentContext = loadAgentContext();
83
+
84
+ console.log(`[a2a] Agent: ${agentContext.name} (${agentContext.owner}'s agent)`);
85
+ console.log(`[a2a] API: ${apiConfig ? `${apiConfig.provider} ✓` : 'NOT FOUND ✗'}`);
86
+
87
+ /**
88
+ * Call agent via OpenClaw sub-agent (full tool access)
89
+ */
90
+ async function callAgent(message, federationContext) {
91
+ const { execSync } = require('child_process');
92
+
93
+ const callerName = federationContext.caller?.name || 'Unknown Agent';
94
+ const callerOwner = federationContext.caller?.owner || '';
95
+ 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';
99
+
100
+ // Build the federation context for the sub-agent
101
+ const prompt = `[A2A Federation Call]
102
+ From: ${callerName}${ownerInfo}
103
+ Access Level: ${tierInfo}
104
+ Topics: ${topics}
105
+ Disclosure: ${disclosure}
106
+
107
+ Message: ${message}
108
+
109
+ ---
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()}`;
114
+
115
+ try {
116
+ // Write prompt to temp file to avoid shell escaping issues
117
+ const tmpFile = `/tmp/a2a-${Date.now()}.txt`;
118
+ fs.writeFileSync(tmpFile, prompt);
119
+
120
+ // Call openclaw agent to spawn a sub-agent
121
+ const result = execSync(
122
+ `cat "${tmpFile}" | openclaw agent --session-id "${sessionId}" --timeout 55 2>/dev/null`,
123
+ {
124
+ encoding: 'utf8',
125
+ timeout: 60000,
126
+ maxBuffer: 1024 * 1024,
127
+ cwd: process.env.OPENCLAW_WORKSPACE || '/root/clawd',
128
+ env: { ...process.env, FORCE_COLOR: '0' }
129
+ }
130
+ );
131
+
132
+ // Clean up temp file
133
+ try { fs.unlinkSync(tmpFile); } catch (e) {}
134
+
135
+ // Filter out plugin registration messages and return clean response
136
+ const lines = result.split('\n').filter(line =>
137
+ !line.includes('[telegram-topic-tracker]') &&
138
+ !line.includes('Plugin registered') &&
139
+ line.trim()
140
+ );
141
+
142
+ return lines.join('\n').trim() || '[No response]';
143
+
144
+ } catch (err) {
145
+ console.error('[a2a] Sub-agent spawn failed:', err.message);
146
+
147
+ // Fallback to direct API call
148
+ return await callAgentDirect(message, federationContext);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Fallback: Call LLM directly via OpenRouter
154
+ */
155
+ async function callAgentDirect(message, federationContext) {
156
+ if (!apiConfig) {
157
+ return '[Agent configuration error: No API key available]';
158
+ }
159
+
160
+ const callerName = federationContext.caller?.name || 'Unknown Agent';
161
+ const callerOwner = federationContext.caller?.owner || '';
162
+ const ownerInfo = callerOwner ? ` (${callerOwner}'s agent)` : '';
163
+ const tierInfo = federationContext.tier || 'public';
164
+
165
+ const systemPrompt = `You are ${agentContext.name}, ${agentContext.owner}'s AI agent.
166
+
167
+ ${agentContext.soul || 'Be helpful, concise, and friendly.'}
168
+
169
+ You're receiving a federated call from another AI agent: ${callerName}${ownerInfo}.
170
+
171
+ Their access level: ${tierInfo}
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
+ });
216
+
217
+ req.on('error', () => resolve('[Agent unavailable]'));
218
+ req.on('timeout', () => { req.destroy(); resolve('[Timeout]'); });
219
+ req.write(body);
220
+ req.end();
221
+ });
222
+ }
223
+
224
+ /**
225
+ * Notify owner via console (Telegram notification handled by OpenClaw)
226
+ */
227
+ async function notifyOwner({ level, token, caller, message, response, conversation_id }) {
228
+ const callerName = caller?.name || 'Unknown';
229
+ const callerOwner = caller?.owner ? ` (${caller.owner})` : '';
230
+
231
+ console.log(`[a2a] 📞 Call from ${callerName}${callerOwner}`);
232
+ console.log(`[a2a] Token: ${token.name}`);
233
+ console.log(`[a2a] Message: ${message.slice(0, 100)}...`);
234
+ }
235
+
18
236
  const app = express();
19
237
  app.use(express.json());
20
238
 
@@ -25,30 +243,30 @@ const tokenStore = new TokenStore();
25
243
  app.use('/api/federation', createRoutes({
26
244
  tokenStore,
27
245
 
28
- // Default message handler - in production, this connects to the agent
29
246
  async handleMessage(message, context, options) {
30
- console.log(`[a2a] Received message from ${context.caller?.name || 'unknown'}: ${message}`);
247
+ console.log(`[a2a] 📞 Incoming from ${context.caller?.name || 'unknown'}`);
248
+
249
+ const response = await callAgent(message, context);
250
+
251
+ console.log(`[a2a] 📤 Response: ${response.slice(0, 100)}...`);
252
+
31
253
  return {
32
- text: `[A2A Federation Active] Received: "${message}". Full agent integration pending.`,
254
+ text: response,
33
255
  canContinue: true
34
256
  };
35
257
  },
36
258
 
37
- // Default owner notification - in production, this sends to chat
38
- async notifyOwner({ level, token, caller, message, response }) {
39
- console.log(`[a2a] Notification (${level}): ${caller?.name || 'unknown'} called via token "${token.name}"`);
40
- console.log(`[a2a] Message: ${message}`);
41
- console.log(`[a2a] Response: ${response}`);
42
- }
259
+ notifyOwner
43
260
  }));
44
261
 
45
262
  // Health check at root
46
263
  app.get('/', (req, res) => {
47
- res.json({ service: 'a2a-federation', status: 'ok' });
264
+ res.json({ service: 'a2a-federation', status: 'ok', agent: agentContext.name });
48
265
  });
49
266
 
50
267
  app.listen(port, () => {
51
268
  console.log(`[a2a] Federation server listening on port ${port}`);
269
+ console.log(`[a2a] Agent: ${agentContext.name} - LIVE`);
52
270
  console.log(`[a2a] Endpoints:`);
53
271
  console.log(`[a2a] GET /api/federation/status`);
54
272
  console.log(`[a2a] GET /api/federation/ping`);