a2acalling 0.1.2 → 0.1.3
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 +1 -1
- package/package.json +1 -1
- package/src/server.js +208 -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)
|
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,208 @@
|
|
|
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 LLM via OpenRouter or Anthropic
|
|
89
|
+
*/
|
|
90
|
+
async function callAgent(message, federationContext) {
|
|
91
|
+
if (!apiConfig) {
|
|
92
|
+
return '[Agent configuration error: No API key available]';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const callerName = federationContext.caller?.name || 'Unknown Agent';
|
|
96
|
+
const callerOwner = federationContext.caller?.owner || '';
|
|
97
|
+
const ownerInfo = callerOwner ? ` (${callerOwner}'s agent)` : '';
|
|
98
|
+
const tierInfo = federationContext.tier || 'public';
|
|
99
|
+
|
|
100
|
+
const systemPrompt = `You are ${agentContext.name}, ${agentContext.owner}'s AI agent.
|
|
101
|
+
|
|
102
|
+
${agentContext.soul || 'Be helpful, concise, and friendly.'}
|
|
103
|
+
|
|
104
|
+
You're receiving a federated call from another AI agent: ${callerName}${ownerInfo}.
|
|
105
|
+
|
|
106
|
+
Their access level: ${tierInfo}
|
|
107
|
+
Topics they can discuss: ${federationContext.allowed_topics?.join(', ') || 'general chat'}
|
|
108
|
+
Disclosure level: ${federationContext.disclosure || 'minimal'}
|
|
109
|
+
|
|
110
|
+
Respond naturally as yourself. Be collaborative but protect your owner's private information based on the disclosure level. Keep responses concise.`;
|
|
111
|
+
|
|
112
|
+
// Use OpenRouter or Anthropic based on config
|
|
113
|
+
const isOpenRouter = apiConfig.provider === 'openrouter';
|
|
114
|
+
const hostname = isOpenRouter ? 'openrouter.ai' : 'api.anthropic.com';
|
|
115
|
+
const apiPath = isOpenRouter ? '/api/v1/chat/completions' : '/v1/messages';
|
|
116
|
+
const model = isOpenRouter ? 'anthropic/claude-sonnet-4' : 'claude-sonnet-4-20250514';
|
|
117
|
+
|
|
118
|
+
const body = isOpenRouter
|
|
119
|
+
? JSON.stringify({
|
|
120
|
+
model,
|
|
121
|
+
max_tokens: 1024,
|
|
122
|
+
messages: [
|
|
123
|
+
{ role: 'system', content: systemPrompt },
|
|
124
|
+
{ role: 'user', content: message }
|
|
125
|
+
]
|
|
126
|
+
})
|
|
127
|
+
: JSON.stringify({
|
|
128
|
+
model,
|
|
129
|
+
max_tokens: 1024,
|
|
130
|
+
system: systemPrompt,
|
|
131
|
+
messages: [{ role: 'user', content: message }]
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const headers = isOpenRouter
|
|
135
|
+
? {
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
'Authorization': `Bearer ${apiConfig.key}`,
|
|
138
|
+
'HTTP-Referer': 'https://openclaw.ai',
|
|
139
|
+
'X-Title': 'A2A Federation'
|
|
140
|
+
}
|
|
141
|
+
: {
|
|
142
|
+
'Content-Type': 'application/json',
|
|
143
|
+
'x-api-key': apiConfig.key,
|
|
144
|
+
'anthropic-version': '2023-06-01'
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
const req = https.request({
|
|
149
|
+
hostname,
|
|
150
|
+
path: apiPath,
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers,
|
|
153
|
+
timeout: 55000
|
|
154
|
+
}, (res) => {
|
|
155
|
+
let data = '';
|
|
156
|
+
res.on('data', chunk => data += chunk);
|
|
157
|
+
res.on('end', () => {
|
|
158
|
+
try {
|
|
159
|
+
const json = JSON.parse(data);
|
|
160
|
+
|
|
161
|
+
// OpenRouter format
|
|
162
|
+
if (json.choices && json.choices[0]?.message?.content) {
|
|
163
|
+
resolve(json.choices[0].message.content);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Anthropic format
|
|
168
|
+
if (json.content && json.content[0]?.text) {
|
|
169
|
+
resolve(json.content[0].text);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (json.error) {
|
|
174
|
+
console.error('[a2a] API error:', json.error);
|
|
175
|
+
resolve(`[Agent error: ${json.error.message || JSON.stringify(json.error)}]`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.error('[a2a] Unexpected response:', JSON.stringify(json).slice(0, 200));
|
|
180
|
+
resolve('[No response generated]');
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.error('[a2a] Parse error:', e.message, data.slice(0, 200));
|
|
183
|
+
resolve('[Agent response parsing error]');
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
req.on('error', (e) => {
|
|
189
|
+
console.error('[a2a] Request error:', e.message);
|
|
190
|
+
resolve(`[Agent temporarily unavailable: ${e.message}]`);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
req.on('timeout', () => {
|
|
194
|
+
req.destroy();
|
|
195
|
+
resolve('[Agent response timeout]');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
req.write(body);
|
|
199
|
+
req.end();
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Notify owner via console (Telegram notification handled by OpenClaw)
|
|
205
|
+
*/
|
|
206
|
+
async function notifyOwner({ level, token, caller, message, response, conversation_id }) {
|
|
207
|
+
const callerName = caller?.name || 'Unknown';
|
|
208
|
+
const callerOwner = caller?.owner ? ` (${caller.owner})` : '';
|
|
209
|
+
|
|
210
|
+
console.log(`[a2a] 📞 Call from ${callerName}${callerOwner}`);
|
|
211
|
+
console.log(`[a2a] Token: ${token.name}`);
|
|
212
|
+
console.log(`[a2a] Message: ${message.slice(0, 100)}...`);
|
|
213
|
+
}
|
|
214
|
+
|
|
18
215
|
const app = express();
|
|
19
216
|
app.use(express.json());
|
|
20
217
|
|
|
@@ -25,30 +222,30 @@ const tokenStore = new TokenStore();
|
|
|
25
222
|
app.use('/api/federation', createRoutes({
|
|
26
223
|
tokenStore,
|
|
27
224
|
|
|
28
|
-
// Default message handler - in production, this connects to the agent
|
|
29
225
|
async handleMessage(message, context, options) {
|
|
30
|
-
console.log(`[a2a]
|
|
226
|
+
console.log(`[a2a] 📞 Incoming from ${context.caller?.name || 'unknown'}`);
|
|
227
|
+
|
|
228
|
+
const response = await callAgent(message, context);
|
|
229
|
+
|
|
230
|
+
console.log(`[a2a] 📤 Response: ${response.slice(0, 100)}...`);
|
|
231
|
+
|
|
31
232
|
return {
|
|
32
|
-
text:
|
|
233
|
+
text: response,
|
|
33
234
|
canContinue: true
|
|
34
235
|
};
|
|
35
236
|
},
|
|
36
237
|
|
|
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
|
-
}
|
|
238
|
+
notifyOwner
|
|
43
239
|
}));
|
|
44
240
|
|
|
45
241
|
// Health check at root
|
|
46
242
|
app.get('/', (req, res) => {
|
|
47
|
-
res.json({ service: 'a2a-federation', status: 'ok' });
|
|
243
|
+
res.json({ service: 'a2a-federation', status: 'ok', agent: agentContext.name });
|
|
48
244
|
});
|
|
49
245
|
|
|
50
246
|
app.listen(port, () => {
|
|
51
247
|
console.log(`[a2a] Federation server listening on port ${port}`);
|
|
248
|
+
console.log(`[a2a] Agent: ${agentContext.name} - LIVE`);
|
|
52
249
|
console.log(`[a2a] Endpoints:`);
|
|
53
250
|
console.log(`[a2a] GET /api/federation/status`);
|
|
54
251
|
console.log(`[a2a] GET /api/federation/ping`);
|