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.
@@ -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
+ };