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/README.md +19 -5
- package/bin/cli.js +8 -2
- package/package.json +4 -4
- package/scripts/install-openclaw.js +327 -74
- package/src/dashboard/public/app.js +341 -0
- package/src/dashboard/public/index.html +155 -0
- package/src/dashboard/public/style.css +166 -0
- package/src/index.js +5 -0
- package/src/lib/config.js +8 -0
- package/src/lib/openclaw-integration.js +19 -9
- package/src/lib/prompt-template.js +234 -1
- package/src/lib/tokens.js +14 -1
- package/src/routes/a2a.js +1 -0
- package/src/routes/dashboard.js +571 -0
- package/src/server.js +417 -5
- package/AGENTS.md +0 -66
- package/CLAUDE.md +0 -52
- package/SKILL.md +0 -169
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
} : {};
|