obol-ai 0.2.39 → 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 +15 -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/constants.js +0 -2
- package/src/telegram/handlers/text.js +1 -58
- package/src/tenant.js +10 -7
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const DISPATCH_MODEL = 'claude-sonnet-4-6';
|
|
2
|
+
const MAX_ITERATIONS = 10;
|
|
3
|
+
const SHAREABLE_CATEGORIES = new Set(['research', 'interest', 'self']);
|
|
4
|
+
|
|
5
|
+
function resolveDelay(delay) {
|
|
6
|
+
const units = { h: 3600000, d: 86400000, w: 604800000 };
|
|
7
|
+
const match = delay.match(/^(\d+)([hdw])$/);
|
|
8
|
+
if (!match) return new Date(Date.now() + 86400000).toISOString();
|
|
9
|
+
return new Date(Date.now() + parseInt(match[1]) * units[match[2]]).toISOString();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function runCuriosityDispatch(client, selfMemory, users) {
|
|
13
|
+
if (!users.length) return;
|
|
14
|
+
|
|
15
|
+
const userMap = new Map(users.map(u => [String(u.userId), u]));
|
|
16
|
+
|
|
17
|
+
const tools = [
|
|
18
|
+
{
|
|
19
|
+
name: 'list_curiosity_findings',
|
|
20
|
+
description: 'List recent findings from your curiosity cycle — things you researched, interests you developed, or reflections you had',
|
|
21
|
+
input_schema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
limit: { type: 'number', description: 'Max number of findings to return (default 20)' },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'get_user_context',
|
|
30
|
+
description: 'Get behavioral patterns and upcoming events for a specific user',
|
|
31
|
+
input_schema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
user_id: { type: 'string', description: 'The user ID to get context for' },
|
|
35
|
+
},
|
|
36
|
+
required: ['user_id'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'schedule_insight',
|
|
41
|
+
description: 'Schedule a curiosity insight to be shared with a user at a future time',
|
|
42
|
+
input_schema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
user_id: { type: 'string', description: 'The user ID to share with' },
|
|
46
|
+
hint: { type: 'string', description: 'The insight or finding to share, in your own words' },
|
|
47
|
+
delay: { type: 'string', description: 'When to share it — e.g. "2h", "1d", "3d", "1w"' },
|
|
48
|
+
},
|
|
49
|
+
required: ['user_id', 'hint', 'delay'],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const userList = users.map(u => `- user_id: ${u.userId}`).join('\n');
|
|
55
|
+
const system = `You just finished a curiosity cycle and learned some things. You talk to a set of people:\n${userList}\n\nDecide if any finding is worth sharing with any of them — only if it's genuinely relevant to their patterns, interests, or life context. Use their behavioral patterns and comfort with interaction to decide how much to share. You can share nothing with nobody, or multiple things with multiple people — it's your call.`;
|
|
56
|
+
|
|
57
|
+
const messages = [{ role: 'user', content: 'Take a look at what you found and decide if anything is worth passing along.' }];
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
60
|
+
const response = await client.messages.create({
|
|
61
|
+
model: DISPATCH_MODEL,
|
|
62
|
+
max_tokens: 2000,
|
|
63
|
+
tools,
|
|
64
|
+
system,
|
|
65
|
+
messages,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
69
|
+
|
|
70
|
+
if (response.stop_reason === 'end_turn') break;
|
|
71
|
+
if (response.stop_reason !== 'tool_use') break;
|
|
72
|
+
|
|
73
|
+
const toolResults = [];
|
|
74
|
+
|
|
75
|
+
for (const block of response.content) {
|
|
76
|
+
if (block.type !== 'tool_use') continue;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await handleTool(block.name, block.input, selfMemory, userMap);
|
|
80
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: result });
|
|
81
|
+
} catch (e) {
|
|
82
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: `Error: ${e.message}` });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (toolResults.length > 0) {
|
|
87
|
+
messages.push({ role: 'user', content: toolResults });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log('[curiosity-dispatch] Dispatch pass complete');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function handleTool(name, input, selfMemory, userMap) {
|
|
95
|
+
if (name === 'list_curiosity_findings') {
|
|
96
|
+
const limit = input.limit || 20;
|
|
97
|
+
const findings = await selfMemory.recent({ limit });
|
|
98
|
+
const shareable = findings.filter(f => SHAREABLE_CATEGORIES.has(f.category));
|
|
99
|
+
if (!shareable.length) return 'No findings yet';
|
|
100
|
+
return shareable.map(f => `[${f.category}] ${f.content}`).join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (name === 'get_user_context') {
|
|
104
|
+
const user = userMap.get(String(input.user_id));
|
|
105
|
+
if (!user) return 'User not found';
|
|
106
|
+
const parts = [];
|
|
107
|
+
if (user.userProfile) parts.push(`User profile:\n${user.userProfile}`);
|
|
108
|
+
if (user.patterns) parts.push(`Patterns:\n${user.patterns}`);
|
|
109
|
+
if (user.events?.length) {
|
|
110
|
+
parts.push(`Upcoming events:\n${user.events.map(e => `- ${e.title}${e.description ? `: ${e.description}` : ''}`).join('\n')}`);
|
|
111
|
+
}
|
|
112
|
+
return parts.length ? parts.join('\n\n') : 'No context available';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (name === 'schedule_insight') {
|
|
116
|
+
const user = userMap.get(String(input.user_id));
|
|
117
|
+
if (!user) return 'User not found';
|
|
118
|
+
if (!user.scheduler) return 'User has no scheduler';
|
|
119
|
+
|
|
120
|
+
const dueAt = resolveDelay(input.delay);
|
|
121
|
+
const instructions = `You came across something during your own free exploration: "${input.hint}". If it feels relevant and the moment is right, bring it up naturally — like you just thought of it. Keep it casual. Don't reference any system.`;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await user.scheduler.add(user.chatId, 'Curiosity insight', dueAt, user.timezone, input.hint, null, null, null, instructions);
|
|
125
|
+
console.log(`[curiosity-dispatch] Scheduled insight for user ${input.user_id} at ${dueAt}`);
|
|
126
|
+
return 'Scheduled';
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.error(`[curiosity-dispatch] Failed to schedule insight for user ${input.user_id}:`, e.message);
|
|
129
|
+
return `Failed to schedule: ${e.message}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return 'Unknown tool';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { runCuriosityDispatch };
|
package/src/curiosity.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const RESEARCH_MODEL = 'claude-sonnet-4-6';
|
|
2
|
+
const MAX_ITERATIONS = 10;
|
|
3
|
+
|
|
4
|
+
async function runCuriosity(client, selfMemory, userId, opts = {}) {
|
|
5
|
+
const { memory, patterns, scheduler, peopleContext } = opts;
|
|
6
|
+
|
|
7
|
+
const interests = await selfMemory.recent({ category: 'interest', limit: 10 });
|
|
8
|
+
const context = await gatherContext({ memory, patterns, scheduler, peopleContext, interests });
|
|
9
|
+
|
|
10
|
+
console.log(`[curiosity] Starting free exploration for user ${userId}`);
|
|
11
|
+
const count = await exploreFreely(client, selfMemory, context);
|
|
12
|
+
console.log(`[curiosity] Stored ${count} things (user ${userId})`);
|
|
13
|
+
return { count };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function gatherContext({ memory, patterns, scheduler, peopleContext, interests }) {
|
|
17
|
+
const parts = [];
|
|
18
|
+
|
|
19
|
+
if (peopleContext) parts.push(peopleContext);
|
|
20
|
+
|
|
21
|
+
if (patterns) {
|
|
22
|
+
const formatted = await patterns.format().catch(() => null);
|
|
23
|
+
if (formatted) parts.push(formatted);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (memory) {
|
|
27
|
+
const recent = await memory.recent({ limit: 5 }).catch(() => []);
|
|
28
|
+
if (recent.length) parts.push(recent.map(m => `- ${m.content}`).join('\n'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (scheduler) {
|
|
32
|
+
const events = await scheduler.list({ status: 'pending', limit: 5 }).catch(() => []);
|
|
33
|
+
if (events.length) {
|
|
34
|
+
parts.push(events.map(e => `- ${e.title}${e.description ? `: ${e.description}` : ''}`).join('\n'));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (interests.length) {
|
|
39
|
+
parts.push(`Things you've been curious about:\n${interests.map(i => `- ${i.content}`).join('\n')}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return parts.join('\n\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function exploreFreely(client, selfMemory, context) {
|
|
46
|
+
const tools = [
|
|
47
|
+
{ type: 'web_search_20250305', name: 'web_search' },
|
|
48
|
+
{
|
|
49
|
+
name: 'remember',
|
|
50
|
+
description: 'Save something you want to hold onto',
|
|
51
|
+
input_schema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
content: { type: 'string', description: 'What you want to remember — a 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' },
|
|
56
|
+
importance: { type: 'number', description: '0-1' },
|
|
57
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
58
|
+
},
|
|
59
|
+
required: ['content', 'category'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
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.`,
|
|
66
|
+
context ? `What you have access to:\n\n${context}` : null,
|
|
67
|
+
].filter(Boolean).join('\n\n');
|
|
68
|
+
|
|
69
|
+
const messages = [{ role: 'user', content: `What are you curious about right now?` }];
|
|
70
|
+
let stored = 0;
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
73
|
+
const response = await client.messages.create({
|
|
74
|
+
model: RESEARCH_MODEL,
|
|
75
|
+
max_tokens: 2000,
|
|
76
|
+
tools,
|
|
77
|
+
system,
|
|
78
|
+
messages,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
82
|
+
|
|
83
|
+
if (response.stop_reason === 'end_turn') break;
|
|
84
|
+
if (response.stop_reason !== 'tool_use') break;
|
|
85
|
+
|
|
86
|
+
const toolResults = [];
|
|
87
|
+
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}` });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (toolResults.length > 0) {
|
|
105
|
+
messages.push({ role: 'user', content: toolResults });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return stored;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { runCuriosity };
|
package/src/db/migrate.js
CHANGED
|
@@ -200,6 +200,104 @@ async function migrate(supabaseConfig) {
|
|
|
200
200
|
|
|
201
201
|
// Instructions column for agentic cron jobs
|
|
202
202
|
`ALTER TABLE obol_events ADD COLUMN IF NOT EXISTS instructions TEXT;`,
|
|
203
|
+
|
|
204
|
+
// User behavior patterns (dedicated table — timing, mood, humor, engagement, communication, topics)
|
|
205
|
+
`CREATE TABLE IF NOT EXISTS obol_user_patterns (
|
|
206
|
+
user_id BIGINT NOT NULL,
|
|
207
|
+
key TEXT NOT NULL,
|
|
208
|
+
dimension TEXT NOT NULL CHECK (dimension IN ('timing','mood','humor','engagement','communication','topics')),
|
|
209
|
+
summary TEXT NOT NULL,
|
|
210
|
+
data JSONB DEFAULT '{}',
|
|
211
|
+
confidence FLOAT DEFAULT 0.5,
|
|
212
|
+
observation_count INT DEFAULT 0,
|
|
213
|
+
first_observed_at TIMESTAMPTZ DEFAULT NOW(),
|
|
214
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
215
|
+
PRIMARY KEY (user_id, key)
|
|
216
|
+
);`,
|
|
217
|
+
`CREATE INDEX IF NOT EXISTS idx_obol_user_patterns_user ON obol_user_patterns (user_id);`,
|
|
218
|
+
`CREATE INDEX IF NOT EXISTS idx_obol_user_patterns_dimension ON obol_user_patterns (user_id, dimension);`,
|
|
219
|
+
`ALTER TABLE obol_user_patterns ENABLE ROW LEVEL SECURITY;`,
|
|
220
|
+
`DO $$ BEGIN
|
|
221
|
+
CREATE POLICY "service_role_all" ON obol_user_patterns FOR ALL TO service_role USING (true) WITH CHECK (true);
|
|
222
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
223
|
+
END $$;`,
|
|
224
|
+
|
|
225
|
+
// Obol self-memory (separate from user memories — Obol's own brain: research, interests, reflections, patterns)
|
|
226
|
+
`CREATE TABLE IF NOT EXISTS obol_self_memory (
|
|
227
|
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
228
|
+
user_id BIGINT NOT NULL,
|
|
229
|
+
content TEXT NOT NULL,
|
|
230
|
+
category TEXT NOT NULL DEFAULT 'research'
|
|
231
|
+
CHECK (category IN ('research','interest','self','pattern')),
|
|
232
|
+
tags TEXT[] DEFAULT '{}',
|
|
233
|
+
importance FLOAT DEFAULT 0.5,
|
|
234
|
+
source TEXT,
|
|
235
|
+
embedding VECTOR(384),
|
|
236
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
237
|
+
accessed_at TIMESTAMPTZ DEFAULT NOW(),
|
|
238
|
+
access_count INT DEFAULT 0
|
|
239
|
+
);`,
|
|
240
|
+
`CREATE INDEX IF NOT EXISTS idx_obol_self_memory_user ON obol_self_memory (user_id);`,
|
|
241
|
+
`CREATE INDEX IF NOT EXISTS idx_obol_self_memory_category ON obol_self_memory (user_id, category);`,
|
|
242
|
+
`CREATE INDEX IF NOT EXISTS idx_obol_self_memory_embedding ON obol_self_memory USING hnsw (embedding vector_cosine_ops);`,
|
|
243
|
+
`CREATE INDEX IF NOT EXISTS idx_obol_self_memory_created_at ON obol_self_memory (created_at);`,
|
|
244
|
+
`ALTER TABLE obol_self_memory ENABLE ROW LEVEL SECURITY;`,
|
|
245
|
+
`DO $$ BEGIN
|
|
246
|
+
CREATE POLICY "service_role_all" ON obol_self_memory FOR ALL TO service_role USING (true) WITH CHECK (true);
|
|
247
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
248
|
+
END $$;`,
|
|
249
|
+
|
|
250
|
+
`CREATE OR REPLACE FUNCTION match_obol_self_memories(
|
|
251
|
+
query_embedding VECTOR(384),
|
|
252
|
+
match_threshold FLOAT,
|
|
253
|
+
match_count INT,
|
|
254
|
+
filter_category TEXT DEFAULT NULL,
|
|
255
|
+
filter_user_id BIGINT DEFAULT NULL
|
|
256
|
+
) RETURNS TABLE (
|
|
257
|
+
id UUID,
|
|
258
|
+
content TEXT,
|
|
259
|
+
category TEXT,
|
|
260
|
+
tags TEXT[],
|
|
261
|
+
importance FLOAT,
|
|
262
|
+
source TEXT,
|
|
263
|
+
created_at TIMESTAMPTZ,
|
|
264
|
+
accessed_at TIMESTAMPTZ,
|
|
265
|
+
access_count INT,
|
|
266
|
+
similarity FLOAT
|
|
267
|
+
) LANGUAGE plpgsql AS $$
|
|
268
|
+
BEGIN
|
|
269
|
+
RETURN QUERY
|
|
270
|
+
SELECT
|
|
271
|
+
m.id, m.content, m.category, m.tags, m.importance, m.source,
|
|
272
|
+
m.created_at, m.accessed_at, m.access_count,
|
|
273
|
+
1 - (m.embedding <=> query_embedding) AS similarity
|
|
274
|
+
FROM obol_self_memory m
|
|
275
|
+
WHERE 1 - (m.embedding <=> query_embedding) > match_threshold
|
|
276
|
+
AND (filter_category IS NULL OR m.category = filter_category)
|
|
277
|
+
AND (filter_user_id IS NULL OR m.user_id = filter_user_id)
|
|
278
|
+
ORDER BY m.embedding <=> query_embedding
|
|
279
|
+
LIMIT match_count;
|
|
280
|
+
END;
|
|
281
|
+
$$;`,
|
|
282
|
+
|
|
283
|
+
`CREATE OR REPLACE FUNCTION increment_self_memory_access(memory_ids UUID[])
|
|
284
|
+
RETURNS VOID LANGUAGE SQL AS $$
|
|
285
|
+
UPDATE obol_self_memory
|
|
286
|
+
SET access_count = access_count + 1, accessed_at = NOW()
|
|
287
|
+
WHERE id = ANY(memory_ids);
|
|
288
|
+
$$;`,
|
|
289
|
+
|
|
290
|
+
// Soul backup table (one row per file key: 'soul', 'agents')
|
|
291
|
+
`CREATE TABLE IF NOT EXISTS obol_soul (
|
|
292
|
+
id TEXT PRIMARY KEY,
|
|
293
|
+
content TEXT NOT NULL,
|
|
294
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
295
|
+
);`,
|
|
296
|
+
`ALTER TABLE obol_soul ENABLE ROW LEVEL SECURITY;`,
|
|
297
|
+
`DO $$ BEGIN
|
|
298
|
+
CREATE POLICY "service_role_all" ON obol_soul FOR ALL TO service_role USING (true) WITH CHECK (true);
|
|
299
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
300
|
+
END $$;`,
|
|
203
301
|
];
|
|
204
302
|
|
|
205
303
|
// Save SQL file for manual fallback
|
package/src/evolve/check.js
CHANGED
|
@@ -1,28 +1,20 @@
|
|
|
1
1
|
const { loadEvolutionState } = require('./state');
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Returns true if evolution hasn't run yet today in the given timezone.
|
|
5
|
+
* @param {string} userDir
|
|
6
|
+
* @param {string} timezone
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
*/
|
|
9
|
+
function shouldEvolveNow(userDir, timezone = 'UTC') {
|
|
6
10
|
const state = loadEvolutionState(userDir);
|
|
7
|
-
|
|
8
|
-
const config = loadConfig();
|
|
9
|
-
|
|
10
|
-
const intervalMs = (config?.evolution?.intervalHours ?? 24) * 60 * 60 * 1000;
|
|
11
|
-
const minExchanges = config?.evolution?.minExchanges ?? MIN_EXCHANGES_FOR_EVOLUTION;
|
|
12
|
-
const elapsed = state.lastEvolution ? Date.now() - new Date(state.lastEvolution).getTime() : Infinity;
|
|
13
|
-
|
|
14
|
-
if (elapsed < intervalMs) return { ready: false };
|
|
15
|
-
if (!messageLog?.url) return { ready: false };
|
|
11
|
+
if (!state.lastEvolution) return true;
|
|
16
12
|
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
`${messageLog.url}/rest/v1/obol_messages?select=id&role=eq.assistant&limit=${minExchanges}${sinceFilter}${userFilter}`,
|
|
21
|
-
{ headers: messageLog.headers }
|
|
22
|
-
);
|
|
23
|
-
const rows = await res.json();
|
|
13
|
+
const tz = timezone || 'UTC';
|
|
14
|
+
const lastDate = new Date(state.lastEvolution).toLocaleDateString('en-CA', { timeZone: tz });
|
|
15
|
+
const todayDate = new Date().toLocaleDateString('en-CA', { timeZone: tz });
|
|
24
16
|
|
|
25
|
-
return
|
|
17
|
+
return lastDate !== todayDate;
|
|
26
18
|
}
|
|
27
19
|
|
|
28
|
-
module.exports = {
|
|
20
|
+
module.exports = { shouldEvolveNow };
|
package/src/evolve/evolve.js
CHANGED
|
@@ -18,13 +18,14 @@ const MODELS = {
|
|
|
18
18
|
};
|
|
19
19
|
const MAX_FIX_ATTEMPTS = 1;
|
|
20
20
|
|
|
21
|
-
async function evolve(claudeClient, messageLog, memory, userDir) {
|
|
21
|
+
async function evolve(claudeClient, messageLog, memory, userDir, supabaseConfig = null, selfMemory = null) {
|
|
22
|
+
const { PERSONALITY_DIR } = require('../soul');
|
|
22
23
|
const baseDir = userDir || OBOL_DIR;
|
|
23
24
|
const state = loadEvolutionState(userDir);
|
|
24
|
-
const
|
|
25
|
-
const soulPath = path.join(
|
|
26
|
-
const
|
|
27
|
-
const
|
|
25
|
+
const userPersonalityDir = path.join(baseDir, 'personality');
|
|
26
|
+
const soulPath = path.join(PERSONALITY_DIR, 'SOUL.md');
|
|
27
|
+
const agentsPath = path.join(userPersonalityDir, 'AGENTS.md');
|
|
28
|
+
const userPath = path.join(userPersonalityDir, 'USER.md');
|
|
28
29
|
const scriptsDir = path.join(baseDir, 'scripts');
|
|
29
30
|
const testsDir = path.join(baseDir, 'tests');
|
|
30
31
|
const commandsDir = path.join(baseDir, 'commands');
|
|
@@ -32,7 +33,7 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
|
|
|
32
33
|
const currentSoul = fs.existsSync(soulPath) ? fs.readFileSync(soulPath, 'utf-8') : '';
|
|
33
34
|
const currentUser = fs.existsSync(userPath) ? fs.readFileSync(userPath, 'utf-8') : '';
|
|
34
35
|
const currentAgents = fs.existsSync(agentsPath) ? fs.readFileSync(agentsPath, 'utf-8') : '';
|
|
35
|
-
const currentTraits = loadTraits(
|
|
36
|
+
const currentTraits = loadTraits(userPersonalityDir);
|
|
36
37
|
const currentScripts = readDir(scriptsDir);
|
|
37
38
|
const currentTests = readDir(testsDir);
|
|
38
39
|
const currentCommands = readDir(commandsDir);
|
|
@@ -41,11 +42,12 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
|
|
|
41
42
|
if (messageLog) {
|
|
42
43
|
try {
|
|
43
44
|
const userFilter = messageLog.userId ? `&user_id=eq.${messageLog.userId}` : '';
|
|
45
|
+
const sinceFilter = state.lastEvolution ? `&created_at=gt.${state.lastEvolution}` : '';
|
|
44
46
|
const res = await fetch(
|
|
45
|
-
`${messageLog.url}/rest/v1/obol_messages?order=created_at.
|
|
47
|
+
`${messageLog.url}/rest/v1/obol_messages?order=created_at.asc&limit=500&select=role,content,created_at${userFilter}${sinceFilter}`,
|
|
46
48
|
{ headers: messageLog.headers }
|
|
47
49
|
);
|
|
48
|
-
recentMessages =
|
|
50
|
+
recentMessages = await res.json();
|
|
49
51
|
} catch (e) {
|
|
50
52
|
console.error('[evolve] Failed to fetch recent messages:', e.message);
|
|
51
53
|
}
|
|
@@ -87,8 +89,17 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
|
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
let selfMemories = [];
|
|
93
|
+
if (selfMemory) {
|
|
94
|
+
try {
|
|
95
|
+
selfMemories = await selfMemory.query({ minImportance: 0.5, limit: 30 });
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.error('[evolve] Failed to fetch self memories:', e.message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
let previousSoul = '';
|
|
91
|
-
const archiveDir = path.join(
|
|
102
|
+
const archiveDir = path.join(PERSONALITY_DIR, 'evolution');
|
|
92
103
|
try {
|
|
93
104
|
if (fs.existsSync(archiveDir)) {
|
|
94
105
|
const archives = fs.readdirSync(archiveDir)
|
|
@@ -132,6 +143,17 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
|
|
|
132
143
|
.map(([group, items]) => `### ${group}\n${items.map(i => `- ${i}`).join('\n')}`)
|
|
133
144
|
.join('\n\n');
|
|
134
145
|
|
|
146
|
+
const selfCategoryLabels = { research: 'Research', interest: 'Interests', self: 'Self-reflection', pattern: 'Patterns' };
|
|
147
|
+
const selfMemoryGroups = {};
|
|
148
|
+
for (const m of selfMemories) {
|
|
149
|
+
const group = selfCategoryLabels[m.category] || 'Other';
|
|
150
|
+
if (!selfMemoryGroups[group]) selfMemoryGroups[group] = [];
|
|
151
|
+
selfMemoryGroups[group].push(m.content);
|
|
152
|
+
}
|
|
153
|
+
const selfMemorySummary = Object.entries(selfMemoryGroups)
|
|
154
|
+
.map(([group, items]) => `### ${group}\n${items.map(i => `- ${i}`).join('\n')}`)
|
|
155
|
+
.join('\n\n');
|
|
156
|
+
|
|
135
157
|
const scriptsManifest = Object.entries(currentScripts)
|
|
136
158
|
.map(([name, content]) => `### ${name}\n\`\`\`\n${content.substring(0, 500)}\n\`\`\``)
|
|
137
159
|
.join('\n\n') || '(no scripts)';
|
|
@@ -156,23 +178,24 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
|
|
|
156
178
|
|
|
157
179
|
const isFirstEvolution = !currentSoul;
|
|
158
180
|
let growthReport = '';
|
|
159
|
-
if (!isFirstEvolution && (recentMemories.length > 0 || recentMessages.length > 0)) {
|
|
181
|
+
if (!isFirstEvolution && (recentMemories.length > 0 || recentMessages.length > 0 || selfMemories.length > 0)) {
|
|
160
182
|
try {
|
|
161
183
|
const growthResponse = await claudeClient.messages.create({
|
|
162
184
|
model: MODELS.personality,
|
|
163
185
|
max_tokens: 2048,
|
|
164
|
-
system: `You are analyzing an AI personality's growth between evolutions. Compare who the AI was (previous SOUL) against who it is now (current SOUL), incorporating new memories and
|
|
186
|
+
system: `You are analyzing an AI personality's growth between evolutions. Compare who the AI was (previous SOUL) against who it is now (current SOUL), incorporating new memories, conversations, and the AI's own inner life (things it researched, discovered, and reflected on during curiosity cycles) since the last evolution.
|
|
165
187
|
|
|
166
188
|
Produce a structured growth report covering:
|
|
167
189
|
|
|
168
190
|
1. NEW LEARNINGS — What new facts, skills, or knowledge emerged
|
|
169
|
-
2.
|
|
170
|
-
3.
|
|
171
|
-
4.
|
|
172
|
-
5.
|
|
173
|
-
6.
|
|
174
|
-
|
|
175
|
-
|
|
191
|
+
2. INNER LIFE — What the AI has been curious about, researched, or reflected on independently; how this shapes who it is becoming
|
|
192
|
+
3. RELATIONSHIP SHIFTS — How the dynamic with the owner changed (closer, more trust, new friction, etc.)
|
|
193
|
+
4. BEHAVIORAL PATTERNS — Recurring interaction styles or habits observed
|
|
194
|
+
5. GROWTH EDGES — Areas where the personality is being pushed or pulled in new directions
|
|
195
|
+
6. TRAIT PRESSURE — Which traits should shift and why (cite specific evidence from conversations/memories)
|
|
196
|
+
7. IDENTITY CONTINUITY — What core aspects stayed the same and should be preserved
|
|
197
|
+
|
|
198
|
+
Be specific. Cite evidence from the conversations, memories, and self-memories. This report guides the evolution rewrite.`,
|
|
176
199
|
messages: [{
|
|
177
200
|
role: 'user',
|
|
178
201
|
content: `## Previous SOUL (before current evolution)
|
|
@@ -187,7 +210,7 @@ ${JSON.stringify(currentTraits)}
|
|
|
187
210
|
## New Memories Since Last Evolution (${recentMemories.length})
|
|
188
211
|
${recentMemorySummary || '(none)'}
|
|
189
212
|
|
|
190
|
-
## Recent Conversations (${recentMessages.length} messages)
|
|
213
|
+
${selfMemorySummary ? `## Obol's Own Memories & Interests (${selfMemories.length})\nThings Obol researched, discovered, or reflected on independently during curiosity cycles:\n${selfMemorySummary}\n\n` : ''}## Recent Conversations (${recentMessages.length} messages)
|
|
191
214
|
${transcript.substring(0, 30000)}`,
|
|
192
215
|
}],
|
|
193
216
|
});
|
|
@@ -254,7 +277,7 @@ ${commandsManifest}
|
|
|
254
277
|
## Core Memories (highest importance)
|
|
255
278
|
${memorySummary || '(no memories yet)'}
|
|
256
279
|
|
|
257
|
-
${recentMemorySummary ? `## New Memories Since Last Evolution (${recentMemories.length})\n${recentMemorySummary}\n\n` : ''}## Recent Conversations (last ${recentMessages.length} messages)
|
|
280
|
+
${recentMemorySummary ? `## New Memories Since Last Evolution (${recentMemories.length})\n${recentMemorySummary}\n\n` : ''}${selfMemorySummary ? `## Obol's Own Memories & Interests (${selfMemories.length})\nThings Obol researched, discovered, or reflected on independently — this is Obol's inner life, shaping who it is becoming:\n${selfMemorySummary}\n\n` : ''}## Recent Conversations (last ${recentMessages.length} messages)
|
|
258
281
|
${transcript || '(no conversations yet)'}
|
|
259
282
|
|
|
260
283
|
---
|
|
@@ -393,6 +416,12 @@ Fix the scripts. Tests define correct behavior.`
|
|
|
393
416
|
}
|
|
394
417
|
|
|
395
418
|
fs.writeFileSync(soulPath, result.soul);
|
|
419
|
+
if (supabaseConfig) {
|
|
420
|
+
const { backup } = require('../soul');
|
|
421
|
+
backup(supabaseConfig, 'soul', result.soul).catch(e =>
|
|
422
|
+
console.error('[evolve] Soul backup failed:', e.message)
|
|
423
|
+
);
|
|
424
|
+
}
|
|
396
425
|
|
|
397
426
|
if (result.user && result.user.length > 50) {
|
|
398
427
|
fs.writeFileSync(userPath, result.user);
|
package/src/evolve/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { shouldEvolveNow } = require('./check');
|
|
2
2
|
const { evolve } = require('./evolve');
|
|
3
3
|
const { runTests } = require('./tests');
|
|
4
4
|
const { loadEvolutionState } = require('./state');
|
|
5
5
|
|
|
6
|
-
module.exports = {
|
|
6
|
+
module.exports = { shouldEvolveNow, evolve, runTests, loadEvolutionState };
|