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 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.4",
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": {
@@ -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 = 10;
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 context = await gatherContext({ memory, patterns, scheduler, peopleContext, interests });
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(`Things you've been curious about:\n${interests.map(i => `- ${i.content}`).join('\n')}`);
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 you want to hold onto',
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 remembera thought, a fact, an insight, a question' },
55
- category: { type: 'string', enum: ['research', 'interest', 'self'], description: 'research: something you found; interest: a new thread to explore; self: your own reflection' },
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 some free time. Explore whatever's on your mind — look things up, follow tangents, sit with an idea. Save what you want to remember. Note new threads if something sparks more curiosity. There's no task here.`,
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' || block.name !== 'remember') continue;
89
-
90
- try {
91
- await selfMemory.add(block.input.content, {
92
- category: block.input.category || 'research',
93
- importance: block.input.importance || 0.6,
94
- tags: block.input.tags || [],
95
- source: 'curiosity-cycle',
96
- });
97
- stored++;
98
- toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: 'Saved' });
99
- } catch (e) {
100
- toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: `Failed: ${e.message}` });
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
- await runCuriosity(client, selfMemory, 0, { peopleContext });
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
- lines.push(`⬡ MEMORY ${route.memoryCount} recalled`);
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) => {