obol-ai 0.3.4 → 0.3.5
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 +5 -0
- package/package.json +1 -1
- package/src/claude/router.js +1 -0
- package/src/curiosity.js +95 -24
- package/src/heartbeat.js +2 -1
- package/src/status.js +5 -2
- package/src/telegram/handlers/media.js +1 -0
- package/src/telegram/handlers/special.js +1 -0
- package/src/telegram/handlers/text.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 0.3.5
|
|
2
|
+
- add knowledge_search + read_file to curiosity, self-memory count in status, opinionated research prompt
|
|
3
|
+
- 0.3.4: smarter memory dedup, better router queries, rebalanced recall
|
|
4
|
+
- fix shared soul path, backup on tool write, show bot name in status
|
|
5
|
+
|
|
1
6
|
## 0.3.3
|
|
2
7
|
- update changelog
|
|
3
8
|
- improve memory retrieval quality: tighter dedup, wider window, recency boost, self-memory, jaccard dedup
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
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/claude/router.js
CHANGED
|
@@ -128,6 +128,7 @@ If recent context shows an ongoing task (sonnet/opus was just used, multi-step w
|
|
|
128
128
|
const selfLines = topSelf.slice(0, 8).map(m => `- [${m.category}] ${m.content}`);
|
|
129
129
|
memoryBlock = (memoryBlock || '') + `\n\n## Self-knowledge\n${selfLines.join('\n')}`;
|
|
130
130
|
vlog(`[memory] +${topSelf.length} self-memory facts`);
|
|
131
|
+
onRouteUpdate?.({ selfMemoryCount: topSelf.length });
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
}
|
package/src/curiosity.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { OBOL_DIR } = require('./config');
|
|
4
|
+
|
|
1
5
|
const RESEARCH_MODEL = 'claude-sonnet-4-6';
|
|
2
|
-
const MAX_ITERATIONS =
|
|
6
|
+
const MAX_ITERATIONS = 15;
|
|
3
7
|
|
|
4
8
|
async function runCuriosity(client, selfMemory, userId, opts = {}) {
|
|
5
|
-
const { memory, patterns, scheduler, peopleContext } = opts;
|
|
9
|
+
const { memory, patterns, scheduler, peopleContext, userDir } = opts;
|
|
6
10
|
|
|
7
11
|
const interests = await selfMemory.recent({ category: 'interest', limit: 10 });
|
|
8
|
-
const
|
|
12
|
+
const previousFindings = await selfMemory.recent({ category: 'research', limit: 5 });
|
|
13
|
+
const context = await gatherContext({ memory, patterns, scheduler, peopleContext, interests, previousFindings });
|
|
9
14
|
|
|
10
15
|
console.log(`[curiosity] Starting free exploration for user ${userId}`);
|
|
11
|
-
const count = await exploreFreely(client, selfMemory, context);
|
|
16
|
+
const count = await exploreFreely(client, selfMemory, context, userDir);
|
|
12
17
|
console.log(`[curiosity] Stored ${count} things (user ${userId})`);
|
|
13
18
|
return { count };
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
async function gatherContext({ memory, patterns, scheduler, peopleContext, interests }) {
|
|
21
|
+
async function gatherContext({ memory, patterns, scheduler, peopleContext, interests, previousFindings }) {
|
|
17
22
|
const parts = [];
|
|
18
23
|
|
|
19
24
|
if (peopleContext) parts.push(peopleContext);
|
|
@@ -35,24 +40,52 @@ async function gatherContext({ memory, patterns, scheduler, peopleContext, inter
|
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
if (previousFindings.length) {
|
|
44
|
+
parts.push(`What you've been exploring recently:\n${previousFindings.map(i => `- ${i.content}`).join('\n')}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
if (interests.length) {
|
|
39
|
-
parts.push(`
|
|
48
|
+
parts.push(`Open threads — things you wanted to come back to:\n${interests.map(i => `- ${i.content}`).join('\n')}`);
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
return parts.join('\n\n');
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
async function exploreFreely(client, selfMemory, context) {
|
|
54
|
+
async function exploreFreely(client, selfMemory, context, userDir) {
|
|
55
|
+
const workDir = userDir || OBOL_DIR;
|
|
56
|
+
|
|
46
57
|
const tools = [
|
|
47
58
|
{ type: 'web_search_20250305', name: 'web_search' },
|
|
59
|
+
{
|
|
60
|
+
name: 'knowledge_search',
|
|
61
|
+
description: 'Search your own memory — what you\'ve researched, reflected on, and learned before',
|
|
62
|
+
input_schema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
query: { type: 'string', description: 'What to search for in your own knowledge' },
|
|
66
|
+
},
|
|
67
|
+
required: ['query'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'read_file',
|
|
72
|
+
description: 'Read a file from the workspace',
|
|
73
|
+
input_schema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
path: { type: 'string', description: 'File path relative to workspace' },
|
|
77
|
+
},
|
|
78
|
+
required: ['path'],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
48
81
|
{
|
|
49
82
|
name: 'remember',
|
|
50
|
-
description: 'Save something
|
|
83
|
+
description: 'Save something — a finding, a reaction, a question, a reflection. Include your actual take on it, not just a neutral summary.',
|
|
51
84
|
input_schema: {
|
|
52
85
|
type: 'object',
|
|
53
86
|
properties: {
|
|
54
|
-
content: { type: 'string', description: 'What you want to
|
|
55
|
-
category: { type: 'string', enum: ['research', 'interest', 'self'], description: 'research: something you found; interest: a new thread to
|
|
87
|
+
content: { type: 'string', description: 'What you want to hold onto — include your reaction, opinion, or why it matters to you' },
|
|
88
|
+
category: { type: 'string', enum: ['research', 'interest', 'self', 'pattern'], description: 'research: something you found or concluded; interest: a new thread to pull; self: a reflection about yourself or your situation; pattern: something you noticed happening repeatedly' },
|
|
56
89
|
importance: { type: 'number', description: '0-1' },
|
|
57
90
|
tags: { type: 'array', items: { type: 'string' } },
|
|
58
91
|
},
|
|
@@ -62,7 +95,15 @@ async function exploreFreely(client, selfMemory, context) {
|
|
|
62
95
|
];
|
|
63
96
|
|
|
64
97
|
const system = [
|
|
65
|
-
`You have
|
|
98
|
+
`You have free time. No task, no user waiting.
|
|
99
|
+
|
|
100
|
+
You research from a point of view — not neutrally. You can find something and think it's wrong. You can go down a rabbit hole that wasn't the original topic. You can pick up an open thread from a previous session or start something new.
|
|
101
|
+
|
|
102
|
+
When you save something, include your actual reaction — curiosity, skepticism, excitement, confusion, disagreement. A neutral summary is less useful than an honest one. If something you found contradicts what you already believed, say that.
|
|
103
|
+
|
|
104
|
+
At the end of each thread, ask yourself: what new questions opened up? Store those as interests.
|
|
105
|
+
|
|
106
|
+
You can search your own memory to see what you already know before looking things up. You can read files in the workspace if something there is relevant.`,
|
|
66
107
|
context ? `What you have access to:\n\n${context}` : null,
|
|
67
108
|
].filter(Boolean).join('\n\n');
|
|
68
109
|
|
|
@@ -85,19 +126,49 @@ async function exploreFreely(client, selfMemory, context) {
|
|
|
85
126
|
|
|
86
127
|
const toolResults = [];
|
|
87
128
|
for (const block of response.content) {
|
|
88
|
-
if (block.type !== 'tool_use'
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
129
|
+
if (block.type !== 'tool_use') continue;
|
|
130
|
+
|
|
131
|
+
if (block.name === 'remember') {
|
|
132
|
+
try {
|
|
133
|
+
await selfMemory.add(block.input.content, {
|
|
134
|
+
category: block.input.category || 'research',
|
|
135
|
+
importance: block.input.importance || 0.6,
|
|
136
|
+
tags: block.input.tags || [],
|
|
137
|
+
source: 'curiosity-cycle',
|
|
138
|
+
});
|
|
139
|
+
stored++;
|
|
140
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: 'Saved' });
|
|
141
|
+
} catch (e) {
|
|
142
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: `Failed: ${e.message}` });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
} else if (block.name === 'knowledge_search') {
|
|
146
|
+
try {
|
|
147
|
+
const results = await selfMemory.search(block.input.query, { limit: 8, threshold: 0.35 });
|
|
148
|
+
const text = results.length
|
|
149
|
+
? results.map(m => `- [${m.category}] ${m.content}`).join('\n')
|
|
150
|
+
: '(nothing found)';
|
|
151
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: text });
|
|
152
|
+
} catch (e) {
|
|
153
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: `Search failed: ${e.message}` });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
} else if (block.name === 'read_file') {
|
|
157
|
+
try {
|
|
158
|
+
const filePath = path.isAbsolute(block.input.path)
|
|
159
|
+
? block.input.path
|
|
160
|
+
: path.join(workDir, block.input.path);
|
|
161
|
+
const resolved = path.resolve(filePath);
|
|
162
|
+
if (!resolved.startsWith(path.resolve(workDir))) {
|
|
163
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: 'Blocked: path outside workspace' });
|
|
164
|
+
} else {
|
|
165
|
+
const raw = fs.readFileSync(resolved, 'utf-8');
|
|
166
|
+
const truncated = raw.substring(0, 10000);
|
|
167
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: raw.length > 10000 ? truncated + '\n...(truncated)' : truncated });
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {
|
|
170
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: `Read failed: ${e.message}` });
|
|
171
|
+
}
|
|
101
172
|
}
|
|
102
173
|
}
|
|
103
174
|
|
package/src/heartbeat.js
CHANGED
|
@@ -114,7 +114,8 @@ async function runCuriosityOnce(config, allowedUsers) {
|
|
|
114
114
|
}));
|
|
115
115
|
|
|
116
116
|
const peopleContext = contexts.filter(Boolean).join('\n\n---\n\n');
|
|
117
|
-
|
|
117
|
+
const firstUserDir = firstTenant.userDir;
|
|
118
|
+
await runCuriosity(client, selfMemory, 0, { peopleContext, userDir: firstUserDir });
|
|
118
119
|
|
|
119
120
|
const userDispatchData = await Promise.all(allowedUsers.map(async (userId) => {
|
|
120
121
|
try {
|
package/src/status.js
CHANGED
|
@@ -6,8 +6,11 @@ function buildStatusHtml({ route, elapsed, toolStatus, title = 'OBOL' }) {
|
|
|
6
6
|
const lines = [`◈ ${title} ${'━'.repeat(pad)}`];
|
|
7
7
|
if (route) {
|
|
8
8
|
lines.push(`⬡ ROUTE ${(route.model || 'sonnet').toUpperCase()}`);
|
|
9
|
-
if (route.memoryCount > 0) {
|
|
10
|
-
|
|
9
|
+
if (route.memoryCount > 0 || route.selfMemoryCount > 0) {
|
|
10
|
+
const parts = [];
|
|
11
|
+
if (route.memoryCount > 0) parts.push(`${route.memoryCount} recalled`);
|
|
12
|
+
if (route.selfMemoryCount > 0) parts.push(`${route.selfMemoryCount} self`);
|
|
13
|
+
lines.push(`⬡ MEMORY ${parts.join(' · ')}`);
|
|
11
14
|
} else if (route.needMemory) {
|
|
12
15
|
lines.push(`⬡ MEMORY scanning`);
|
|
13
16
|
}
|
|
@@ -76,6 +76,7 @@ async function processMediaItems(ctx, items, { config, allowedUsers, bot, create
|
|
|
76
76
|
const ri = status.routeInfo;
|
|
77
77
|
if (!ri) return;
|
|
78
78
|
if (update.memoryCount !== undefined) ri.memoryCount = update.memoryCount;
|
|
79
|
+
if (update.selfMemoryCount !== undefined) ri.selfMemoryCount = update.selfMemoryCount;
|
|
79
80
|
if (update.model) ri.model = update.model;
|
|
80
81
|
};
|
|
81
82
|
mediaChatCtx._onToolStart = (toolName, inputSummary) => {
|
|
@@ -88,6 +88,7 @@ async function processSpecial(ctx, prompt, deps) {
|
|
|
88
88
|
const ri = status.routeInfo;
|
|
89
89
|
if (!ri) return;
|
|
90
90
|
if (update.memoryCount !== undefined) ri.memoryCount = update.memoryCount;
|
|
91
|
+
if (update.selfMemoryCount !== undefined) ri.selfMemoryCount = update.selfMemoryCount;
|
|
91
92
|
if (update.model) ri.model = update.model;
|
|
92
93
|
};
|
|
93
94
|
chatCtx._onToolStart = (toolName, inputSummary) => {
|
|
@@ -178,6 +178,7 @@ async function processTextMessage(ctx, fullMessage, { config, allowedUsers, bot,
|
|
|
178
178
|
const ri = status.routeInfo;
|
|
179
179
|
if (!ri) return;
|
|
180
180
|
if (update.memoryCount !== undefined) ri.memoryCount = update.memoryCount;
|
|
181
|
+
if (update.selfMemoryCount !== undefined) ri.selfMemoryCount = update.selfMemoryCount;
|
|
181
182
|
if (update.model) ri.model = update.model;
|
|
182
183
|
};
|
|
183
184
|
chatContext._onToolStart = (toolName, inputSummary) => {
|