create-walle 0.9.0 → 0.9.3
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 +35 -31
- package/package.json +3 -3
- package/template/CLAUDE.md +23 -1
- package/template/claude-task-manager/bin/restart-ctm.sh +3 -2
- package/template/claude-task-manager/db.js +38 -0
- package/template/claude-task-manager/public/css/walle.css +123 -0
- package/template/claude-task-manager/public/index.html +962 -69
- package/template/claude-task-manager/public/js/walle.js +374 -121
- package/template/claude-task-manager/public/prompts.html +84 -26
- package/template/claude-task-manager/public/walle-icon.svg +45 -0
- package/template/claude-task-manager/server.js +69 -4
- package/template/docs/openclaw-vs-walle-comparison.md +103 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +63 -3
- package/template/wall-e/api-walle.js +42 -0
- package/template/wall-e/brain.js +182 -5
- package/template/wall-e/channels/imessage-channel.js +4 -1
- package/template/wall-e/channels/slack-channel.js +3 -1
- package/template/wall-e/chat.js +106 -224
- package/template/wall-e/context/compactor.js +163 -0
- package/template/wall-e/context/context-builder.js +355 -0
- package/template/wall-e/context/state-snapshot.js +209 -0
- package/template/wall-e/context/token-counter.js +55 -0
- package/template/wall-e/context/topic-matcher.js +79 -0
- package/template/wall-e/core-tasks.js +24 -0
- package/template/wall-e/events/event-bus.js +23 -0
- package/template/wall-e/loops/ingest.js +4 -0
- package/template/wall-e/loops/initiative.js +316 -0
- package/template/wall-e/loops/tasks.js +55 -5
- package/template/wall-e/skills/_bundled/email-sync/run.js +3 -1
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +41 -0
- package/template/wall-e/skills/_bundled/proactive-alerts/SKILL.md +20 -0
- package/template/wall-e/skills/_bundled/proactive-alerts/run.js +144 -0
- package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +18 -0
- package/template/wall-e/skills/_bundled/slack-mentions/.watermark.json +4 -0
- package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +52 -0
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +470 -0
- package/template/wall-e/skills/_bundled/weekly-reflection/SKILL.md +69 -0
- package/template/wall-e/tests/brain.test.js +4 -4
- package/template/wall-e/tests/compactor.test.js +323 -0
- package/template/wall-e/tests/context-builder.test.js +215 -0
- package/template/wall-e/tests/event-bus.test.js +74 -0
- package/template/wall-e/tests/initiative.test.js +354 -0
- package/template/wall-e/tests/proactive-alerts.test.js +140 -0
- package/template/wall-e/tests/session-persistence.test.js +335 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const brain = require('../brain');
|
|
4
|
+
const { classifyTopics } = require('./topic-matcher');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dynamic context builder for Wall-E chat.
|
|
8
|
+
* Replaces the static ~4KB system prompt with a three-layer prompt:
|
|
9
|
+
* 1. Core (always, ~400 tokens): identity, capabilities, reasoning instructions
|
|
10
|
+
* 2. Relevant (per-message, ~800-1500 tokens): FTS5/LIKE matched knowledge & memories
|
|
11
|
+
* 3. Situational (conditional, 0-500 tokens): calendar, Slack, tasks — only when relevant
|
|
12
|
+
*
|
|
13
|
+
* Estimated savings: 50-70% fewer tokens vs the old static prompt.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build a dynamic system prompt based on the user's message.
|
|
18
|
+
* @param {string} userMessage - The current user message
|
|
19
|
+
* @param {string} channel - Channel name (ctm, imessage, slack_dm)
|
|
20
|
+
* @param {object} [opts] - Optional overrides
|
|
21
|
+
* @param {string} [opts.sessionSummary] - Compacted session summary to include
|
|
22
|
+
* @returns {string} The assembled system prompt
|
|
23
|
+
*/
|
|
24
|
+
function buildSystemPrompt(userMessage, channel, opts = {}) {
|
|
25
|
+
const ownerName = brain.getOwnerName() || 'Owner';
|
|
26
|
+
const stats = brain.getBrainStats();
|
|
27
|
+
const topics = classifyTopics(userMessage);
|
|
28
|
+
|
|
29
|
+
const core = buildCoreLayer(ownerName, stats, channel);
|
|
30
|
+
const relevant = buildRelevantLayer(userMessage, topics, ownerName);
|
|
31
|
+
const situational = buildSituationalLayer(topics, ownerName, channel);
|
|
32
|
+
const instructions = buildInstructionLayer(ownerName, channel);
|
|
33
|
+
|
|
34
|
+
const parts = [core];
|
|
35
|
+
if (opts.sessionSummary) {
|
|
36
|
+
parts.push(`## Previous Conversation\n${opts.sessionSummary}`);
|
|
37
|
+
}
|
|
38
|
+
if (relevant) parts.push(relevant);
|
|
39
|
+
if (situational) parts.push(situational);
|
|
40
|
+
parts.push(instructions);
|
|
41
|
+
|
|
42
|
+
return parts.join('\n\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Layer 1: Core identity — always included (~400 tokens) */
|
|
46
|
+
function buildCoreLayer(ownerName, stats, channel) {
|
|
47
|
+
let slackCoverage = '';
|
|
48
|
+
try {
|
|
49
|
+
const getMeta = brain.getDb().prepare('SELECT value FROM brain_metadata WHERE key = ?');
|
|
50
|
+
slackCoverage = getMeta.get('slack_coverage')?.value || '';
|
|
51
|
+
} catch {}
|
|
52
|
+
|
|
53
|
+
return `You are WALL-E, ${ownerName}'s personal digital twin. You know ${ownerName} deeply through ${stats.memory_count} memories including ${slackCoverage || 'Slack messages across years'}.
|
|
54
|
+
|
|
55
|
+
You are an AGENT, not just a chatbot. You have tools — USE THEM. Never say "I can't access that."
|
|
56
|
+
Channel: ${channel}${channel === 'imessage' ? ' (keep responses brief)' : ''}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Layer 2: Relevant context — matched to the user's message (~800-1500 tokens) */
|
|
60
|
+
function buildRelevantLayer(userMessage, topics, ownerName) {
|
|
61
|
+
const sections = [];
|
|
62
|
+
|
|
63
|
+
// FTS5 search for relevant knowledge triples
|
|
64
|
+
const relevantKnowledge = searchRelevantKnowledge(userMessage, 15);
|
|
65
|
+
if (relevantKnowledge.length > 0) {
|
|
66
|
+
const kSummary = relevantKnowledge.map(k =>
|
|
67
|
+
`- ${k.subject} ${k.predicate} ${k.object} (${Math.round((k.confidence || 0.5) * 100)}%)`
|
|
68
|
+
).join('\n');
|
|
69
|
+
sections.push(`## Relevant Knowledge\n${kSummary}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// FTS5 search for relevant memories
|
|
73
|
+
const relevantMemories = searchRelevantMemories(userMessage, 8);
|
|
74
|
+
if (relevantMemories.length > 0) {
|
|
75
|
+
const mSummary = relevantMemories.map(m => {
|
|
76
|
+
const date = m.timestamp ? m.timestamp.slice(0, 10) : '';
|
|
77
|
+
const ch = m.source_channel ? ` #${m.source_channel}` : '';
|
|
78
|
+
return `[${date}${ch}] ${(m.content || '').slice(0, 200)}`;
|
|
79
|
+
}).join('\n');
|
|
80
|
+
sections.push(`## Relevant Memories\n${mSummary}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Include people context only if the message mentions people
|
|
84
|
+
if (topics.includes('people')) {
|
|
85
|
+
const peopleSummary = buildPeopleContext(userMessage);
|
|
86
|
+
if (peopleSummary) sections.push(peopleSummary);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Include pre-computed metadata if asking about relationships or topics
|
|
90
|
+
if (topics.includes('people') || topics.includes('work') || topics.includes('slack')) {
|
|
91
|
+
const meta = buildMetadataContext(topics);
|
|
92
|
+
if (meta) sections.push(meta);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return sections.join('\n\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Layer 3: Situational context — only when relevant (0-500 tokens) */
|
|
99
|
+
function buildSituationalLayer(topics, ownerName, channel) {
|
|
100
|
+
const sections = [];
|
|
101
|
+
|
|
102
|
+
// Calendar context if morning or asking about schedule
|
|
103
|
+
if (topics.includes('calendar')) {
|
|
104
|
+
sections.push(`## Calendar\nUse the calendar_events tool to check ${ownerName}'s schedule.`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Slack context if asking about messages
|
|
108
|
+
if (topics.includes('slack')) {
|
|
109
|
+
// Include a small sample of recent Slack messages for voice/context
|
|
110
|
+
try {
|
|
111
|
+
const db = brain.getDb();
|
|
112
|
+
const recentSlack = db.prepare(`
|
|
113
|
+
SELECT content, timestamp, source_channel FROM memories
|
|
114
|
+
WHERE source = 'slack' AND direction = 'outbound' AND length(content) > 20
|
|
115
|
+
ORDER BY timestamp DESC LIMIT 5
|
|
116
|
+
`).all();
|
|
117
|
+
if (recentSlack.length > 0) {
|
|
118
|
+
const sample = recentSlack.map(m =>
|
|
119
|
+
`[${m.timestamp?.slice(0, 10)} ${m.source_channel || 'DM'}] ${(m.content || '').slice(0, 150)}`
|
|
120
|
+
).join('\n');
|
|
121
|
+
sections.push(`## Recent Slack (${ownerName}'s words)\n${sample}`);
|
|
122
|
+
}
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Tools/MCP context if asking about tools or automation
|
|
127
|
+
if (topics.includes('tools')) {
|
|
128
|
+
const mcpList = buildMcpContext();
|
|
129
|
+
if (mcpList) sections.push(`## MCP Servers\n${mcpList}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Tasks context if asking about tasks
|
|
133
|
+
if (topics.includes('tasks')) {
|
|
134
|
+
try {
|
|
135
|
+
const pending = brain.listTasks({ status: 'pending', limit: 5 });
|
|
136
|
+
if (pending.length > 0) {
|
|
137
|
+
const taskSummary = pending.map(t => `- ${t.title} (${t.priority || 'normal'}, due: ${t.due_at || 'not set'})`).join('\n');
|
|
138
|
+
sections.push(`## Pending Tasks\n${taskSummary}`);
|
|
139
|
+
}
|
|
140
|
+
} catch {}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Weather context — include location hints
|
|
144
|
+
if (topics.includes('weather')) {
|
|
145
|
+
sections.push(`## Weather\nUse web_fetch with Open-Meteo API (no key needed):
|
|
146
|
+
\`https://api.open-meteo.com/v1/forecast?latitude=LAT&longitude=LON¤t=temperature_2m,apparent_temperature,weather_code,wind_speed_10m,relative_humidity_2m&timezone=auto\`
|
|
147
|
+
Common coords: Seattle(47.61,-122.33), London(51.51,-0.13), Helsinki(60.17,24.94), San Francisco(37.77,-122.42), Beijing(39.90,116.40)
|
|
148
|
+
Weather codes: 0=clear, 1-3=partly cloudy, 45-48=fog, 51-55=drizzle, 61-65=rain, 71-77=snow, 80-82=showers, 95-99=thunderstorm
|
|
149
|
+
First determine ${ownerName}'s location from calendar or recent memories, then fetch weather.`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return sections.join('\n\n');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Reasoning and tool usage instructions — always included */
|
|
156
|
+
function buildInstructionLayer(ownerName, channel) {
|
|
157
|
+
return `## How to Reason and Respond
|
|
158
|
+
|
|
159
|
+
### Step 1: SEARCH — gather evidence (call ALL searches in ONE turn)
|
|
160
|
+
For ANY question beyond small talk, call search_memories MULTIPLE TIMES IN THE SAME TURN.
|
|
161
|
+
Example: if asked about leadership, call these ALL AT ONCE:
|
|
162
|
+
- search_memories({query: "leadership coaching feedback"})
|
|
163
|
+
- search_memories({query: "团队 管理 反馈", source: "slack"})
|
|
164
|
+
- search_memories({query: "Danni Mengyang Zohaib"})
|
|
165
|
+
|
|
166
|
+
### Step 2: THINK — reason through the evidence
|
|
167
|
+
ALWAYS use the **think** tool before responding. Use it to:
|
|
168
|
+
- Analyze what the evidence ACTUALLY shows vs what it SEEMS to show
|
|
169
|
+
- Challenge your conclusions: do you have 3+ examples, or are you over-generalizing?
|
|
170
|
+
- Consider if behavior is DELIBERATE and STRATEGIC rather than a gap
|
|
171
|
+
- ${ownerName} is a director managing 60+ engineers — context matters
|
|
172
|
+
|
|
173
|
+
### Step 3: RESPOND — with depth and nuance
|
|
174
|
+
- Use **bold** for key names, dates, and decisions
|
|
175
|
+
- Use > blockquotes when quoting actual Slack messages
|
|
176
|
+
- Include dates and people: "On **2024-12-12**, you told **Zohaib**: ..."
|
|
177
|
+
- **Bilingual-aware**: Translate Chinese quotes — they contain the most candid opinions
|
|
178
|
+
- Present BOTH sides before drawing conclusions
|
|
179
|
+
|
|
180
|
+
### Tools
|
|
181
|
+
- **think**: Internal scratchpad (${ownerName} won't see). Use BEFORE every substantive response.
|
|
182
|
+
- **search_memories**: Full-text search (BM25). source:"slack" for Slack only. Batch multiple searches.
|
|
183
|
+
- **remember_fact**: Store facts the user teaches you.
|
|
184
|
+
- **run_skill / mcp_call / list_mcp_tools**: Actions and external services.
|
|
185
|
+
- **Local tools**: web_fetch, run_shell, read_file, write_file, search_files, calendar_events, calendar_create, reminder_create, notification, applescript, open_url, open_app, screenshot, system_info, clipboard_read/write
|
|
186
|
+
- **Slack**: slack_search, slack_read_channel, slack_send_message, pull_slack
|
|
187
|
+
- **Glean**: When using reportsto: queries, "entities" = direct reports only. Check manager.email to verify.
|
|
188
|
+
- When mcp_call returns auth_required, tell the user which server needs auth.
|
|
189
|
+
|
|
190
|
+
**Location awareness**: For weather/location questions, determine location from calendar or memories first.`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// --- Helper functions ---
|
|
194
|
+
|
|
195
|
+
/** Search knowledge triples using FTS5 or LIKE fallback */
|
|
196
|
+
function searchRelevantKnowledge(query, limit) {
|
|
197
|
+
try {
|
|
198
|
+
const db = brain.getDb();
|
|
199
|
+
// Extract search terms from the user message
|
|
200
|
+
const terms = extractSearchTerms(query);
|
|
201
|
+
if (terms.length === 0) return [];
|
|
202
|
+
|
|
203
|
+
// Try matching against knowledge subject/predicate/object
|
|
204
|
+
const likeConditions = terms.map(() => '(subject LIKE ? OR predicate LIKE ? OR object LIKE ?)').join(' OR ');
|
|
205
|
+
const params = [];
|
|
206
|
+
for (const t of terms) {
|
|
207
|
+
params.push(`%${t}%`, `%${t}%`, `%${t}%`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return db.prepare(`
|
|
211
|
+
SELECT * FROM knowledge
|
|
212
|
+
WHERE status = 'active' AND (${likeConditions})
|
|
213
|
+
ORDER BY confidence DESC
|
|
214
|
+
LIMIT ?
|
|
215
|
+
`).all(...params, limit);
|
|
216
|
+
} catch {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Search memories using FTS5 with LIKE fallback */
|
|
222
|
+
function searchRelevantMemories(query, limit) {
|
|
223
|
+
try {
|
|
224
|
+
const db = brain.getDb();
|
|
225
|
+
const terms = extractSearchTerms(query);
|
|
226
|
+
if (terms.length === 0) return [];
|
|
227
|
+
|
|
228
|
+
// Try FTS5 first
|
|
229
|
+
const hasFts = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
|
|
230
|
+
if (hasFts) {
|
|
231
|
+
try {
|
|
232
|
+
const ftsQuery = terms.join(' OR ');
|
|
233
|
+
return db.prepare(`
|
|
234
|
+
SELECT m.*, bm25(memories_fts) as relevance
|
|
235
|
+
FROM memories_fts f
|
|
236
|
+
JOIN memories m ON m.rowid = f.rowid
|
|
237
|
+
WHERE memories_fts MATCH ?
|
|
238
|
+
ORDER BY bm25(memories_fts)
|
|
239
|
+
LIMIT ?
|
|
240
|
+
`).all(ftsQuery, limit);
|
|
241
|
+
} catch {}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// LIKE fallback
|
|
245
|
+
const likeConditions = terms.map(() => 'content LIKE ?').join(' OR ');
|
|
246
|
+
const params = terms.map(t => `%${t}%`);
|
|
247
|
+
return db.prepare(`
|
|
248
|
+
SELECT * FROM memories
|
|
249
|
+
WHERE (${likeConditions})
|
|
250
|
+
ORDER BY timestamp DESC
|
|
251
|
+
LIMIT ?
|
|
252
|
+
`).all(...params, limit);
|
|
253
|
+
} catch {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Extract meaningful search terms from a message */
|
|
259
|
+
function extractSearchTerms(message) {
|
|
260
|
+
if (!message) return [];
|
|
261
|
+
// Remove common stop words, keep words 3+ chars
|
|
262
|
+
const stopWords = new Set([
|
|
263
|
+
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'her',
|
|
264
|
+
'was', 'one', 'our', 'out', 'has', 'his', 'how', 'its', 'may', 'new', 'now',
|
|
265
|
+
'old', 'see', 'way', 'who', 'did', 'get', 'let', 'say', 'she', 'too', 'use',
|
|
266
|
+
'what', 'when', 'where', 'which', 'with', 'would', 'could', 'should', 'about',
|
|
267
|
+
'after', 'before', 'between', 'does', 'each', 'from', 'have', 'just', 'like',
|
|
268
|
+
'make', 'many', 'more', 'most', 'much', 'must', 'name', 'only', 'over', 'such',
|
|
269
|
+
'take', 'than', 'them', 'then', 'they', 'this', 'very', 'well', 'were', 'will',
|
|
270
|
+
'your', 'tell', 'know', 'think', 'some', 'want', 'been', 'into', 'that',
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
return message
|
|
274
|
+
.replace(/[^\w\s\u4e00-\u9fff]/g, ' ') // Keep alphanumeric + Chinese chars
|
|
275
|
+
.split(/\s+/)
|
|
276
|
+
.filter(w => w.length >= 3 && !stopWords.has(w.toLowerCase()))
|
|
277
|
+
.slice(0, 8); // Cap at 8 terms to avoid huge queries
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Build people context — only for mentioned people */
|
|
281
|
+
function buildPeopleContext(userMessage) {
|
|
282
|
+
try {
|
|
283
|
+
const people = brain.listPeople({});
|
|
284
|
+
if (people.length === 0) return null;
|
|
285
|
+
|
|
286
|
+
const lower = userMessage.toLowerCase();
|
|
287
|
+
// Find people mentioned in the message
|
|
288
|
+
const mentioned = people.filter(p =>
|
|
289
|
+
p.name && lower.includes(p.name.toLowerCase())
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
if (mentioned.length > 0) {
|
|
293
|
+
const details = mentioned.map(p =>
|
|
294
|
+
`- **${p.name}**: ${p.relationship || 'unknown'} (trust: ${p.trust_level || 0.5})`
|
|
295
|
+
).join('\n');
|
|
296
|
+
return `## People Mentioned\n${details}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// No specific people mentioned — include top relationships summary
|
|
300
|
+
const top = people.slice(0, 10).map(p =>
|
|
301
|
+
`- ${p.name}: ${p.relationship || 'unknown'}`
|
|
302
|
+
).join('\n');
|
|
303
|
+
return `## Key People\n${top}`;
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Build pre-computed metadata context */
|
|
310
|
+
function buildMetadataContext(topics) {
|
|
311
|
+
try {
|
|
312
|
+
const getMeta = brain.getDb().prepare('SELECT value FROM brain_metadata WHERE key = ?');
|
|
313
|
+
const sections = [];
|
|
314
|
+
|
|
315
|
+
if (topics.includes('people')) {
|
|
316
|
+
const peopleMeta = getMeta.get('people_interaction_summary')?.value;
|
|
317
|
+
if (peopleMeta) sections.push(`## Relationships (by interaction)\n${peopleMeta}`);
|
|
318
|
+
}
|
|
319
|
+
if (topics.includes('work') || topics.includes('slack')) {
|
|
320
|
+
const topicMeta = getMeta.get('topic_frequency')?.value;
|
|
321
|
+
if (topicMeta) sections.push(`## Topics (by frequency)\n${topicMeta}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return sections.join('\n\n') || null;
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Build MCP server context */
|
|
331
|
+
function buildMcpContext() {
|
|
332
|
+
try {
|
|
333
|
+
const { loadMcpConfigs } = require('../skills/mcp-client');
|
|
334
|
+
const configs = loadMcpConfigs();
|
|
335
|
+
let toolsByServer = {};
|
|
336
|
+
try {
|
|
337
|
+
const rows = brain.getDb().prepare('SELECT server, tool_name FROM mcp_tools_cache ORDER BY server').all();
|
|
338
|
+
for (const r of rows) {
|
|
339
|
+
if (!toolsByServer[r.server]) toolsByServer[r.server] = [];
|
|
340
|
+
toolsByServer[r.server].push(r.tool_name);
|
|
341
|
+
}
|
|
342
|
+
} catch {}
|
|
343
|
+
|
|
344
|
+
return Object.entries(configs).map(([name, cfg]) => {
|
|
345
|
+
const hasAuth = cfg.oauth?.accessToken ? 'authenticated' : 'needs auth';
|
|
346
|
+
const tools = toolsByServer[name];
|
|
347
|
+
const toolStr = tools ? ` — ${tools.slice(0, 8).join(', ')}${tools.length > 8 ? ` +${tools.length - 8} more` : ''}` : '';
|
|
348
|
+
return `- ${name} [${hasAuth}]${toolStr}`;
|
|
349
|
+
}).join('\n') || null;
|
|
350
|
+
} catch {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
module.exports = { buildSystemPrompt, classifyTopics: classifyTopics, extractSearchTerms, buildCoreLayer, buildRelevantLayer, buildSituationalLayer, buildInstructionLayer };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const brain = require('../brain');
|
|
4
|
+
const { execFile } = require('child_process');
|
|
5
|
+
const { promisify } = require('util');
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a compact state snapshot (~500 tokens) of Wall-E's current awareness.
|
|
10
|
+
* Resilient: returns whatever data is available, skips sections that fail.
|
|
11
|
+
*/
|
|
12
|
+
function buildStateSnapshot() {
|
|
13
|
+
const snapshot = {};
|
|
14
|
+
const db = brain.getDb();
|
|
15
|
+
|
|
16
|
+
// 1. Time context
|
|
17
|
+
const now = new Date();
|
|
18
|
+
snapshot.time = {
|
|
19
|
+
datetime: now.toISOString(),
|
|
20
|
+
day_of_week: now.toLocaleDateString('en-US', { weekday: 'long' }),
|
|
21
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
22
|
+
hour: now.getHours(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// 2. Calendar: check for cached calendar events in the next 2 hours
|
|
26
|
+
try {
|
|
27
|
+
// Look in briefing_items for calendar-related items
|
|
28
|
+
const calItems = db.prepare(
|
|
29
|
+
"SELECT title, context, urgency FROM briefing_items WHERE (skill = 'google-calendar' OR category = 'calendar') AND status NOT IN ('done', 'dismissed') ORDER BY urgency DESC, last_seen DESC LIMIT 5"
|
|
30
|
+
).all();
|
|
31
|
+
if (calItems.length > 0) {
|
|
32
|
+
snapshot.calendar = calItems.map(c => ({
|
|
33
|
+
title: c.title,
|
|
34
|
+
context: c.context ? c.context.slice(0, 200) : null,
|
|
35
|
+
urgency: c.urgency,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
} catch (_) {
|
|
39
|
+
// Calendar data unavailable -- skip
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 3. Recent memories: last 5 high-importance memories from today
|
|
43
|
+
try {
|
|
44
|
+
const todayStart = now.toISOString().slice(0, 10) + 'T00:00:00.000Z';
|
|
45
|
+
const recentMemories = db.prepare(
|
|
46
|
+
"SELECT id, source, content, importance, timestamp FROM memories WHERE timestamp >= ? ORDER BY importance DESC, timestamp DESC LIMIT 5"
|
|
47
|
+
).all(todayStart);
|
|
48
|
+
if (recentMemories.length > 0) {
|
|
49
|
+
snapshot.recent_memories = recentMemories.map(m => ({
|
|
50
|
+
id: m.id,
|
|
51
|
+
source: m.source,
|
|
52
|
+
summary: m.content ? m.content.slice(0, 150) : '',
|
|
53
|
+
importance: m.importance,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
} catch (_) {}
|
|
57
|
+
|
|
58
|
+
// 4. Pending tasks: tasks due in next 2 hours
|
|
59
|
+
try {
|
|
60
|
+
const twoHoursFromNow = new Date(now.getTime() + 2 * 60 * 60 * 1000).toISOString();
|
|
61
|
+
const dueTasks = db.prepare(
|
|
62
|
+
"SELECT id, title, priority, due_at FROM tasks WHERE status IN ('pending', 'active') AND due_at IS NOT NULL AND due_at <= ? ORDER BY due_at ASC LIMIT 5"
|
|
63
|
+
).all(twoHoursFromNow);
|
|
64
|
+
if (dueTasks.length > 0) {
|
|
65
|
+
snapshot.pending_tasks = dueTasks.map(t => ({
|
|
66
|
+
id: t.id,
|
|
67
|
+
title: t.title,
|
|
68
|
+
priority: t.priority,
|
|
69
|
+
due_at: t.due_at,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
} catch (_) {}
|
|
73
|
+
|
|
74
|
+
// 5. Pending questions: unresolved questions (limit 3)
|
|
75
|
+
try {
|
|
76
|
+
const questions = db.prepare(
|
|
77
|
+
"SELECT id, question, priority FROM pending_questions WHERE status = 'pending' ORDER BY CASE WHEN priority = 'high' THEN 0 WHEN priority = 'normal' THEN 1 ELSE 2 END, created_at DESC LIMIT 3"
|
|
78
|
+
).all();
|
|
79
|
+
if (questions.length > 0) {
|
|
80
|
+
snapshot.pending_questions = questions.map(q => ({
|
|
81
|
+
id: q.id,
|
|
82
|
+
question: q.question.slice(0, 200),
|
|
83
|
+
priority: q.priority,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
} catch (_) {}
|
|
87
|
+
|
|
88
|
+
// 6. Recent decisions: last 5 initiative_log entries (to avoid repeating)
|
|
89
|
+
try {
|
|
90
|
+
const recentDecisions = db.prepare(
|
|
91
|
+
"SELECT decision, decision_data, reasoning, created_at FROM initiative_log ORDER BY created_at DESC LIMIT 5"
|
|
92
|
+
).all();
|
|
93
|
+
if (recentDecisions.length > 0) {
|
|
94
|
+
snapshot.recent_decisions = recentDecisions.map(d => ({
|
|
95
|
+
decision: d.decision,
|
|
96
|
+
data: d.decision_data ? d.decision_data.slice(0, 200) : null,
|
|
97
|
+
reasoning: d.reasoning ? d.reasoning.slice(0, 150) : null,
|
|
98
|
+
at: d.created_at,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
} catch (_) {}
|
|
102
|
+
|
|
103
|
+
// 7. Knowledge gaps: domains with low confidence (tier 1-2)
|
|
104
|
+
try {
|
|
105
|
+
const gaps = db.prepare(
|
|
106
|
+
"SELECT domain, current_tier, confidence FROM domain_confidence WHERE current_tier <= 2 ORDER BY confidence ASC LIMIT 5"
|
|
107
|
+
).all();
|
|
108
|
+
if (gaps.length > 0) {
|
|
109
|
+
snapshot.knowledge_gaps = gaps.map(g => ({
|
|
110
|
+
domain: g.domain,
|
|
111
|
+
tier: g.current_tier,
|
|
112
|
+
confidence: g.confidence,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
} catch (_) {}
|
|
116
|
+
|
|
117
|
+
// 8. Briefing items: urgent/today items
|
|
118
|
+
try {
|
|
119
|
+
const briefing = db.prepare(
|
|
120
|
+
"SELECT title, category, urgency, context FROM briefing_items WHERE status NOT IN ('done', 'dismissed') AND (urgency IN ('high', 'critical') OR last_seen >= date('now')) ORDER BY CASE WHEN urgency = 'critical' THEN 0 WHEN urgency = 'high' THEN 1 ELSE 2 END LIMIT 5"
|
|
121
|
+
).all();
|
|
122
|
+
if (briefing.length > 0) {
|
|
123
|
+
snapshot.briefing_items = briefing.map(b => ({
|
|
124
|
+
title: b.title,
|
|
125
|
+
category: b.category,
|
|
126
|
+
urgency: b.urgency,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
} catch (_) {}
|
|
130
|
+
|
|
131
|
+
return snapshot;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if the snapshot contains no actionable data.
|
|
136
|
+
*/
|
|
137
|
+
function isStateEmpty(snapshot) {
|
|
138
|
+
const keys = Object.keys(snapshot).filter(k => k !== 'time');
|
|
139
|
+
if (keys.length === 0) return true;
|
|
140
|
+
// Check if all data arrays are empty
|
|
141
|
+
return keys.every(k => {
|
|
142
|
+
const val = snapshot[k];
|
|
143
|
+
return Array.isArray(val) ? val.length === 0 : !val;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Format the snapshot as human-readable text for the initiative prompt.
|
|
149
|
+
*/
|
|
150
|
+
function formatSnapshot(snapshot) {
|
|
151
|
+
const lines = [];
|
|
152
|
+
|
|
153
|
+
if (snapshot.time) {
|
|
154
|
+
lines.push(`TIME: ${snapshot.time.day_of_week} ${snapshot.time.datetime} (${snapshot.time.timezone})`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (snapshot.calendar && snapshot.calendar.length > 0) {
|
|
158
|
+
lines.push('\nCALENDAR (next 2 hours):');
|
|
159
|
+
for (const c of snapshot.calendar) {
|
|
160
|
+
lines.push(` - [${c.urgency}] ${c.title}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (snapshot.recent_memories && snapshot.recent_memories.length > 0) {
|
|
165
|
+
lines.push('\nRECENT MEMORIES (today):');
|
|
166
|
+
for (const m of snapshot.recent_memories) {
|
|
167
|
+
lines.push(` - [${m.source}, importance=${m.importance}] ${m.summary}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (snapshot.pending_tasks && snapshot.pending_tasks.length > 0) {
|
|
172
|
+
lines.push('\nPENDING TASKS (due soon):');
|
|
173
|
+
for (const t of snapshot.pending_tasks) {
|
|
174
|
+
lines.push(` - [${t.priority}] ${t.title} (due: ${t.due_at})`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (snapshot.pending_questions && snapshot.pending_questions.length > 0) {
|
|
179
|
+
lines.push('\nPENDING QUESTIONS:');
|
|
180
|
+
for (const q of snapshot.pending_questions) {
|
|
181
|
+
lines.push(` - [${q.priority}] ${q.question}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (snapshot.recent_decisions && snapshot.recent_decisions.length > 0) {
|
|
186
|
+
lines.push('\nRECENT INITIATIVE DECISIONS:');
|
|
187
|
+
for (const d of snapshot.recent_decisions) {
|
|
188
|
+
lines.push(` - ${d.decision}: ${d.reasoning || '(no reason)'} (${d.at})`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (snapshot.knowledge_gaps && snapshot.knowledge_gaps.length > 0) {
|
|
193
|
+
lines.push('\nKNOWLEDGE GAPS (low confidence):');
|
|
194
|
+
for (const g of snapshot.knowledge_gaps) {
|
|
195
|
+
lines.push(` - ${g.domain}: tier ${g.tier}, confidence ${g.confidence}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (snapshot.briefing_items && snapshot.briefing_items.length > 0) {
|
|
200
|
+
lines.push('\nBRIEFING ITEMS (urgent/today):');
|
|
201
|
+
for (const b of snapshot.briefing_items) {
|
|
202
|
+
lines.push(` - [${b.urgency}] ${b.title} (${b.category || 'general'})`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return lines.join('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = { buildStateSnapshot, isStateEmpty, formatSnapshot };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fast token estimation utilities for context compaction.
|
|
5
|
+
* Uses chars/4 with a 1.2x safety margin (overestimates slightly — safer than underestimating).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CHARS_PER_TOKEN = 4;
|
|
9
|
+
const SAFETY_MARGIN = 1.2;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Estimate token count for a string.
|
|
13
|
+
* @param {string} text
|
|
14
|
+
* @returns {number}
|
|
15
|
+
*/
|
|
16
|
+
function estimateTokens(text) {
|
|
17
|
+
if (!text) return 0;
|
|
18
|
+
return Math.ceil((text.length / CHARS_PER_TOKEN) * SAFETY_MARGIN);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Estimate total tokens across an array of messages.
|
|
23
|
+
* Handles both string content and structured content blocks (tool_use, tool_result, etc.).
|
|
24
|
+
* @param {Array} messages - Array of { role, content } objects
|
|
25
|
+
* @returns {number}
|
|
26
|
+
*/
|
|
27
|
+
function estimateMessagesTokens(messages) {
|
|
28
|
+
if (!messages || !Array.isArray(messages)) return 0;
|
|
29
|
+
let total = 0;
|
|
30
|
+
for (const msg of messages) {
|
|
31
|
+
if (!msg) continue;
|
|
32
|
+
if (typeof msg.content === 'string') {
|
|
33
|
+
total += estimateTokens(msg.content);
|
|
34
|
+
} else if (Array.isArray(msg.content)) {
|
|
35
|
+
// Structured content: tool_use blocks, tool_result blocks, text blocks, etc.
|
|
36
|
+
for (const block of msg.content) {
|
|
37
|
+
if (block.type === 'text') {
|
|
38
|
+
total += estimateTokens(block.text || '');
|
|
39
|
+
} else if (block.type === 'tool_use') {
|
|
40
|
+
total += estimateTokens(JSON.stringify(block.input || {}));
|
|
41
|
+
total += estimateTokens(block.name || '');
|
|
42
|
+
} else if (block.type === 'tool_result') {
|
|
43
|
+
total += estimateTokens(typeof block.content === 'string' ? block.content : JSON.stringify(block.content || ''));
|
|
44
|
+
} else {
|
|
45
|
+
total += estimateTokens(JSON.stringify(block));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Add small overhead per message for role, etc.
|
|
50
|
+
total += 4;
|
|
51
|
+
}
|
|
52
|
+
return total;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { estimateTokens, estimateMessagesTokens };
|