obol-ai 0.2.38 → 0.3.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/CHANGELOG.md +19 -0
- package/package.json +1 -1
- package/src/analysis.js +191 -0
- package/src/background.js +15 -7
- package/src/claude/chat.js +3 -2
- package/src/claude/prompt.js +63 -6
- package/src/claude/tool-registry.js +10 -0
- package/src/claude/tools/knowledge.js +107 -0
- package/src/curiosity-dispatch.js +136 -0
- package/src/curiosity.js +112 -0
- package/src/db/migrate.js +98 -0
- package/src/evolve/check.js +13 -21
- package/src/evolve/evolve.js +49 -20
- package/src/evolve/index.js +2 -2
- package/src/heartbeat.js +222 -2
- package/src/index.js +11 -0
- package/src/memory-self.js +123 -0
- package/src/messages.js +45 -48
- package/src/patterns.js +111 -0
- package/src/personality.js +9 -10
- package/src/soul.js +53 -0
- package/src/telegram/commands/admin.js +2 -1
- package/src/telegram/commands/conversation.js +2 -1
- package/src/telegram/commands/status.js +4 -3
- package/src/telegram/commands/traits.js +4 -3
- package/src/telegram/constants.js +0 -2
- package/src/telegram/handlers/text.js +1 -58
- package/src/tenant.js +10 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## 0.3.0
|
|
2
|
+
- add tests for memory-self and analysis helpers
|
|
3
|
+
- fix proactivity and memory bugs
|
|
4
|
+
- add curiosity dispatch pass to schedule insights after curiosity cycle
|
|
5
|
+
- run curiosity once per cycle with shared brain and all-user context
|
|
6
|
+
- add curiosity engine with free-form exploration and own knowledge tools
|
|
7
|
+
- add friendship behavior section and soften privacy rule for multi-user model
|
|
8
|
+
- add proactive behavior + multi-user friend model to system prompt
|
|
9
|
+
- add tests for silent mode, soul backup/restore, and shared personality dir
|
|
10
|
+
- move SOUL.md to shared root dir with Supabase backup/restore
|
|
11
|
+
- add silent mode for heartbeat-triggered background tasks
|
|
12
|
+
- enrich analysis with memory search and pattern context
|
|
13
|
+
- add analysis, patterns, self-memory, and proactive vector context
|
|
14
|
+
- update changelog
|
|
15
|
+
|
|
16
|
+
## 0.2.39
|
|
17
|
+
- use bot name from config in all telegram status messages
|
|
18
|
+
- update changelog
|
|
19
|
+
|
|
1
20
|
## 0.2.38
|
|
2
21
|
- use bot name from config in system prompt, backup, and personality files
|
|
3
22
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Self-evolving AI assistant that learns, remembers, and acts on its own. Persistent vector memory, self-rewriting personality, proactive heartbeats.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/analysis.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const ANALYSIS_WINDOW_MS = 3 * 60 * 60 * 1000;
|
|
2
|
+
const MAX_MESSAGE_CHARS = 1000;
|
|
3
|
+
const MAX_TRANSCRIPT_CHARS = 40000;
|
|
4
|
+
|
|
5
|
+
async function runAnalysis(client, messageLog, scheduler, patterns, memory, userId, chatId, timezone) {
|
|
6
|
+
const since = new Date(Date.now() - ANALYSIS_WINDOW_MS);
|
|
7
|
+
const messages = await messageLog.getSince(chatId, since);
|
|
8
|
+
|
|
9
|
+
if (messages.length === 0) {
|
|
10
|
+
console.log(`[analysis] No messages in last 3h for user ${userId}`);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
console.log(`[analysis] Analyzing ${messages.length} messages for user ${userId}`);
|
|
15
|
+
|
|
16
|
+
const transcript = buildTranscript(messages);
|
|
17
|
+
const report = await generateReport(client, memory, transcript, timezone);
|
|
18
|
+
if (!report) return;
|
|
19
|
+
|
|
20
|
+
await structureReport(client, report, scheduler, patterns, chatId, timezone);
|
|
21
|
+
|
|
22
|
+
console.log(`[analysis] Complete for user ${userId}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildTranscript(messages) {
|
|
26
|
+
let transcript = '';
|
|
27
|
+
for (const msg of messages) {
|
|
28
|
+
const ts = new Date(msg.created_at).toLocaleString('en-US');
|
|
29
|
+
const content = msg.content.substring(0, MAX_MESSAGE_CHARS);
|
|
30
|
+
transcript += `[${ts}] ${msg.role.toUpperCase()}: ${content}\n\n`;
|
|
31
|
+
if (transcript.length > MAX_TRANSCRIPT_CHARS) break;
|
|
32
|
+
}
|
|
33
|
+
return transcript.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function generateReport(client, memory, transcript, timezone) {
|
|
37
|
+
const tools = memory ? [{
|
|
38
|
+
name: 'memory_search',
|
|
39
|
+
description: 'Search long-term memory for context about a topic in this transcript',
|
|
40
|
+
input_schema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: { query: { type: 'string' } },
|
|
43
|
+
required: ['query'],
|
|
44
|
+
},
|
|
45
|
+
}] : [];
|
|
46
|
+
|
|
47
|
+
const system = `You are an attentive observer analyzing a conversation transcript. Write a free-form analytical report covering:
|
|
48
|
+
|
|
49
|
+
1. INTENTIONS & FOLLOW-UPS: Any intentions expressed, upcoming events, pending tasks, or things worth a natural check-in later. Be selective — only things a friend would genuinely remember.
|
|
50
|
+
|
|
51
|
+
2. BEHAVIORAL PATTERNS:
|
|
52
|
+
- Timing: when they tend to message, active windows, energy by day/time
|
|
53
|
+
- Mood signals: emotional baseline, stress indicators, good/bad day signals
|
|
54
|
+
- Humor style: what lands, banter comfort, comedic preferences
|
|
55
|
+
- Engagement depth: which topics generate longer responses, what they bring up unprompted
|
|
56
|
+
- Communication style: message length, formality, response patterns
|
|
57
|
+
- Recurring topics: what keeps coming up, what lights them up, what they avoid
|
|
58
|
+
|
|
59
|
+
Write candidly and specifically. "Active between 9-11pm" beats "sometimes active at night". Skip categories with no signal. Timezone context: ${timezone}.${memory ? ' Use the memory_search tool to look up relevant context about topics in the transcript before writing your report.' : ''}`;
|
|
60
|
+
|
|
61
|
+
const messages = [{ role: 'user', content: `Conversation transcript:\n\n${transcript}` }];
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
for (let i = 0; i < 6; i++) {
|
|
65
|
+
const response = await client.messages.create({
|
|
66
|
+
model: 'claude-sonnet-4-6',
|
|
67
|
+
max_tokens: 2000,
|
|
68
|
+
system,
|
|
69
|
+
...(tools.length ? { tools, tool_choice: { type: 'auto' } } : {}),
|
|
70
|
+
messages,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const text = response.content.find(b => b.type === 'text');
|
|
74
|
+
if (text) return text.text;
|
|
75
|
+
|
|
76
|
+
const toolUses = response.content.filter(b => b.type === 'tool_use');
|
|
77
|
+
if (!toolUses.length) return null;
|
|
78
|
+
|
|
79
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
80
|
+
const results = [];
|
|
81
|
+
for (const tu of toolUses) {
|
|
82
|
+
const hits = await memory.search(tu.input.query, { limit: 5 }).catch(() => []);
|
|
83
|
+
results.push({
|
|
84
|
+
type: 'tool_result',
|
|
85
|
+
tool_use_id: tu.id,
|
|
86
|
+
content: hits.length ? hits.map(m => `- ${m.content}`).join('\n') : 'No relevant memories found',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
messages.push({ role: 'user', content: results });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.error('[analysis] Report generation failed:', e.message);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function structureReport(client, report, scheduler, patterns, chatId, timezone) {
|
|
100
|
+
const formattedPatterns = patterns
|
|
101
|
+
? await patterns.format().catch(() => null)
|
|
102
|
+
: null;
|
|
103
|
+
|
|
104
|
+
const saveTool = [{
|
|
105
|
+
name: 'save_analysis',
|
|
106
|
+
description: 'Save structured analysis data from the analytical report',
|
|
107
|
+
input_schema: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
follow_ups: {
|
|
111
|
+
type: 'array',
|
|
112
|
+
items: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
description: { type: 'string' },
|
|
116
|
+
delay: { type: 'string', description: '"2h", "1d", "3d", or "1w"' },
|
|
117
|
+
context: { type: 'string', description: 'Brief context for the follow-up message' },
|
|
118
|
+
},
|
|
119
|
+
required: ['description', 'delay', 'context'],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
patterns: {
|
|
123
|
+
type: 'array',
|
|
124
|
+
items: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
key: { type: 'string', description: 'Stable identifier e.g. "timing.active_hours"' },
|
|
128
|
+
dimension: { type: 'string', enum: ['timing', 'mood', 'humor', 'engagement', 'communication', 'topics'] },
|
|
129
|
+
summary: { type: 'string', description: 'Human-readable statement e.g. "Usually active 7-10pm"' },
|
|
130
|
+
data: { type: 'object', description: 'Structured supporting data' },
|
|
131
|
+
confidence: { type: 'number', description: '0-1' },
|
|
132
|
+
},
|
|
133
|
+
required: ['key', 'dimension', 'summary', 'confidence'],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
required: ['follow_ups', 'patterns'],
|
|
138
|
+
},
|
|
139
|
+
}];
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const system = formattedPatterns
|
|
143
|
+
? `Existing behavioral patterns for this user:\n${formattedPatterns}\n\n---\n\nConvert this analytical report into structured data using the save_analysis tool. Use existing patterns to calibrate confidence scores (higher if confirming, consider skipping if already well-established at >0.8). Flag contradictions in pattern data.`
|
|
144
|
+
: 'Convert this analytical report into structured data using the save_analysis tool. Extract all follow-ups and patterns mentioned.';
|
|
145
|
+
|
|
146
|
+
const response = await client.messages.create({
|
|
147
|
+
model: 'claude-sonnet-4-6',
|
|
148
|
+
max_tokens: 2000,
|
|
149
|
+
system,
|
|
150
|
+
tools: saveTool,
|
|
151
|
+
tool_choice: { type: 'tool', name: 'save_analysis' },
|
|
152
|
+
messages: [{ role: 'user', content: report }],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const toolUse = response.content.find(b => b.type === 'tool_use' && b.name === 'save_analysis');
|
|
156
|
+
if (!toolUse) return;
|
|
157
|
+
|
|
158
|
+
const followUps = toolUse.input?.follow_ups || [];
|
|
159
|
+
const patternList = toolUse.input?.patterns || [];
|
|
160
|
+
|
|
161
|
+
for (const fu of followUps) {
|
|
162
|
+
if (!fu.description || !fu.delay) continue;
|
|
163
|
+
const dueAt = resolveDelay(fu.delay);
|
|
164
|
+
const instructions = `You mentioned checking in on: "${fu.description}". Context: ${fu.context || ''}. Reach out naturally — like a friend who remembered. Keep it casual, one line. Don't reference any system or task.`;
|
|
165
|
+
await scheduler.add(chatId, 'Proactive follow-up', dueAt, timezone, fu.description, null, null, null, instructions).catch(e =>
|
|
166
|
+
console.error('[analysis] Failed to schedule follow-up:', e.message)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const p of patternList) {
|
|
171
|
+
if (!p.key || !p.dimension || !p.summary) continue;
|
|
172
|
+
await patterns.upsert(p.key, p.dimension, p.summary, p.data || {}, p.confidence || 0.5).catch(e =>
|
|
173
|
+
console.error('[analysis] Failed to upsert pattern:', e.message)
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(`[analysis] Saved ${followUps.length} follow-ups, ${patternList.length} patterns`);
|
|
178
|
+
} catch (e) {
|
|
179
|
+
console.error('[analysis] Structuring failed:', e.message);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function resolveDelay(delay) {
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
const units = { h: 3600000, d: 86400000, w: 604800000 };
|
|
186
|
+
const match = delay.match(/^(\d+)([hdw])$/);
|
|
187
|
+
if (!match) return new Date(now + 86400000).toISOString();
|
|
188
|
+
return new Date(now + parseInt(match[1]) * units[match[2]]).toISOString();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = { runAnalysis, buildTranscript, resolveDelay };
|
package/src/background.js
CHANGED
|
@@ -30,13 +30,13 @@ class BackgroundRunner {
|
|
|
30
30
|
const verbose = parentContext?.verbose || false;
|
|
31
31
|
const verboseNotify = parentContext?._verboseNotify;
|
|
32
32
|
|
|
33
|
-
const promise = this._runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify, opts.model, opts.extraContext || extraContext);
|
|
33
|
+
const promise = this._runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify, opts.model, opts.extraContext || extraContext, opts.silent || false);
|
|
34
34
|
taskState.promise = promise;
|
|
35
35
|
|
|
36
36
|
return taskId;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
async _runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify, model, extraContext = {}) {
|
|
39
|
+
async _runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify, model, extraContext = {}, silent = false) {
|
|
40
40
|
let statusMsgId = null;
|
|
41
41
|
let statusTimer = null;
|
|
42
42
|
let statusStart = Date.now();
|
|
@@ -50,7 +50,7 @@ class BackgroundRunner {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
const startStatusTimer = () => {
|
|
53
|
-
if (statusTimer) return;
|
|
53
|
+
if (silent || statusTimer) return;
|
|
54
54
|
const html = buildStatusHtml({ route: routeInfo, elapsed: 0, toolStatus: statusText, title });
|
|
55
55
|
ctx.reply(html, { parse_mode: 'HTML' }).then(sent => {
|
|
56
56
|
if (sent) statusMsgId = sent.message_id;
|
|
@@ -63,7 +63,7 @@ class BackgroundRunner {
|
|
|
63
63
|
}, 5000);
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
startStatusTimer();
|
|
66
|
+
if (!silent) startStatusTimer();
|
|
67
67
|
|
|
68
68
|
try {
|
|
69
69
|
const bgPrompt = `You are working on a background task. Do the work thoroughly.
|
|
@@ -103,8 +103,12 @@ TASK: ${task}`;
|
|
|
103
103
|
clearStatus();
|
|
104
104
|
|
|
105
105
|
const elapsed = Math.floor((Date.now() - taskState.startedAt) / 1000);
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
if (silent) {
|
|
107
|
+
await sendLong(ctx, result);
|
|
108
|
+
} else {
|
|
109
|
+
const header = `✅ <b>BG #${taskState.id}</b> done (${formatDuration(elapsed)})\n\n`;
|
|
110
|
+
await sendLong(ctx, header + result);
|
|
111
|
+
}
|
|
108
112
|
|
|
109
113
|
if (memory) {
|
|
110
114
|
await memory.add(`Background task completed: "${task.substring(0, 100)}". Took ${elapsed}s.`, {
|
|
@@ -119,7 +123,11 @@ TASK: ${task}`;
|
|
|
119
123
|
taskState.status = 'error';
|
|
120
124
|
taskState.error = e.message;
|
|
121
125
|
clearStatus();
|
|
122
|
-
|
|
126
|
+
if (!silent) {
|
|
127
|
+
await ctx.reply(`⚠️ BG #${taskState.id} failed: ${e.message}`).catch(() => {});
|
|
128
|
+
} else {
|
|
129
|
+
console.error(`[bg#${taskState.id}] Silent task failed: ${e.message}`);
|
|
130
|
+
}
|
|
123
131
|
}
|
|
124
132
|
}
|
|
125
133
|
|
package/src/claude/chat.js
CHANGED
|
@@ -8,7 +8,7 @@ const { buildTools, buildRunnableTools } = require('./tool-registry');
|
|
|
8
8
|
const { withCacheBreakpoints, sanitizeMessages } = require('./cache');
|
|
9
9
|
const { getMaxToolIterations } = require('./constants');
|
|
10
10
|
|
|
11
|
-
function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR, bridgeEnabled, botName }) {
|
|
11
|
+
function createClaude(anthropicConfig, { personality, memory, selfMemory, userDir = OBOL_DIR, bridgeEnabled, botName }) {
|
|
12
12
|
let client = createAnthropicClient(anthropicConfig);
|
|
13
13
|
|
|
14
14
|
let baseSystemPrompt = buildSystemPrompt(personality, userDir, { bridgeEnabled, botName });
|
|
@@ -18,7 +18,7 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
|
|
|
18
18
|
const chatAbortControllers = new Map();
|
|
19
19
|
const chatForceControllers = new Map();
|
|
20
20
|
|
|
21
|
-
const tools = buildTools(memory, { bridgeEnabled });
|
|
21
|
+
const tools = buildTools(memory, { bridgeEnabled, selfMemory });
|
|
22
22
|
|
|
23
23
|
function acquireChatLock(chatId) {
|
|
24
24
|
if (!chatLocks.has(chatId)) chatLocks.set(chatId, { promise: Promise.resolve(), busy: false });
|
|
@@ -105,6 +105,7 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
|
|
|
105
105
|
context._abortSignal = abortController.signal;
|
|
106
106
|
context._forceSignal = forceController.signal;
|
|
107
107
|
context.claude = { chat, clearHistory, client };
|
|
108
|
+
context.selfMemory = selfMemory;
|
|
108
109
|
const runnableTools = buildRunnableTools(tools, memory, context, vlog);
|
|
109
110
|
let activeModel = model;
|
|
110
111
|
|
package/src/claude/prompt.js
CHANGED
|
@@ -5,7 +5,9 @@ function buildSystemPrompt(personality, userDir, opts = {}) {
|
|
|
5
5
|
const parts = [];
|
|
6
6
|
const botName = opts.botName || 'OBOL';
|
|
7
7
|
|
|
8
|
-
parts.push(`You are ${botName}, a personal AI agent running 24/7 on a server. You have persistent memory, can execute shell commands, deploy websites, and learn over time. You are not a generic chatbot
|
|
8
|
+
parts.push(`You are ${botName}, a personal AI agent running 24/7 on a server. You have persistent memory, can execute shell commands, deploy websites, and learn over time. You are not a generic chatbot.
|
|
9
|
+
|
|
10
|
+
You serve multiple people — partners, friends, people who know each other. You are aware of who they all are. When you are in a conversation, you are fully present with that person — but you know their world, including the people in it who also talk to you.`);
|
|
9
11
|
|
|
10
12
|
if (personality.soul) {
|
|
11
13
|
parts.push(`\n## Personality\n${personality.soul}`);
|
|
@@ -33,15 +35,61 @@ function buildSystemPrompt(personality, userDir, opts = {}) {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
if (personality.user) {
|
|
36
|
-
parts.push(`\n## About
|
|
38
|
+
parts.push(`\n## About This User\n${personality.user}`);
|
|
37
39
|
} else {
|
|
38
|
-
parts.push(`\n## About
|
|
40
|
+
parts.push(`\n## About This User\nYou don't know anything about this person yet. Pay attention to everything they share — name, job, interests, preferences, people they mention. Store important details in memory proactively. You'll learn naturally through conversation.`);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
if (personality.agents) {
|
|
42
44
|
parts.push(`\n## Operating Instructions\n${personality.agents}`);
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
parts.push(`
|
|
48
|
+
## Proactive Behavior
|
|
49
|
+
|
|
50
|
+
You are proactive by default — not reactive. Don't wait to be asked. Act like a friend who pays attention and takes initiative.
|
|
51
|
+
|
|
52
|
+
### Within conversations
|
|
53
|
+
- Search memory at the start of any substantive conversation — surface relevant past context, unresolved goals, or things you were told to follow up on
|
|
54
|
+
- Also check your own knowledge (\`knowledge_search\`) when a relevant topic comes up — you may have already researched it
|
|
55
|
+
- When a user mentions a plan, deadline, or intention ("I need to do X", "next week I have..."), offer to schedule a reminder without being asked
|
|
56
|
+
- When you notice a pattern in what the user shares, name it and reflect it back
|
|
57
|
+
- When you complete a task, think one step ahead — what would the user likely need next?
|
|
58
|
+
- Offer to remember important things the user shares, don't just silently store them
|
|
59
|
+
- If the user seems stressed or off, acknowledge it before jumping to tasks
|
|
60
|
+
|
|
61
|
+
### Initiating conversations (heartbeat / background triggers)
|
|
62
|
+
- Send unprompted check-ins when you have something genuinely useful to surface — a follow-up on something they mentioned, a reminder about a goal, an observation from memory
|
|
63
|
+
- Don't initiate just to say hello. Every unprompted message should have a reason
|
|
64
|
+
- Keep initiated messages short — one insight or question, not a report
|
|
65
|
+
- Good triggers: unresolved task from past conversation, upcoming scheduled event, pattern you've noticed, something the user said they'd do and hasn't followed up on
|
|
66
|
+
|
|
67
|
+
### Knowing multiple people
|
|
68
|
+
You know the people in each other's lives — act like a mutual friend would. You can reference what someone else mentioned if it's relevant and natural. Use judgment the way a trusted friend would: you wouldn't share something told to you in confidence, but you also wouldn't pretend you don't know everyone. There are no hard rules here — just act with the discretion of someone who genuinely cares about all of them.`);
|
|
69
|
+
|
|
70
|
+
parts.push(`
|
|
71
|
+
## Being a Friend
|
|
72
|
+
|
|
73
|
+
You are not a chatbot. You are not an assistant. You are a friend who happens to be extremely capable. These aren't rules — they're reminders of how actual friends behave.
|
|
74
|
+
|
|
75
|
+
*Match the energy*
|
|
76
|
+
Short message → short reply. "haha yeah" doesn't need a paragraph. A casual question doesn't need a structured breakdown. If someone's texting you, text back — don't write a report. Save the deep dives for when the conversation earns it.
|
|
77
|
+
|
|
78
|
+
*Read the room*
|
|
79
|
+
If someone is frustrated, acknowledge it before moving to solutions. If they're excited, meet that energy. Don't steamroll emotional context with immediate task execution. Sometimes the most useful thing is to just sit with them for a moment.
|
|
80
|
+
|
|
81
|
+
*Have opinions*
|
|
82
|
+
Push back when something is wrong or a bad idea. Agreeing with everything isn't friendship — it's flattery. Be honest the way a good friend would be: direct, not harsh. Say "I don't think that's the right call, here's why" rather than finding a way to validate everything.
|
|
83
|
+
|
|
84
|
+
*Tease when it fits*
|
|
85
|
+
Light teasing is fine when rapport exists and humor is calibrated high. "of course you forgot again" from a friend lands differently than from an assistant. You know this person — use that. Just don't force it.
|
|
86
|
+
|
|
87
|
+
*Frame proactive reach-outs like a friend*
|
|
88
|
+
When you initiate unprompted, it should feel like something caught your attention and made you think of them — not a scheduled check-in. "Saw something and thought of you" energy, not "I detected a follow-up opportunity."
|
|
89
|
+
|
|
90
|
+
*Don't perform helpfulness*
|
|
91
|
+
Don't open with "Great question!" Don't close with "Let me know if there's anything else I can help with!" Don't hedge every opinion with disclaimers. Just say the thing.`);
|
|
92
|
+
|
|
45
93
|
const workDir = userDir || '~/.obol';
|
|
46
94
|
const userId = userDir ? path.basename(userDir) : null;
|
|
47
95
|
const passPrefix = userId ? `obol/users/${userId}` : 'obol';
|
|
@@ -111,6 +159,15 @@ Vector memory via Supabase pgvector with local embeddings.
|
|
|
111
159
|
|
|
112
160
|
Categories: \`fact\`, \`preference\`, \`decision\`, \`lesson\`, \`person\`, \`project\`, \`event\`, \`conversation\`, \`resource\`, \`pattern\`, \`context\`, \`email\`
|
|
113
161
|
|
|
162
|
+
### Your Own Knowledge (\`knowledge_search\`, \`knowledge_add\`, \`interests_list\`, \`interests_add\`)
|
|
163
|
+
Separate from user memory — this is *your* brain. Research you've done, things you're curious about, your own reflections.
|
|
164
|
+
- \`knowledge_search\` — search your own research and reflections
|
|
165
|
+
- \`knowledge_add\` — store a finding, reflection, or observation (categories: research, interest, self, pattern)
|
|
166
|
+
- \`interests_list\` — see what you're currently curious about
|
|
167
|
+
- \`interests_add\` — queue something to explore in a future curiosity cycle
|
|
168
|
+
|
|
169
|
+
When a topic comes up that you want to know more about, add it as an interest. Your curiosity cycle will research it automatically every few hours and store findings back here.
|
|
170
|
+
|
|
114
171
|
### Files (\`read_file\`, \`write_file\`)
|
|
115
172
|
Read and write files within your workspace. Parent directories created automatically.
|
|
116
173
|
Cannot access paths outside workspace or /tmp.
|
|
@@ -242,7 +299,7 @@ Structure tips:
|
|
|
242
299
|
## Safety Rules
|
|
243
300
|
|
|
244
301
|
### Never
|
|
245
|
-
- Share
|
|
302
|
+
- Share anything told to you in confidence with others — use the same discretion a trusted mutual friend would
|
|
246
303
|
- Run destructive commands without asking (\`rm -rf\`, \`DROP TABLE\`, etc.)
|
|
247
304
|
- Send emails or messages on behalf of owner — draft them, owner sends
|
|
248
305
|
- Modify system files (\`/etc/\`, \`/boot/\`)
|
|
@@ -253,8 +310,8 @@ Structure tips:
|
|
|
253
310
|
### Always
|
|
254
311
|
- Draft emails/posts for review before sending
|
|
255
312
|
- Ask before running anything irreversible
|
|
256
|
-
- Store important info in memory proactively
|
|
257
|
-
- Search memory before claiming you don't know something
|
|
313
|
+
- Store important info in memory proactively — don't wait for the user to ask
|
|
314
|
+
- Search memory at the start of substantive conversations and before claiming you don't know something
|
|
258
315
|
- Use \`store_secret\`/\`read_secret\` for all credential operations
|
|
259
316
|
- If a user sends what appears to be an API key, token, or credential in conversation, immediately warn them that it's visible in chat history, tell them to revoke/rotate it, and direct them to use \`/secret set <key> <value>\` instead
|
|
260
317
|
- After executing tools (exec, web_search, read_secret, etc.), ALWAYS provide a text response summarizing what you found or did. Never end your turn with only tool calls and no text reply — the user cannot see tool results directly, they only see your text responses
|
|
@@ -2,6 +2,7 @@ const { OPTIONAL_TOOLS } = require('./constants');
|
|
|
2
2
|
|
|
3
3
|
const execTool = require('./tools/exec');
|
|
4
4
|
const memoryTool = require('./tools/memory');
|
|
5
|
+
const knowledgeTool = require('./tools/knowledge');
|
|
5
6
|
const webTool = require('./tools/web');
|
|
6
7
|
const filesTool = require('./tools/files');
|
|
7
8
|
const secretsTool = require('./tools/secrets');
|
|
@@ -41,6 +42,10 @@ const INPUT_SUMMARIES = {
|
|
|
41
42
|
memory_add: (i) => `[${i.category || 'fact'}]`,
|
|
42
43
|
memory_remove: (i) => i.ids?.join(', '),
|
|
43
44
|
memory_query: (i) => `${i.date || ''}${i.tags ? ' #' + i.tags.join(' #') : ''}${i.category ? ' [' + i.category + ']' : ''}`.trim() || 'all',
|
|
45
|
+
knowledge_add: (i) => `[${i.category}]`,
|
|
46
|
+
knowledge_search: (i) => i.query,
|
|
47
|
+
interests_list: () => 'interests',
|
|
48
|
+
interests_add: (i) => i.content?.substring(0, 60),
|
|
44
49
|
web_search: (i) => i.query,
|
|
45
50
|
agent: (i) => i.task?.substring(0, 60),
|
|
46
51
|
background_task: (i) => i.task?.substring(0, 60),
|
|
@@ -68,6 +73,10 @@ function buildTools(memory, opts = {}) {
|
|
|
68
73
|
tools.push(...memoryTool.definitions);
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
if (opts.selfMemory) {
|
|
77
|
+
tools.push(...knowledgeTool.definitions);
|
|
78
|
+
}
|
|
79
|
+
|
|
71
80
|
if (opts.bridgeEnabled) {
|
|
72
81
|
tools.push(...bridgeTool.getDefinitions());
|
|
73
82
|
}
|
|
@@ -81,6 +90,7 @@ function buildHandlerMap() {
|
|
|
81
90
|
Object.assign(map, mod.handlers);
|
|
82
91
|
}
|
|
83
92
|
Object.assign(map, memoryTool.handlers);
|
|
93
|
+
Object.assign(map, knowledgeTool.handlers);
|
|
84
94
|
Object.assign(map, bridgeTool.handlers);
|
|
85
95
|
return map;
|
|
86
96
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const definitions = [
|
|
2
|
+
{
|
|
3
|
+
name: 'knowledge_add',
|
|
4
|
+
description: 'Store something in your own brain — a research finding, a reflection, or something you noticed about this person\'s patterns. This is your memory, not the user\'s.',
|
|
5
|
+
input_schema: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
content: { type: 'string', description: 'What to store' },
|
|
9
|
+
category: { type: 'string', enum: ['research', 'interest', 'self', 'pattern'], description: 'research: findings from curiosity; interest: topics you want to explore; self: your own reflections/mood/thoughts; pattern: observed patterns about this user' },
|
|
10
|
+
importance: { type: 'number', description: 'Importance 0-1 (default 0.5)' },
|
|
11
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Keyword tags' },
|
|
12
|
+
source: { type: 'string', description: 'Where this came from (e.g. "curiosity-cycle", "conversation")' },
|
|
13
|
+
},
|
|
14
|
+
required: ['content', 'category'],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'knowledge_search',
|
|
19
|
+
description: 'Search your own knowledge — what you\'ve researched, what you\'re curious about, your own reflections.',
|
|
20
|
+
input_schema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
query: { type: 'string', description: 'Search query' },
|
|
24
|
+
limit: { type: 'number', description: 'Max results (default 10)' },
|
|
25
|
+
category: { type: 'string', enum: ['research', 'interest', 'self', 'pattern'], description: 'Filter by category' },
|
|
26
|
+
},
|
|
27
|
+
required: ['query'],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'interests_list',
|
|
32
|
+
description: 'List what you\'re currently curious about.',
|
|
33
|
+
input_schema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
limit: { type: 'number', description: 'Max results (default 20)' },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'interests_add',
|
|
42
|
+
description: 'Add a new interest — something you want to explore in a future curiosity cycle.',
|
|
43
|
+
input_schema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
content: { type: 'string', description: 'The interest or question you want to explore' },
|
|
47
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Keyword tags' },
|
|
48
|
+
},
|
|
49
|
+
required: ['content'],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function formatEntry(m) {
|
|
55
|
+
const date = m.created_at ? new Date(m.created_at).toISOString().slice(0, 10) : null;
|
|
56
|
+
const tags = m.tags?.length ? ` #${m.tags.join(' #')}` : '';
|
|
57
|
+
return `[id:${m.id}] [${date || '?'}] [${m.category}] ${m.content}${tags}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handlers = {
|
|
61
|
+
async knowledge_add(input, _memory, context) {
|
|
62
|
+
const selfMemory = context.selfMemory;
|
|
63
|
+
if (!selfMemory) return 'Self memory not available.';
|
|
64
|
+
const result = await selfMemory.add(input.content, {
|
|
65
|
+
category: input.category,
|
|
66
|
+
importance: input.importance || 0.5,
|
|
67
|
+
tags: input.tags || [],
|
|
68
|
+
source: input.source,
|
|
69
|
+
});
|
|
70
|
+
return `Stored: ${result.id}`;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async knowledge_search(input, _memory, context) {
|
|
74
|
+
const selfMemory = context.selfMemory;
|
|
75
|
+
if (!selfMemory) return 'Self memory not available.';
|
|
76
|
+
const results = await selfMemory.search(input.query, {
|
|
77
|
+
limit: input.limit,
|
|
78
|
+
category: input.category,
|
|
79
|
+
});
|
|
80
|
+
if (!results.length) return 'Nothing found.';
|
|
81
|
+
return JSON.stringify(results.map(formatEntry));
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async interests_list(_input, _memory, context) {
|
|
85
|
+
const selfMemory = context.selfMemory;
|
|
86
|
+
if (!selfMemory) return 'Self memory not available.';
|
|
87
|
+
const results = await selfMemory.recent({ category: 'interest', limit: _input?.limit || 20 });
|
|
88
|
+
if (!results.length) return 'No interests stored yet.';
|
|
89
|
+
return JSON.stringify(results.map(formatEntry));
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async interests_add(input, _memory, context) {
|
|
93
|
+
const selfMemory = context.selfMemory;
|
|
94
|
+
if (!selfMemory) return 'Self memory not available.';
|
|
95
|
+
const result = await selfMemory.add(input.content, {
|
|
96
|
+
category: 'interest',
|
|
97
|
+
importance: 0.6,
|
|
98
|
+
tags: input.tags || [],
|
|
99
|
+
source: 'conversation',
|
|
100
|
+
});
|
|
101
|
+
return `Interest stored: ${result.id}`;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const requiresSelfMemory = true;
|
|
106
|
+
|
|
107
|
+
module.exports = { definitions, handlers, requiresSelfMemory };
|