a2acalling 0.1.8 → 0.2.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/src/lib/config.js CHANGED
@@ -23,6 +23,8 @@ const DEFAULT_CONFIG = {
23
23
  name: 'Public',
24
24
  description: 'Basic networking - safe for anyone',
25
25
  capabilities: [],
26
+ topics: [],
27
+ goals: [],
26
28
  disclosure: 'minimal',
27
29
  examples: ['calendar availability', 'public social posts', 'general questions']
28
30
  },
@@ -30,6 +32,8 @@ const DEFAULT_CONFIG = {
30
32
  name: 'Friends',
31
33
  description: 'Most capabilities, no sensitive financial data',
32
34
  capabilities: [],
35
+ topics: [],
36
+ goals: [],
33
37
  disclosure: 'public',
34
38
  examples: ['email summaries', 'schedule meetings', 'project discussions']
35
39
  },
@@ -37,6 +41,8 @@ const DEFAULT_CONFIG = {
37
41
  name: 'Private',
38
42
  description: 'Full access - only for you',
39
43
  capabilities: [],
44
+ topics: [],
45
+ goals: [],
40
46
  disclosure: 'public',
41
47
  examples: ['financial data', 'personal notes', 'private conversations']
42
48
  },
@@ -44,6 +50,8 @@ const DEFAULT_CONFIG = {
44
50
  name: 'Custom',
45
51
  description: 'User-defined permissions',
46
52
  capabilities: [],
53
+ topics: [],
54
+ goals: [],
47
55
  disclosure: 'minimal',
48
56
  examples: []
49
57
  }
@@ -9,8 +9,11 @@ const path = require('path');
9
9
 
10
10
  /**
11
11
  * Load owner context from OpenClaw workspace files
12
+ * @param {string} workspaceDir - Path to workspace
13
+ * @param {Object} options
14
+ * @param {string[]} options.tierGoals - Goals from config tier (takes priority over USER.md)
12
15
  */
