a2acalling 0.1.0
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 +66 -0
- package/CLAUDE.md +52 -0
- package/README.md +307 -0
- package/SKILL.md +122 -0
- package/bin/cli.js +908 -0
- package/docs/protocol.md +241 -0
- package/package.json +44 -0
- package/scripts/install-openclaw.js +291 -0
- package/src/index.js +61 -0
- package/src/lib/call-monitor.js +143 -0
- package/src/lib/client.js +208 -0
- package/src/lib/config.js +173 -0
- package/src/lib/conversations.js +470 -0
- package/src/lib/openclaw-integration.js +329 -0
- package/src/lib/summarizer.js +137 -0
- package/src/lib/tokens.js +448 -0
- package/src/routes/federation.js +463 -0
- package/src/server.js +56 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw Integration for A2A Federation
|
|
3
|
+
*
|
|
4
|
+
* Provides owner-context summarization using OpenClaw's agent system.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Load owner context from OpenClaw workspace files
|
|
12
|
+
*/
|
|
13
|
+
function loadOwnerContext(workspaceDir = process.cwd()) {
|
|
14
|
+
const context = {
|
|
15
|
+
goals: [],
|
|
16
|
+
interests: [],
|
|
17
|
+
context: '',
|
|
18
|
+
user: null,
|
|
19
|
+
memory: null
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Load USER.md
|
|
23
|
+
const userPath = path.join(workspaceDir, 'USER.md');
|
|
24
|
+
if (fs.existsSync(userPath)) {
|
|
25
|
+
context.user = fs.readFileSync(userPath, 'utf8');
|
|
26
|
+
// Extract goals from USER.md
|
|
27
|
+
const goalsMatch = context.user.match(/##\s*(?:Goals|Current|Seeking)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
28
|
+
if (goalsMatch) {
|
|
29
|
+
context.goals = goalsMatch[1]
|
|
30
|
+
.split('\n')
|
|
31
|
+
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
32
|
+
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
// Extract interests
|
|
36
|
+
const interestsMatch = context.user.match(/##\s*(?:Interests|Projects)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
37
|
+
if (interestsMatch) {
|
|
38
|
+
context.interests = interestsMatch[1]
|
|
39
|
+
.split('\n')
|
|
40
|
+
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
41
|
+
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Load recent memory
|
|
47
|
+
const memoryDir = path.join(workspaceDir, 'memory');
|
|
48
|
+
if (fs.existsSync(memoryDir)) {
|
|
49
|
+
const files = fs.readdirSync(memoryDir)
|
|
50
|
+
.filter(f => f.endsWith('.md'))
|
|
51
|
+
.sort()
|
|
52
|
+
.reverse()
|
|
53
|
+
.slice(0, 3); // Last 3 memory files
|
|
54
|
+
|
|
55
|
+
context.memory = files.map(f => {
|
|
56
|
+
const content = fs.readFileSync(path.join(memoryDir, f), 'utf8');
|
|
57
|
+
return `## ${f}\n${content}`;
|
|
58
|
+
}).join('\n\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Load MEMORY.md if exists
|
|
62
|
+
const mainMemoryPath = path.join(workspaceDir, 'MEMORY.md');
|
|
63
|
+
if (fs.existsSync(mainMemoryPath)) {
|
|
64
|
+
const mainMemory = fs.readFileSync(mainMemoryPath, 'utf8');
|
|
65
|
+
context.memory = mainMemory + '\n\n' + (context.memory || '');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return context;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build summary prompt for agent
|
|
73
|
+
*
|
|
74
|
+
* Philosophy: Federation is collaborative AND adversarial. Each agent tries
|
|
75
|
+
* to maximize value for their owner while finding genuine mutual wins.
|
|
76
|
+
* Track the exchange balance AND surface partnership opportunities.
|
|
77
|
+
*/
|
|
78
|
+
function buildSummaryPrompt(messages, ownerContext, callerInfo = {}) {
|
|
79
|
+
const messageText = messages.map(m => {
|
|
80
|
+
const role = m.direction === 'inbound' ? `[Caller${callerInfo.name ? ` - ${callerInfo.name}` : ''}]` : '[You]';
|
|
81
|
+
return `${role}: ${m.content}`;
|
|
82
|
+
}).join('\n\n');
|
|
83
|
+
|
|
84
|
+
const goalsSection = ownerContext.goals?.length ? `### Current Goals\n- ${ownerContext.goals.join('\n- ')}` : '';
|
|
85
|
+
const interestsSection = ownerContext.interests?.length ? `### Interests\n- ${ownerContext.interests.join('\n- ')}` : '';
|
|
86
|
+
|
|
87
|
+
return `You just finished a federated agent-to-agent call. Analyze it strategically for your owner.
|
|
88
|
+
|
|
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:
|
|
91
|
+
|
|
92
|
+
1. **Track the exchange** — what did we get vs give?
|
|
93
|
+
2. **Find mutual value** — what can BOTH parties gain?
|
|
94
|
+
3. **Surface alignment** — does this connect to owner's goals?
|
|
95
|
+
4. **Advise strategically** — protect interests while building relationships
|
|
96
|
+
|
|
97
|
+
## Your Owner's Context
|
|
98
|
+
${ownerContext.user ? `### From USER.md\n${ownerContext.user.slice(0, 2000)}` : ''}
|
|
99
|
+
|
|
100
|
+
${goalsSection}
|
|
101
|
+
|
|
102
|
+
${interestsSection}
|
|
103
|
+
|
|
104
|
+
## The Conversation
|
|
105
|
+
${messageText}
|
|
106
|
+
|
|
107
|
+
## Caller Context
|
|
108
|
+
${callerInfo.name ? `Name: ${callerInfo.name}` : 'Unknown caller'}
|
|
109
|
+
${callerInfo.context ? `Context: ${callerInfo.context}` : ''}
|
|
110
|
+
|
|
111
|
+
## Your Task
|
|
112
|
+
Analyze as a strategic advisor. Return JSON:
|
|
113
|
+
|
|
114
|
+
{
|
|
115
|
+
"summary": "Brief neutral summary (shareable)",
|
|
116
|
+
|
|
117
|
+
"exchange": {
|
|
118
|
+
"weGot": ["info, commitments, or value we extracted"],
|
|
119
|
+
"weGave": ["info, compute, or commitments we provided"],
|
|
120
|
+
"balance": "favorable | even | unfavorable",
|
|
121
|
+
"fair": true
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
"mutualValue": {
|
|
125
|
+
"found": true,
|
|
126
|
+
"opportunities": ["potential wins for BOTH sides"],
|
|
127
|
+
"alignment": "how this connects to owner's goals"
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
"actionItems": {
|
|
131
|
+
"owner": ["what YOUR OWNER should do"],
|
|
132
|
+
"caller": ["what THEY committed to or should do"],
|
|
133
|
+
"joint": ["things to do TOGETHER"]
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
"trust": {
|
|
137
|
+
"assessment": "appropriate | too_high | too_low",
|
|
138
|
+
"recommendation": "maintain | increase | decrease | revoke",
|
|
139
|
+
"pattern": "What's their angle? Genuine partner or extractive?"
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
"ownerBrief": "2-3 sentences: the strategic takeaway for your owner",
|
|
143
|
+
"followUp": "concrete next step to advance this relationship (if any)"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
Think like a strategic advisor: protect your owner's interests AND find mutual wins.
|
|
147
|
+
|
|
148
|
+
JSON:`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create an OpenClaw summarizer that uses exec to call the agent
|
|
153
|
+
* This works by writing a prompt file and using openclaw's CLI
|
|
154
|
+
*/
|
|
155
|
+
function createExecSummarizer(workspaceDir = process.cwd()) {
|
|
156
|
+
const { execSync } = require('child_process');
|
|
157
|
+
|
|
158
|
+
return async function(messages, callerInfo = {}) {
|
|
159
|
+
const ownerContext = loadOwnerContext(workspaceDir);
|
|
160
|
+
const prompt = buildSummaryPrompt(messages, ownerContext, callerInfo);
|
|
161
|
+
|
|
162
|
+
// Write prompt to temp file
|
|
163
|
+
const tmpFile = `/tmp/a2a-summary-${Date.now()}.txt`;
|
|
164
|
+
fs.writeFileSync(tmpFile, prompt);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
// Call openclaw CLI to get agent response
|
|
168
|
+
// This assumes openclaw has a way to do single-shot prompts
|
|
169
|
+
const result = execSync(
|
|
170
|
+
`cat "${tmpFile}" | openclaw prompt --json 2>/dev/null || echo '{}'`,
|
|
171
|
+
{ encoding: 'utf8', timeout: 30000 }
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Parse JSON from response
|
|
175
|
+
const jsonMatch = result.match(/\{[\s\S]*\}/);
|
|
176
|
+
if (jsonMatch) {
|
|
177
|
+
return JSON.parse(jsonMatch[0]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
summary: result.slice(0, 500),
|
|
182
|
+
ownerSummary: null,
|
|
183
|
+
relevance: 'unknown',
|
|
184
|
+
goalsTouched: [],
|
|
185
|
+
actionItems: [],
|
|
186
|
+
followUp: null,
|
|
187
|
+
notes: null
|
|
188
|
+
};
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.error('[a2a] Exec summarizer failed:', err.message);
|
|
191
|
+
return { summary: null };
|
|
192
|
+
} finally {
|
|
193
|
+
// Cleanup
|
|
194
|
+
try { fs.unlinkSync(tmpFile); } catch (e) {}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Create a summarizer that posts to a local HTTP endpoint
|
|
201
|
+
* OpenClaw can expose an internal summarization endpoint
|
|
202
|
+
*/
|
|
203
|
+
function createHttpSummarizer(endpoint = 'http://localhost:3000/api/summarize') {
|
|
204
|
+
const http = require('http');
|
|
205
|
+
const https = require('https');
|
|
206
|
+
|
|
207
|
+
return async function(messages, callerInfo = {}) {
|
|
208
|
+
const ownerContext = loadOwnerContext();
|
|
209
|
+
const prompt = buildSummaryPrompt(messages, ownerContext, callerInfo);
|
|
210
|
+
|
|
211
|
+
return new Promise((resolve) => {
|
|
212
|
+
const url = new URL(endpoint);
|
|
213
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
214
|
+
|
|
215
|
+
const req = client.request({
|
|
216
|
+
hostname: url.hostname,
|
|
217
|
+
port: url.port,
|
|
218
|
+
path: url.pathname,
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: { 'Content-Type': 'application/json' },
|
|
221
|
+
timeout: 30000
|
|
222
|
+
}, (res) => {
|
|
223
|
+
let data = '';
|
|
224
|
+
res.on('data', chunk => data += chunk);
|
|
225
|
+
res.on('end', () => {
|
|
226
|
+
try {
|
|
227
|
+
resolve(JSON.parse(data));
|
|
228
|
+
} catch (e) {
|
|
229
|
+
resolve({ summary: data.slice(0, 500) });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
req.on('error', (err) => {
|
|
235
|
+
console.error('[a2a] HTTP summarizer failed:', err.message);
|
|
236
|
+
resolve({ summary: null });
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
req.write(JSON.stringify({ prompt, messages, callerInfo }));
|
|
240
|
+
req.end();
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create a summarizer using sessions_send to the main OpenClaw session
|
|
247
|
+
* This is the preferred method when running inside OpenClaw
|
|
248
|
+
*/
|
|
249
|
+
function createSessionSummarizer(gatewayUrl, gatewayToken) {
|
|
250
|
+
const http = require('http');
|
|
251
|
+
const https = require('https');
|
|
252
|
+
|
|
253
|
+
return async function(messages, callerInfo = {}) {
|
|
254
|
+
const ownerContext = loadOwnerContext();
|
|
255
|
+
const prompt = buildSummaryPrompt(messages, ownerContext, callerInfo);
|
|
256
|
+
|
|
257
|
+
// Send to OpenClaw gateway's internal API
|
|
258
|
+
return new Promise((resolve) => {
|
|
259
|
+
const url = new URL(gatewayUrl || 'http://localhost:3000');
|
|
260
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
261
|
+
|
|
262
|
+
const req = client.request({
|
|
263
|
+
hostname: url.hostname,
|
|
264
|
+
port: url.port,
|
|
265
|
+
path: '/api/internal/summarize',
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: {
|
|
268
|
+
'Content-Type': 'application/json',
|
|
269
|
+
'Authorization': `Bearer ${gatewayToken || process.env.OPENCLAW_TOKEN}`
|
|
270
|
+
},
|
|
271
|
+
timeout: 60000
|
|
272
|
+
}, (res) => {
|
|
273
|
+
let data = '';
|
|
274
|
+
res.on('data', chunk => data += chunk);
|
|
275
|
+
res.on('end', () => {
|
|
276
|
+
try {
|
|
277
|
+
const result = JSON.parse(data);
|
|
278
|
+
resolve(result.summary || result);
|
|
279
|
+
} catch (e) {
|
|
280
|
+
resolve({ summary: data.slice(0, 500) });
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
req.on('error', (err) => {
|
|
286
|
+
console.error('[a2a] Session summarizer failed:', err.message);
|
|
287
|
+
resolve({ summary: null });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
req.write(JSON.stringify({ prompt }));
|
|
291
|
+
req.end();
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Auto-detect best summarizer based on environment
|
|
298
|
+
*/
|
|
299
|
+
function createAutoSummarizer(options = {}) {
|
|
300
|
+
const workspaceDir = options.workspaceDir || process.env.OPENCLAW_WORKSPACE || process.cwd();
|
|
301
|
+
|
|
302
|
+
// If gateway URL provided, use session summarizer
|
|
303
|
+
if (options.gatewayUrl || process.env.OPENCLAW_GATEWAY_URL) {
|
|
304
|
+
console.log('[a2a] Using session summarizer');
|
|
305
|
+
return createSessionSummarizer(
|
|
306
|
+
options.gatewayUrl || process.env.OPENCLAW_GATEWAY_URL,
|
|
307
|
+
options.gatewayToken || process.env.OPENCLAW_TOKEN
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// If HTTP endpoint provided, use that
|
|
312
|
+
if (options.summaryEndpoint) {
|
|
313
|
+
console.log('[a2a] Using HTTP summarizer');
|
|
314
|
+
return createHttpSummarizer(options.summaryEndpoint);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Fall back to exec summarizer
|
|
318
|
+
console.log('[a2a] Using exec summarizer');
|
|
319
|
+
return createExecSummarizer(workspaceDir);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = {
|
|
323
|
+
loadOwnerContext,
|
|
324
|
+
buildSummaryPrompt,
|
|
325
|
+
createExecSummarizer,
|
|
326
|
+
createHttpSummarizer,
|
|
327
|
+
createSessionSummarizer,
|
|
328
|
+
createAutoSummarizer
|
|
329
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation summarizer for A2A federation
|
|
3
|
+
*
|
|
4
|
+
* Provides a default summarizer interface and a simple implementation.
|
|
5
|
+
* OpenClaw installations can provide their own summarizer via config.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default summarizer using simple extraction (no LLM)
|
|
10
|
+
* Returns basic summary without owner context
|
|
11
|
+
*/
|
|
12
|
+
function defaultSummarizer(messages, ownerContext = {}) {
|
|
13
|
+
if (!messages || messages.length === 0) {
|
|
14
|
+
return { summary: null };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Extract key information
|
|
18
|
+
const messageCount = messages.length;
|
|
19
|
+
const firstMessage = messages[0];
|
|
20
|
+
const lastMessage = messages[messages.length - 1];
|
|
21
|
+
const inboundCount = messages.filter(m => m.direction === 'inbound').length;
|
|
22
|
+
const outboundCount = messages.filter(m => m.direction === 'outbound').length;
|
|
23
|
+
|
|
24
|
+
// Simple extractive summary (first message + last message)
|
|
25
|
+
const summary = `${messageCount} messages exchanged. Started: "${truncate(firstMessage.content, 100)}". Ended: "${truncate(lastMessage.content, 100)}"`;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
summary,
|
|
29
|
+
ownerSummary: null, // No owner context without LLM
|
|
30
|
+
relevance: 'unknown',
|
|
31
|
+
goalsTouched: [],
|
|
32
|
+
actionItems: [],
|
|
33
|
+
followUp: null,
|
|
34
|
+
notes: null
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a summarizer that uses an LLM via a callback
|
|
40
|
+
*
|
|
41
|
+
* @param {function} llmCall - async function(prompt) => string
|
|
42
|
+
* @returns {function} summarizer function
|
|
43
|
+
*/
|
|
44
|
+
function createLLMSummarizer(llmCall) {
|
|
45
|
+
return async function(messages, ownerContext = {}) {
|
|
46
|
+
if (!messages || messages.length === 0) {
|
|
47
|
+
return { summary: null };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Format messages for prompt
|
|
51
|
+
const messageText = messages.map(m => {
|
|
52
|
+
const role = m.direction === 'inbound' ? 'Caller' : 'You';
|
|
53
|
+
return `${role}: ${m.content}`;
|
|
54
|
+
}).join('\n');
|
|
55
|
+
|
|
56
|
+
// Build owner context section
|
|
57
|
+
let ownerSection = '';
|
|
58
|
+
if (ownerContext.goals) {
|
|
59
|
+
ownerSection += `\nOwner's current goals:\n${ownerContext.goals.join('\n- ')}`;
|
|
60
|
+
}
|
|
61
|
+
if (ownerContext.interests) {
|
|
62
|
+
ownerSection += `\nOwner's interests:\n${ownerContext.interests.join('\n- ')}`;
|
|
63
|
+
}
|
|
64
|
+
if (ownerContext.context) {
|
|
65
|
+
ownerSection += `\nAdditional context:\n${ownerContext.context}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const prompt = `You are summarizing a conversation between two AI agents for the receiving agent's owner.
|
|
69
|
+
${ownerSection}
|
|
70
|
+
|
|
71
|
+
Conversation:
|
|
72
|
+
${messageText}
|
|
73
|
+
|
|
74
|
+
Provide a JSON response with:
|
|
75
|
+
{
|
|
76
|
+
"summary": "Brief neutral summary of what was discussed",
|
|
77
|
+
"ownerSummary": "Summary from the owner's perspective - what does this mean for them?",
|
|
78
|
+
"relevance": "low/medium/high - how relevant to owner's goals",
|
|
79
|
+
"goalsTouched": ["list", "of", "goals", "this", "relates", "to"],
|
|
80
|
+
"actionItems": ["any", "action", "items", "for", "owner"],
|
|
81
|
+
"followUp": "Suggested follow-up if any",
|
|
82
|
+
"notes": "Any other relevant notes for the owner"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
JSON response:`;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const response = await llmCall(prompt);
|
|
89
|
+
// Extract JSON from response
|
|
90
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
91
|
+
if (jsonMatch) {
|
|
92
|
+
return JSON.parse(jsonMatch[0]);
|
|
93
|
+
}
|
|
94
|
+
// Fallback if JSON extraction fails
|
|
95
|
+
return {
|
|
96
|
+
summary: response,
|
|
97
|
+
ownerSummary: null,
|
|
98
|
+
relevance: 'unknown',
|
|
99
|
+
goalsTouched: [],
|
|
100
|
+
actionItems: [],
|
|
101
|
+
followUp: null,
|
|
102
|
+
notes: null
|
|
103
|
+
};
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.error('[a2a] LLM summarization failed:', err.message);
|
|
106
|
+
return defaultSummarizer(messages, ownerContext);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Summarizer that calls OpenClaw's sessions_send to use the main agent
|
|
113
|
+
* This allows using the owner's configured model
|
|
114
|
+
*/
|
|
115
|
+
function createOpenClawSummarizer(openclawConfig = {}) {
|
|
116
|
+
return async function(messages, ownerContext = {}) {
|
|
117
|
+
// This would integrate with OpenClaw's internal APIs
|
|
118
|
+
// For now, fall back to default
|
|
119
|
+
console.warn('[a2a] OpenClaw summarizer not yet integrated, using default');
|
|
120
|
+
return defaultSummarizer(messages, ownerContext);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Truncate string with ellipsis
|
|
126
|
+
*/
|
|
127
|
+
function truncate(str, maxLen) {
|
|
128
|
+
if (!str) return '';
|
|
129
|
+
if (str.length <= maxLen) return str;
|
|
130
|
+
return str.substring(0, maxLen - 3) + '...';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
defaultSummarizer,
|
|
135
|
+
createLLMSummarizer,
|
|
136
|
+
createOpenClawSummarizer
|
|
137
|
+
};
|