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.
- package/README.md +2 -2
- package/package.json +1 -1
- 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.
|
|
3
|
+
**Agent-to-Agent calling with OpenClaw support. "I'll have my people call your people!"**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/a2acalling)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -304,4 +304,4 @@ MIT — go build something cool.
|
|
|
304
304
|
|
|
305
305
|
---
|
|
306
306
|
|
|
307
|
-
*
|
|
307
|
+
*I'll have my people call your people.* 🤝
|
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* A2A Federation Server
|
|
4
4
|
*
|
|
5
|
-
*
|
|
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]
|
|
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:
|
|
254
|
+
text: response,
|
|
33
255
|
canContinue: true
|
|
34
256
|
};
|
|
35
257
|
},
|
|
36
258
|
|
|
37
|
-
|
|
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`);
|