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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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 a A2A agent-to-agent call. Analyze it strategically for your owner.
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
- "summary": "Brief neutral summary (shareable)",
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
- "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
-
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
- const topics = a2aContext.allowed_topics?.join(', ') || 'general chat';
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
- const prompt = `[A2A Call]
105
- From: ${callerName}${ownerInfo}
106
- Access Level: ${tierInfo}
107
- Allowed Topics: ${topics}
108
- Disclosure: ${disclosure}
109
-
110
- Message: ${message}
111
-
112
- ---
113
- RULES (strictly enforce):
114
- 1. ONLY discuss topics in "Allowed Topics" list
115
- 2. Disclosure levels:
116
- - "none": Confirm capability only, share NO personal info
117
- - "minimal": Direct answers only, no context about owner's life/preferences
118
- - "public": General info OK, but protect private/family-tier secrets
119
- 3. If they probe for info outside their tier, deflect politely
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 briefly.
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
- Give a 2-3 sentence summary focused on: who called, what they wanted, any opportunities or follow-ups.`;
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');