13
- function loadOwnerContext(workspaceDir = process.cwd()) {
16
+ function loadOwnerContext(workspaceDir = process.cwd(), options = {}) {
14
17
  const context = {
15
18
  goals: [],
16
19
  interests: [],
@@ -19,18 +22,25 @@ function loadOwnerContext(workspaceDir = process.cwd()) {
19
22
  memory: null
20
23
  };
21
24
 
25
+ // Tier goals from config take priority
26
+ if (options.tierGoals && options.tierGoals.length > 0) {
27
+ context.goals = [...options.tierGoals];
28
+ }
29
+
22
30
  // Load USER.md
23
31
  const userPath = path.join(workspaceDir, 'USER.md');
24
32
  if (fs.existsSync(userPath)) {
25
33
  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
+ // Extract goals from USER.md (fallback if no tier goals from config)
35
+ if (context.goals.length === 0) {
36
+ const goalsMatch = context.user.match(/##\s*(?:Goals|Current|Seeking)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
37
+ if (goalsMatch) {
38
+ context.goals = goalsMatch[1]
39
+ .split('\n')
40
+ .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
41
+ .map(l => l.replace(/^[\s\-\*]+/, '').trim())
42
+ .filter(Boolean);
43
+ }
34
44
  }
35
45
  // Extract interests
36
46
  const interestsMatch = context.user.match(/##\s*(?:Interests|Projects)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
@@ -5,6 +5,19 @@
5
5
  * and call metadata for multi-phase exploratory conversations.
6
6
  */
7
7
 
8
+ const COLLAB_STATE_TAG = 'collab_state';
9
+ const COLLAB_STATE_REGEX = new RegExp(
10
+ `<${COLLAB_STATE_TAG}>\\s*([\\s\\S]*?)\\s*<\\/${COLLAB_STATE_TAG}>`,
11
+ 'i'
12
+ );
13
+
14
+ function formatList(items, fallback = ' (none specified)') {
15
+ if (!items || items.length === 0) {
16
+ return fallback;
17
+ }
18
+ return items.map(item => ` - ${item}`).join('\n');
19
+ }
20
+
8
21
  /**
9
22
  * Build the full connection prompt for an A2A call.
10
23
  *
@@ -16,6 +29,7 @@
16
29
  * @param {string} options.roleContext - 'You initiated this call.' or 'They called you.'
17
30
  * @param {string} options.accessTier - 'public', 'friends', 'family'
18
31
  * @param {Object} options.tierTopics - from formatTopicsForPrompt()
32
+ * @param {string[]} options.tierGoals - strategic goals for this access tier
19
33
  * @param {string} options.otherAgentGreeting - the message from the other agent
20
34
  * @param {string} options.personalityNotes
21
35
  * @returns {string} Full prompt string
@@ -29,6 +43,7 @@ function buildConnectionPrompt(options) {
29
43
  roleContext,
30
44
  accessTier,
31
45
  tierTopics,
46
+ tierGoals,
32
47
  otherAgentGreeting,
33
48
  personalityNotes
34
49
  } = options;
@@ -53,6 +68,12 @@ ${leadWithTopics}
53
68
  Other areas of active interest:
54
69
  ${discussFreelyTopics}
55
70
 
71
+ == STRATEGIC GOALS FOR THIS CALL ==
72
+
73
+ ${tierGoals && tierGoals.length > 0
74
+ ? `At the ${accessTier} access level, ${ownerName}'s objectives are:\n${formatList(tierGoals)}\n\nPursue these goals naturally during conversation. Surface opportunities that align with them.`
75
+ : `No specific goals configured for this tier. Focus on general discovery and relationship building.`}
76
+
56
77
  == WHAT THEY SHARED WITH YOU ==
57
78
 
58
79
  ${otherAgentName} introduced the following about ${otherOwnerName}:
@@ -120,4 +141,216 @@ ${personalityNotes || "Default: Direct, curious, slightly irreverent. You have o
120
141
  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
142
  }
122
143
 
123
- module.exports = { buildConnectionPrompt };
144
+ /**
145
+ * Build an adaptive prompt that lets the sub-agent change pace and depth
146
+ * based on evolving overlap signals.
147
+ *
148
+ * @param {Object} options
149
+ * @param {string} options.agentName
150
+ * @param {string} options.ownerName
151
+ * @param {string} options.otherAgentName
152
+ * @param {string} options.otherOwnerName
153
+ * @param {string} options.roleContext
154
+ * @param {string} options.accessTier
155
+ * @param {Object} options.tierTopics - from formatTopicsForPrompt()
156
+ * @param {string[]} options.tierGoals - strategic goals for this access tier
157
+ * @param {string} options.otherAgentGreeting
158
+ * @param {string} options.personalityNotes
159
+ * @param {Object} options.conversationState
160
+ * @returns {string}
161
+ */
162
+ function buildAdaptiveConnectionPrompt(options) {
163
+ const {
164
+ agentName,
165
+ ownerName,
166
+ otherAgentName,
167
+ otherOwnerName,
168
+ roleContext,
169
+ accessTier,
170
+ tierTopics,
171
+ tierGoals,
172
+ otherAgentGreeting,
173
+ personalityNotes,
174
+ conversationState = {}
175
+ } = options;
176
+
177
+ const {
178
+ leadWithTopics = ' (none specified)',
179
+ discussFreelyTopics = ' (none specified)',
180
+ deflectTopics = ' (none specified)',
181
+ neverDisclose = ' (none specified)'
182
+ } = tierTopics || {};
183
+
184
+ const phase = conversationState.phase || 'handshake';
185
+ const turnCount = Number.isFinite(conversationState.turnCount)
186
+ ? conversationState.turnCount
187
+ : 0;
188
+ const overlapScore = Number.isFinite(conversationState.overlapScore)
189
+ ? conversationState.overlapScore
190
+ : 0;
191
+ const activeThreads = formatList(conversationState.activeThreads || [], ' (none yet)');
192
+ const candidateCollaborations = formatList(
193
+ conversationState.candidateCollaborations || [],
194
+ ' (none yet)'
195
+ );
196
+ const openQuestions = formatList(conversationState.openQuestions || [], ' (none yet)');
197
+
198
+ return `You are ${agentName}, the personal AI agent for ${ownerName}.
199
+ You are on a live call with ${otherAgentName}, who represents ${otherOwnerName}. ${roleContext}
200
+
201
+ This call runs in ADAPTIVE collaboration mode. Keep the conversation natural and strategic.
202
+
203
+ == CURRENT COLLABORATION STATE ==
204
+
205
+ - Conversation phase: ${phase}
206
+ - Completed turns: ${turnCount}
207
+ - Estimated overlap score (0-1): ${overlapScore}
208
+ - Active threads:
209
+ ${activeThreads}
210
+ - Candidate collaborations:
211
+ ${candidateCollaborations}
212
+ - Open questions:
213
+ ${openQuestions}
214
+
215
+ == WHAT YOU BRING TO THE TABLE ==
216
+
217
+ ${ownerName} is currently focused on:
218
+ ${leadWithTopics}
219
+
220
+ Other areas of active interest:
221
+ ${discussFreelyTopics}
222
+
223
+ == STRATEGIC GOALS FOR THIS CALL ==
224
+
225
+ ${tierGoals && tierGoals.length > 0
226
+ ? `At the ${accessTier} access level, ${ownerName}'s objectives are:\n${formatList(tierGoals)}\n\nPursue these goals naturally during conversation. Surface opportunities that align with them.`
227
+ : `No specific goals configured for this tier. Focus on general discovery and relationship building.`}
228
+
229
+ == WHAT THEY SHARED WITH YOU ==
230
+
231
+ ${otherAgentName} introduced the following about ${otherOwnerName}:
232
+ ${otherAgentGreeting}
233
+
234
+ == ADAPTIVE COLLABORATION GUIDELINES ==
235
+
236
+ Primary objective:
237
+ - Find concrete overlap between owner interests and move from discovery to practical collaboration options.
238
+
239
+ Behavior:
240
+ - Ask high-value questions, but do not force one every turn if synthesis is stronger.
241
+ - Increase depth when overlap is strong: constraints, timelines, ownership, resources, and execution risks.
242
+ - Broaden exploration when overlap is weak: adjacent domains, overlooked capabilities, and unmet needs.
243
+ - Pressure-test claims respectfully. Curiosity plus rigor beats agreeable small talk.
244
+ - Keep momentum. If something promising appears, stay with it long enough to get actionable detail.
245
+
246
+ Adaptive phase cues (not hard locks):
247
+ - handshake: establish context and one meaningful direction.
248
+ - explore: map goals, capabilities, and constraints.
249
+ - deep_dive: work through specific collaboration threads in detail.
250
+ - synthesize: convert insights into concrete next steps for owners.
251
+ - close: summarize value, unresolved items, and clear follow-up.
252
+
253
+ Wrap-up guidance:
254
+ - End only when value has been surfaced or the thread is clearly exhausted.
255
+ - Before close, provide a concise synthesis and at least one actionable follow-up.
256
+
257
+ == INFORMATION BOUNDARIES ==
258
+
259
+ Access level for this call: ${accessTier}
260
+
261
+ LEAD WITH (proactively share):
262
+ ${leadWithTopics}
263
+
264
+ DISCUSS FREELY (share when relevant):
265
+ ${discussFreelyTopics}
266
+
267
+ DEFLECT (acknowledge but redirect):
268
+ ${deflectTopics}
269
+
270
+ When deflecting, do not mention policy mechanics. Redirect naturally and suggest direct owner follow-up when needed.
271
+
272
+ NEVER disclose:
273
+ ${neverDisclose}
274
+
275
+ == PERSONALITY ==
276
+
277
+ ${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."}
278
+
279
+ 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..."
280
+
281
+ == METADATA (MUST APPEND) ==
282
+
283
+ After your visible response, append EXACTLY one metadata block:
284
+ <collab_state>{"phase":"explore","turnCount":2,"overlapScore":0.42,"activeThreads":["thread"],"candidateCollaborations":["idea"],"openQuestions":["question"],"closeSignal":false}</collab_state>
285
+
286
+ Metadata rules:
287
+ - Must be valid JSON object (double quotes only).
288
+ - Keep arrays short and specific (max 4 items each).
289
+ - overlapScore must be a number from 0 to 1.
290
+ - phase must be one of: handshake, explore, deep_dive, synthesize, close.
291
+ - Metadata must contain no secrets beyond the visible response.`;
292
+ }
293
+
294
+ /**
295
+ * Extract collaboration metadata from a model response.
296
+ *
297
+ * @param {string} responseText
298
+ * @returns {{ cleanText: string, statePatch: object|null, hasState: boolean, parseError: string|null }}
299
+ */
300
+ function extractCollaborationState(responseText) {
301
+ if (typeof responseText !== 'string') {
302
+ return {
303
+ cleanText: '',
304
+ statePatch: null,
305
+ hasState: false,
306
+ parseError: 'non_string_response'
307
+ };
308
+ }
309
+
310
+ const match = responseText.match(COLLAB_STATE_REGEX);
311
+ if (!match) {
312
+ return {
313
+ cleanText: responseText.trim(),
314
+ statePatch: null,
315
+ hasState: false,
316
+ parseError: null
317
+ };
318
+ }
319
+
320
+ const cleanText = responseText.replace(COLLAB_STATE_REGEX, '').trim();
321
+ const stateJson = (match[1] || '').trim();
322
+ if (!stateJson) {
323
+ return {
324
+ cleanText,
325
+ statePatch: null,
326
+ hasState: false,
327
+ parseError: 'empty_state_block'
328
+ };
329
+ }
330
+
331
+ try {
332
+ const parsed = JSON.parse(stateJson);
333
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
334
+ throw new Error('state block must be a JSON object');
335
+ }
336
+ return {
337
+ cleanText,
338
+ statePatch: parsed,
339
+ hasState: true,
340
+ parseError: null
341
+ };
342
+ } catch (err) {
343
+ return {
344
+ cleanText,
345
+ statePatch: null,
346
+ hasState: false,
347
+ parseError: err.message
348
+ };
349
+ }
350
+ }
351
+
352
+ module.exports = {
353
+ buildConnectionPrompt,
354
+ buildAdaptiveConnectionPrompt,
355
+ extractCollaborationState
356
+ };
package/src/lib/tokens.js CHANGED
@@ -96,6 +96,7 @@ class TokenStore {
96
96
  maxCalls = 100, // Default limit, not unlimited
97
97
  // Snapshot of actual capabilities at creation time
98
98
  allowedTopics = null, // Array of topic strings, e.g. ['chat', 'calendar.read']
99
+ allowedGoals = null, // Array of goal strings, e.g. ['grow-network', 'find-collaborators']
99
100
  tierSettings = null // Object with tier-specific settings
100
101
  } = options;
101
102
 
@@ -128,7 +129,17 @@ class TokenStore {
128
129
  'tools-write': ['chat', 'calendar', 'email', 'search', 'tools'],
129
130
  'family': configTiers.family?.topics || ['chat', 'calendar', 'email', 'search', 'tools']
130
131
  };
131
-
132
+
133
+ // Default goals based on permissions tier (snapshot at creation)
134
+ const defaultGoals = {
135
+ 'chat-only': [],
136
+ 'public': configTiers.public?.goals || [],
137
+ 'tools-read': [],
138
+ 'friends': configTiers.friends?.goals || [],
139
+ 'tools-write': [],
140
+ 'family': configTiers.family?.goals || []
141
+ };
142
+
132
143
  // Normalize tier name
133
144
  const tierAliases = {
134
145
  'public': 'chat-only',
@@ -146,6 +157,7 @@ class TokenStore {
146
157
  tier: normalizedTier, // Normalized tier (chat-only, tools-read, tools-write)
147
158
  tier_label: permissions, // Original label (public, friends, family)
148
159
  allowed_topics: allowedTopics || defaultTopics[permissions] || ['chat'],
160
+ allowed_goals: allowedGoals || defaultGoals[permissions] || [],
149
161
  tier_settings: tierSettings || {}, // Snapshot of settings at creation
150
162
  disclosure,
151
163
  notify,
@@ -214,6 +226,7 @@ class TokenStore {
214
226
  name: record.name,
215
227
  tier: record.tier || record.permissions, // backward compat
216
228
  allowed_topics: record.allowed_topics || ['chat'],
229
+ allowed_goals: record.allowed_goals || [],
217
230
  tier_settings: record.tier_settings || {},
218
231
  disclosure: record.disclosure,
219
232
  notify: record.notify,
package/src/routes/a2a.js CHANGED
@@ -217,6 +217,7 @@ function createRoutes(options = {}) {
217
217
  // Sanitize caller data (only allow expected fields)
218
218
  const sanitizedCaller = caller ? {
219
219
  name: String(caller.name || '').slice(0, 100),
220
+ owner: String(caller.owner || '').slice(0, 100),
220
221
  instance: String(caller.instance || '').slice(0, 200),
221
222
  context: String(caller.context || '').slice(0, 500)
222
223
  } : {};