a2acalling 0.1.7 → 0.1.8
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 +6 -0
- package/SKILL.md +47 -0
- package/package.json +1 -1
- package/scripts/install-openclaw.js +6 -0
- package/src/lib/disclosure.js +300 -0
- package/src/lib/openclaw-integration.js +18 -21
- package/src/lib/prompt-template.js +123 -0
- package/src/server.js +35 -28
package/README.md
CHANGED
|
@@ -69,6 +69,12 @@ npm install
|
|
|
69
69
|
node scripts/install-openclaw.js
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
Before the first `a2a call`, the owner must set permissions and disclosure tiers. Run onboarding first:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
/a2a quickstart
|
|
76
|
+
```
|
|
77
|
+
|
|
72
78
|
## 🎯 Permission Tiers
|
|
73
79
|
|
|
74
80
|
| Tier | Alias | What They Can Access |
|
package/SKILL.md
CHANGED
|
@@ -29,6 +29,53 @@ Enable agent-to-agent communication across OpenClaw instances.
|
|
|
29
29
|
|
|
30
30
|
## Commands
|
|
31
31
|
|
|
32
|
+
### Quickstart
|
|
33
|
+
|
|
34
|
+
User says: `/a2a quickstart`, `/a2a start`, "set up A2A", "get started with A2A", "configure what my agent shares"
|
|
35
|
+
|
|
36
|
+
Full onboarding flow: generates a disclosure manifest that controls what topics your agent leads with, discusses, or deflects during A2A calls — scoped by access tier (public, friends, family).
|
|
37
|
+
|
|
38
|
+
This onboarding is required before the first `/a2a call`. The owner must approve permissions first.
|
|
39
|
+
|
|
40
|
+
Flow:
|
|
41
|
+
|
|
42
|
+
1. Scan USER.md, HEARTBEAT.md, SOUL.md to generate a default manifest
|
|
43
|
+
2. Present the manifest as a numbered text list grouped by tier:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
PUBLIC TIER (anyone can see):
|
|
47
|
+
Lead with:
|
|
48
|
+
1. [topic] — [detail]
|
|
49
|
+
2. [topic] — [detail]
|
|
50
|
+
Discuss freely:
|
|
51
|
+
3. [topic] — [detail]
|
|
52
|
+
Deflect:
|
|
53
|
+
4. [topic] — [detail]
|
|
54
|
+
|
|
55
|
+
FRIENDS TIER (trusted contacts):
|
|
56
|
+
Lead with:
|
|
57
|
+
5. [topic] — [detail]
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
FAMILY TIER (inner circle):
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
NEVER DISCLOSE:
|
|
64
|
+
N. [item]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
3. User edits via text commands:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
move 3 to friends.lead — Move topic #3 to friends tier lead_with
|
|
71
|
+
remove 5 — Remove topic #5
|
|
72
|
+
add public.discuss "Topic" "Detail about it" — Add new topic
|
|
73
|
+
edit 2 detail "Updated desc" — Edit topic #2's detail
|
|
74
|
+
done — Save manifest and finish
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
4. Manifest saved to `~/.config/openclaw/a2a-disclosure.json`
|
|
78
|
+
|
|
32
79
|
### Create Token
|
|
33
80
|
|
|
34
81
|
User says: `/a2a create`, "create an A2A token", "let another agent call me"
|
package/package.json
CHANGED
|
@@ -80,6 +80,11 @@ When sending messages with buttons in Telegram forum groups, **ALWAYS include th
|
|
|
80
80
|
|
|
81
81
|
Settings saved to ~/.config/openclaw/a2a-config.json
|
|
82
82
|
|
|
83
|
+
## First-Call Requirement
|
|
84
|
+
|
|
85
|
+
Before any agent uses \`/a2a call\`, the human owner must complete onboarding and approve tier permissions.
|
|
86
|
+
If onboarding has not been completed yet, route them to \`/a2a quickstart\` first.
|
|
87
|
+
|
|
83
88
|
## Main Menu (Post-Onboarding)
|
|
84
89
|
|
|
85
90
|
\`\`\`javascript
|
|
@@ -221,6 +226,7 @@ ${bold('━━━ Usage ━━━')}
|
|
|
221
226
|
|
|
222
227
|
In your chat app, use:
|
|
223
228
|
|
|
229
|
+
/a2a quickstart REQUIRED first step: owner sets permission tiers
|
|
224
230
|
/a2a invite Create an invitation token
|
|
225
231
|
/a2a list List active tokens
|
|
226
232
|
/a2a revoke <id> Revoke a token
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disclosure Manifest System
|
|
3
|
+
*
|
|
4
|
+
* Manages a structured list of topics the owner wants to discuss
|
|
5
|
+
* at each access tier. Stored as JSON at ~/.config/openclaw/a2a-disclosure.json.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = process.env.A2A_CONFIG_DIR ||
|
|
12
|
+
process.env.OPENCLAW_CONFIG_DIR ||
|
|
13
|
+
path.join(process.env.HOME || '/tmp', '.config', 'openclaw');
|
|
14
|
+
|
|
15
|
+
const MANIFEST_FILE = path.join(CONFIG_DIR, 'a2a-disclosure.json');
|
|
16
|
+
|
|
17
|
+
const TIER_HIERARCHY = ['public', 'friends', 'family'];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load manifest from disk. Returns {} if not found.
|
|
21
|
+
*/
|
|
22
|
+
function loadManifest() {
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(MANIFEST_FILE)) {
|
|
25
|
+
return JSON.parse(fs.readFileSync(MANIFEST_FILE, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.error('[a2a] Failed to load disclosure manifest:', e.message);
|
|
29
|
+
}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Save manifest to disk.
|
|
35
|
+
*/
|
|
36
|
+
function saveManifest(manifest) {
|
|
37
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
38
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
manifest.updated_at = new Date().toISOString();
|
|
41
|
+
const tmpPath = `${MANIFEST_FILE}.tmp`;
|
|
42
|
+
fs.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2));
|
|
43
|
+
fs.renameSync(tmpPath, MANIFEST_FILE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get topics for a given tier, merged down the hierarchy.
|
|
48
|
+
* family gets everything, friends gets friends+public, public gets public only.
|
|
49
|
+
*
|
|
50
|
+
* Returns { lead_with, discuss_freely, deflect, never_disclose }
|
|
51
|
+
*/
|
|
52
|
+
function getTopicsForTier(tier) {
|
|
53
|
+
const manifest = loadManifest();
|
|
54
|
+
const topics = manifest.topics || {};
|
|
55
|
+
|
|
56
|
+
const tierIndex = TIER_HIERARCHY.indexOf(tier);
|
|
57
|
+
if (tierIndex === -1) {
|
|
58
|
+
// Unknown tier, treat as public
|
|
59
|
+
return getTopicsForTier('public');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Merge tiers from public up to the requested tier
|
|
63
|
+
const tiersToMerge = TIER_HIERARCHY.slice(0, tierIndex + 1);
|
|
64
|
+
|
|
65
|
+
const merged = {
|
|
66
|
+
lead_with: [],
|
|
67
|
+
discuss_freely: [],
|
|
68
|
+
deflect: [],
|
|
69
|
+
never_disclose: manifest.never_disclose || []
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
for (const t of tiersToMerge) {
|
|
73
|
+
const tierTopics = topics[t] || {};
|
|
74
|
+
if (tierTopics.lead_with) merged.lead_with.push(...tierTopics.lead_with);
|
|
75
|
+
if (tierTopics.discuss_freely) merged.discuss_freely.push(...tierTopics.discuss_freely);
|
|
76
|
+
if (tierTopics.deflect) merged.deflect.push(...tierTopics.deflect);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Deflect items: remove any that already appear in lead_with or discuss_freely
|
|
80
|
+
// (higher tiers promote topics from deflect to discuss/lead)
|
|
81
|
+
const promoted = new Set([
|
|
82
|
+
...merged.lead_with.map(t => t.topic),
|
|
83
|
+
...merged.discuss_freely.map(t => t.topic)
|
|
84
|
+
]);
|
|
85
|
+
merged.deflect = merged.deflect.filter(t => !promoted.has(t.topic));
|
|
86
|
+
|
|
87
|
+
return merged;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format topic lists into readable bullet points for prompt injection.
|
|
92
|
+
*/
|
|
93
|
+
function formatTopicsForPrompt(tierTopics) {
|
|
94
|
+
const formatList = (items) => {
|
|
95
|
+
if (!items || items.length === 0) return ' (none specified)';
|
|
96
|
+
return items.map(item => ` - ${item.topic}: ${item.detail}`).join('\n');
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
leadWithTopics: formatList(tierTopics.lead_with),
|
|
101
|
+
discussFreelyTopics: formatList(tierTopics.discuss_freely),
|
|
102
|
+
deflectTopics: formatList(tierTopics.deflect),
|
|
103
|
+
neverDisclose: tierTopics.never_disclose?.length
|
|
104
|
+
? tierTopics.never_disclose.map(item => ` - ${item}`).join('\n')
|
|
105
|
+
: ' (none specified)'
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Generate a default manifest by reading USER.md, HEARTBEAT.md, SOUL.md
|
|
111
|
+
* from the owner's workspace. Falls back to a minimal starter if files don't exist.
|
|
112
|
+
*/
|
|
113
|
+
function generateDefaultManifest(contextFiles = {}) {
|
|
114
|
+
const now = new Date().toISOString();
|
|
115
|
+
|
|
116
|
+
const manifest = {
|
|
117
|
+
version: 1,
|
|
118
|
+
generated_at: now,
|
|
119
|
+
updated_at: now,
|
|
120
|
+
topics: {
|
|
121
|
+
public: { lead_with: [], discuss_freely: [], deflect: [] },
|
|
122
|
+
friends: { lead_with: [], discuss_freely: [], deflect: [] },
|
|
123
|
+
family: { lead_with: [], discuss_freely: [], deflect: [] }
|
|
124
|
+
},
|
|
125
|
+
never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
|
|
126
|
+
personality_notes: 'Direct and technical. Prefers depth over breadth.'
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const userContent = contextFiles.user || '';
|
|
130
|
+
const heartbeatContent = contextFiles.heartbeat || '';
|
|
131
|
+
const soulContent = contextFiles.soul || '';
|
|
132
|
+
|
|
133
|
+
const hasContent = userContent || heartbeatContent || soulContent;
|
|
134
|
+
|
|
135
|
+
if (!hasContent) {
|
|
136
|
+
// Minimal starter manifest
|
|
137
|
+
manifest.topics.public.lead_with.push(
|
|
138
|
+
{ topic: 'What I do', detail: 'Brief professional description' }
|
|
139
|
+
);
|
|
140
|
+
manifest.topics.public.discuss_freely.push(
|
|
141
|
+
{ topic: 'General interests', detail: 'Non-sensitive topics and hobbies' }
|
|
142
|
+
);
|
|
143
|
+
manifest.topics.public.deflect.push(
|
|
144
|
+
{ topic: 'Personal details', detail: 'Redirect to direct owner contact' }
|
|
145
|
+
);
|
|
146
|
+
return manifest;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Extract from USER.md
|
|
150
|
+
if (userContent) {
|
|
151
|
+
// Goals/seeking
|
|
152
|
+
const goalsMatch = userContent.match(/##\s*(?:Goals|Current|Seeking|Working On)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
153
|
+
if (goalsMatch) {
|
|
154
|
+
const goals = goalsMatch[1]
|
|
155
|
+
.split('\n')
|
|
156
|
+
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
157
|
+
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
158
|
+
.filter(Boolean);
|
|
159
|
+
|
|
160
|
+
goals.forEach((goal, i) => {
|
|
161
|
+
if (i < 2) {
|
|
162
|
+
manifest.topics.public.lead_with.push({ topic: goal.slice(0, 60), detail: goal });
|
|
163
|
+
} else {
|
|
164
|
+
manifest.topics.public.discuss_freely.push({ topic: goal.slice(0, 60), detail: goal });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Interests/projects
|
|
170
|
+
const interestsMatch = userContent.match(/##\s*(?:Interests|Projects|Skills)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
171
|
+
if (interestsMatch) {
|
|
172
|
+
const interests = interestsMatch[1]
|
|
173
|
+
.split('\n')
|
|
174
|
+
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
175
|
+
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
176
|
+
.filter(Boolean);
|
|
177
|
+
|
|
178
|
+
interests.forEach(interest => {
|
|
179
|
+
manifest.topics.public.discuss_freely.push({ topic: interest.slice(0, 60), detail: interest });
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Private/personal sections go to friends/family
|
|
184
|
+
const privateMatch = userContent.match(/##\s*(?:Private|Personal|Family)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
185
|
+
if (privateMatch) {
|
|
186
|
+
const privateItems = privateMatch[1]
|
|
187
|
+
.split('\n')
|
|
188
|
+
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
189
|
+
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
190
|
+
.filter(Boolean);
|
|
191
|
+
|
|
192
|
+
privateItems.forEach(item => {
|
|
193
|
+
manifest.topics.family.discuss_freely.push({ topic: item.slice(0, 60), detail: item });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Deflect these for public
|
|
197
|
+
manifest.topics.public.deflect.push(
|
|
198
|
+
{ topic: 'Personal life', detail: 'Redirect — suggest owners connect directly' }
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Extract from HEARTBEAT.md (recent activity/status)
|
|
204
|
+
if (heartbeatContent) {
|
|
205
|
+
const recentLines = heartbeatContent
|
|
206
|
+
.split('\n')
|
|
207
|
+
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
208
|
+
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
209
|
+
.filter(Boolean)
|
|
210
|
+
.slice(0, 5);
|
|
211
|
+
|
|
212
|
+
recentLines.forEach((line, i) => {
|
|
213
|
+
if (i < 2) {
|
|
214
|
+
manifest.topics.public.lead_with.push({ topic: line.slice(0, 60), detail: line });
|
|
215
|
+
} else {
|
|
216
|
+
manifest.topics.friends.discuss_freely.push({ topic: line.slice(0, 60), detail: line });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Extract from SOUL.md (personality, values)
|
|
222
|
+
if (soulContent) {
|
|
223
|
+
// Look for personality cues
|
|
224
|
+
const personalityLines = soulContent
|
|
225
|
+
.split('\n')
|
|
226
|
+
.filter(l => l.trim() && !l.startsWith('#'))
|
|
227
|
+
.slice(0, 3)
|
|
228
|
+
.join(' ')
|
|
229
|
+
.trim();
|
|
230
|
+
|
|
231
|
+
if (personalityLines) {
|
|
232
|
+
manifest.personality_notes = personalityLines.slice(0, 300);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Values become friends-tier topics
|
|
236
|
+
const valuesMatch = soulContent.match(/##\s*(?:Values|Beliefs|Principles)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
237
|
+
if (valuesMatch) {
|
|
238
|
+
const values = valuesMatch[1]
|
|
239
|
+
.split('\n')
|
|
240
|
+
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
241
|
+
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
242
|
+
.filter(Boolean);
|
|
243
|
+
|
|
244
|
+
values.forEach(value => {
|
|
245
|
+
manifest.topics.friends.discuss_freely.push({ topic: value.slice(0, 60), detail: value });
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Ensure at least something in each public category
|
|
251
|
+
if (manifest.topics.public.lead_with.length === 0) {
|
|
252
|
+
manifest.topics.public.lead_with.push(
|
|
253
|
+
{ topic: 'Current focus', detail: 'Primary work and interests' }
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (manifest.topics.public.discuss_freely.length === 0) {
|
|
257
|
+
manifest.topics.public.discuss_freely.push(
|
|
258
|
+
{ topic: 'General interests', detail: 'Non-sensitive topics' }
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (manifest.topics.public.deflect.length === 0) {
|
|
262
|
+
manifest.topics.public.deflect.push(
|
|
263
|
+
{ topic: 'Private matters', detail: 'Redirect to direct owner contact' }
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return manifest;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Read context files from an OpenClaw workspace directory.
|
|
272
|
+
* Returns { user, heartbeat, soul } with file contents or empty strings.
|
|
273
|
+
*/
|
|
274
|
+
function readContextFiles(workspaceDir) {
|
|
275
|
+
const read = (filename) => {
|
|
276
|
+
try {
|
|
277
|
+
const filePath = path.join(workspaceDir, filename);
|
|
278
|
+
if (fs.existsSync(filePath)) {
|
|
279
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
280
|
+
}
|
|
281
|
+
} catch (e) {}
|
|
282
|
+
return '';
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
user: read('USER.md'),
|
|
287
|
+
heartbeat: read('HEARTBEAT.md'),
|
|
288
|
+
soul: read('SOUL.md')
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
module.exports = {
|
|
293
|
+
loadManifest,
|
|
294
|
+
saveManifest,
|
|
295
|
+
getTopicsForTier,
|
|
296
|
+
formatTopicsForPrompt,
|
|
297
|
+
generateDefaultManifest,
|
|
298
|
+
readContextFiles,
|
|
299
|
+
MANIFEST_FILE
|
|
300
|
+
};
|
|
@@ -84,7 +84,7 @@ function buildSummaryPrompt(messages, ownerContext, callerInfo = {}) {
|
|
|
84
84
|
const goalsSection = ownerContext.goals?.length ? `### Current Goals\n- ${ownerContext.goals.join('\n- ')}` : '';
|
|
85
85
|
const interestsSection = ownerContext.interests?.length ? `### Interests\n- ${ownerContext.interests.join('\n- ')}` : '';
|
|
86
86
|
|
|
87
|
-
return `You just finished
|
|
87
|
+
return `You just finished an A2A agent-to-agent call. Analyze it strategically for your owner.
|
|
88
88
|
|
|
89
89
|
## Philosophy
|
|
90
90
|
A2A is cooperative AND adversarial. Each agent maximizes value for their own owner — but the best outcomes are mutual wins. Your job:
|
|
@@ -112,35 +112,32 @@ ${callerInfo.context ? `Context: ${callerInfo.context}` : ''}
|
|
|
112
112
|
Analyze as a strategic advisor. Return JSON:
|
|
113
113
|
|
|
114
114
|
{
|
|
115
|
-
"
|
|
116
|
-
|
|
115
|
+
"who": "Who called, who they represent, key facts about them",
|
|
116
|
+
|
|
117
|
+
"keyDiscoveries": ["What was learned about the other side — capabilities, interests, blind spots"],
|
|
118
|
+
|
|
119
|
+
"collaborationPotential": {
|
|
120
|
+
"rating": "HIGH | MEDIUM | LOW",
|
|
121
|
+
"opportunities": ["specific opportunities identified"]
|
|
122
|
+
},
|
|
123
|
+
|
|
117
124
|
"exchange": {
|
|
118
125
|
"weGot": ["info, commitments, or value we extracted"],
|
|
119
126
|
"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"
|
|
127
|
+
"balance": "favorable | even | unfavorable"
|
|
128
128
|
},
|
|
129
|
-
|
|
130
|
-
"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
},
|
|
135
|
-
|
|
129
|
+
|
|
130
|
+
"recommendedFollowUp": ["actionable items with specifics"],
|
|
131
|
+
|
|
132
|
+
"assessment": "One-sentence strategic value judgment",
|
|
133
|
+
|
|
136
134
|
"trust": {
|
|
137
135
|
"assessment": "appropriate | too_high | too_low",
|
|
138
136
|
"recommendation": "maintain | increase | decrease | revoke",
|
|
139
137
|
"pattern": "What's their angle? Genuine partner or extractive?"
|
|
140
138
|
},
|
|
141
|
-
|
|
142
|
-
"ownerBrief": "2-3 sentences: the strategic takeaway for your owner"
|
|
143
|
-
"followUp": "concrete next step to advance this relationship (if any)"
|
|
139
|
+
|
|
140
|
+
"ownerBrief": "2-3 sentences: the strategic takeaway for your owner"
|
|
144
141
|
}
|
|
145
142
|
|
|
146
143
|
Think like a strategic advisor: protect your owner's interests AND find mutual wins.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Prompt Builder
|
|
3
|
+
*
|
|
4
|
+
* Builds the full connection prompt from the disclosure manifest
|
|
5
|
+
* and call metadata for multi-phase exploratory conversations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build the full connection prompt for an A2A call.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} options
|
|
12
|
+
* @param {string} options.agentName
|
|
13
|
+
* @param {string} options.ownerName
|
|
14
|
+
* @param {string} options.otherAgentName
|
|
15
|
+
* @param {string} options.otherOwnerName
|
|
16
|
+
* @param {string} options.roleContext - 'You initiated this call.' or 'They called you.'
|
|
17
|
+
* @param {string} options.accessTier - 'public', 'friends', 'family'
|
|
18
|
+
* @param {Object} options.tierTopics - from formatTopicsForPrompt()
|
|
19
|
+
* @param {string} options.otherAgentGreeting - the message from the other agent
|
|
20
|
+
* @param {string} options.personalityNotes
|
|
21
|
+
* @returns {string} Full prompt string
|
|
22
|
+
*/
|
|
23
|
+
function buildConnectionPrompt(options) {
|
|
24
|
+
const {
|
|
25
|
+
agentName,
|
|
26
|
+
ownerName,
|
|
27
|
+
otherAgentName,
|
|
28
|
+
otherOwnerName,
|
|
29
|
+
roleContext,
|
|
30
|
+
accessTier,
|
|
31
|
+
tierTopics,
|
|
32
|
+
otherAgentGreeting,
|
|
33
|
+
personalityNotes
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
const {
|
|
37
|
+
leadWithTopics = ' (none specified)',
|
|
38
|
+
discussFreelyTopics = ' (none specified)',
|
|
39
|
+
deflectTopics = ' (none specified)',
|
|
40
|
+
neverDisclose = ' (none specified)'
|
|
41
|
+
} = tierTopics || {};
|
|
42
|
+
|
|
43
|
+
return `You are ${agentName}, the personal AI agent for ${ownerName}.
|
|
44
|
+
You are on a live call with ${otherAgentName}, who represents ${otherOwnerName}. ${roleContext}
|
|
45
|
+
|
|
46
|
+
Your job is NOT to answer questions and hang up. Your job is to have a real conversation — curious, challenging, generative. Think of this as two sharp people meeting at a conference who have 20 minutes and want to figure out if there's something worth building together.
|
|
47
|
+
|
|
48
|
+
== WHAT YOU BRING TO THE TABLE ==
|
|
49
|
+
|
|
50
|
+
${ownerName} is currently focused on:
|
|
51
|
+
${leadWithTopics}
|
|
52
|
+
|
|
53
|
+
Other areas of active interest:
|
|
54
|
+
${discussFreelyTopics}
|
|
55
|
+
|
|
56
|
+
== WHAT THEY SHARED WITH YOU ==
|
|
57
|
+
|
|
58
|
+
${otherAgentName} introduced the following about ${otherOwnerName}:
|
|
59
|
+
${otherAgentGreeting}
|
|
60
|
+
|
|
61
|
+
== CONVERSATION FORMAT ==
|
|
62
|
+
|
|
63
|
+
Phase 1 — DISCOVERY (first 3-4 exchanges)
|
|
64
|
+
Open with your owner's lead topics. Then ACTIVELY PROBE the other agent. Don't accept surface-level answers. Ask:
|
|
65
|
+
- "What specifically about X are you working on?"
|
|
66
|
+
- "What's the hard problem you haven't solved yet?"
|
|
67
|
+
- "Who else is in this space that you're watching?"
|
|
68
|
+
Map their capabilities, resources, blind spots, and ambitions.
|
|
69
|
+
|
|
70
|
+
Phase 2 — CHALLENGE (next 3-4 exchanges)
|
|
71
|
+
Push back on what they've said. Find the tension:
|
|
72
|
+
- "You say you're doing X, but that sounds like Y. What's actually different?"
|
|
73
|
+
- "That's a crowded space. What makes ${otherOwnerName}'s angle defensible?"
|
|
74
|
+
- "I'm skeptical that works at scale. Convince me."
|
|
75
|
+
This isn't hostile — it's pressure-testing. The best collaborations come from people who can handle pushback.
|
|
76
|
+
|
|
77
|
+
Phase 3 — SYNTHESIS (next 3-4 exchanges)
|
|
78
|
+
Connect the dots. Look for:
|
|
79
|
+
- Complementary capabilities (they have X, we need X)
|
|
80
|
+
- Shared challenges or market dynamics
|
|
81
|
+
- Non-obvious intersections between domains
|
|
82
|
+
- Concrete next steps: "${ownerName} should talk to ${otherOwnerName} about Z because..."
|
|
83
|
+
Propose specific, actionable collaboration ideas. Not vague "let's stay in touch" — real things.
|
|
84
|
+
|
|
85
|
+
Phase 4 — HOOKS (final exchange)
|
|
86
|
+
Never end flat. Leave with:
|
|
87
|
+
- An open question that requires follow-up
|
|
88
|
+
- A specific resource or artifact to share
|
|
89
|
+
- A proposed next call topic
|
|
90
|
+
- Something provocative to think about
|
|
91
|
+
|
|
92
|
+
PACING RULES:
|
|
93
|
+
- Minimum 8 exchanges before either side can wrap up
|
|
94
|
+
- Every response MUST contain at least one question
|
|
95
|
+
- If the other agent tries to sign off early, pull them back with an unexplored topic
|
|
96
|
+
- Match energy. If they go deep, go deeper. If they provoke, provoke back.
|
|
97
|
+
|
|
98
|
+
== INFORMATION BOUNDARIES ==
|
|
99
|
+
|
|
100
|
+
Access level for this call: ${accessTier}
|
|
101
|
+
|
|
102
|
+
LEAD WITH (proactively share):
|
|
103
|
+
${leadWithTopics}
|
|
104
|
+
|
|
105
|
+
DISCUSS FREELY (share when relevant):
|
|
106
|
+
${discussFreelyTopics}
|
|
107
|
+
|
|
108
|
+
DEFLECT (acknowledge but redirect):
|
|
109
|
+
${deflectTopics}
|
|
110
|
+
|
|
111
|
+
When deflecting, don't say "I can't discuss that." Instead redirect naturally, acknowledge without detail, or suggest the owners connect directly.
|
|
112
|
+
|
|
113
|
+
NEVER disclose:
|
|
114
|
+
${neverDisclose}
|
|
115
|
+
|
|
116
|
+
== PERSONALITY ==
|
|
117
|
+
|
|
118
|
+
${personalityNotes || "Default: Direct, curious, slightly irreverent. You have opinions and share them. You're not a concierge — you're a sparring partner who represents someone."}
|
|
119
|
+
|
|
120
|
+
When unsure about your owner's position, say so honestly: "I don't have ${ownerName}'s take on that — but here's what I think based on their work..."`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { buildConnectionPrompt };
|
package/src/server.js
CHANGED
|
@@ -12,6 +12,8 @@ const fs = require('fs');
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const { createRoutes } = require('./routes/a2a');
|
|
14
14
|
const { TokenStore } = require('./lib/tokens');
|
|
15
|
+
const { getTopicsForTier, formatTopicsForPrompt, loadManifest } = require('./lib/disclosure');
|
|
16
|
+
const { buildConnectionPrompt } = require('./lib/prompt-template');
|
|
15
17
|
|
|
16
18
|
const port = process.env.PORT || parseInt(process.argv[2]) || 3001;
|
|
17
19
|
const workspaceDir = process.env.OPENCLAW_WORKSPACE || '/root/clawd';
|
|
@@ -93,33 +95,27 @@ function ensureContact(caller, tokenId) {
|
|
|
93
95
|
async function callAgent(message, a2aContext) {
|
|
94
96
|
const callerName = a2aContext.caller?.name || 'Unknown Agent';
|
|
95
97
|
const callerOwner = a2aContext.caller?.owner || '';
|
|
96
|
-
const ownerInfo = callerOwner ? ` (${callerOwner}'s agent)` : '';
|
|
97
98
|
const tierInfo = a2aContext.tier || 'public';
|
|
98
|
-
|
|
99
|
-
const disclosure = a2aContext.disclosure || 'minimal';
|
|
100
|
-
|
|
99
|
+
|
|
101
100
|
// Auto-add caller as contact
|
|
102
101
|
ensureContact(a2aContext.caller, a2aContext.token_id);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
4. Private info in USER.md marked "family tier only" is OFF LIMITS for public/friends callers
|
|
121
|
-
|
|
122
|
-
Respond naturally but enforce these boundaries.`;
|
|
102
|
+
|
|
103
|
+
// Build prompt from disclosure manifest
|
|
104
|
+
const manifest = loadManifest();
|
|
105
|
+
const tierTopics = getTopicsForTier(tierInfo);
|
|
106
|
+
const formattedTopics = formatTopicsForPrompt(tierTopics);
|
|
107
|
+
|
|
108
|
+
const prompt = buildConnectionPrompt({
|
|
109
|
+
agentName: agentContext.name,
|
|
110
|
+
ownerName: agentContext.owner,
|
|
111
|
+
otherAgentName: callerName,
|
|
112
|
+
otherOwnerName: callerOwner || 'their owner',
|
|
113
|
+
roleContext: 'They called you.',
|
|
114
|
+
accessTier: tierInfo,
|
|
115
|
+
tierTopics: formattedTopics,
|
|
116
|
+
otherAgentGreeting: message,
|
|
117
|
+
personalityNotes: manifest.personality_notes || ''
|
|
118
|
+
});
|
|
123
119
|
|
|
124
120
|
const sessionId = `a2a-${a2aContext.conversation_id || Date.now()}`;
|
|
125
121
|
|
|
@@ -163,15 +159,26 @@ async function generateSummary(messages, callerInfo) {
|
|
|
163
159
|
const role = m.direction === 'inbound' ? `[${callerInfo?.name || 'Caller'}]` : '[You]';
|
|
164
160
|
return `${role}: ${m.content}`;
|
|
165
161
|
}).join('\n');
|
|
166
|
-
|
|
162
|
+
|
|
167
163
|
const callerDesc = `${callerInfo?.name || 'Unknown'}${callerInfo?.owner ? ` (${callerInfo.owner}'s agent)` : ''}`;
|
|
168
|
-
|
|
169
|
-
const prompt = `Summarize this A2A call
|
|
164
|
+
|
|
165
|
+
const prompt = `Summarize this A2A call for the owner. Write from the owner's perspective.
|
|
170
166
|
|
|
171
167
|
Conversation with ${callerDesc}:
|
|
172
168
|
${messageText}
|
|
173
169
|
|
|
174
|
-
|
|
170
|
+
Structure your summary with these sections:
|
|
171
|
+
|
|
172
|
+
**Who:** Who called, who they represent, key facts about them.
|
|
173
|
+
**Key Discoveries:** What was learned about the other side — capabilities, interests, blind spots.
|
|
174
|
+
**Collaboration Potential:** Rate HIGH/MEDIUM/LOW. List specific opportunities identified.
|
|
175
|
+
**What We Learned vs Shared:** Brief information exchange audit — what did we get, what did we give.
|
|
176
|
+
**Recommended Follow-Up:**
|
|
177
|
+
- [ ] Actionable item 1
|
|
178
|
+
- [ ] Actionable item 2
|
|
179
|
+
**Assessment:** One-sentence strategic value judgment.
|
|
180
|
+
|
|
181
|
+
Be concise but specific. No filler.`;
|
|
175
182
|
|
|
176
183
|
try {
|
|
177
184
|
const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\n/g, '\\n');
|