obol-ai 0.3.0 → 0.3.2
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 +8 -0
- package/package.json +1 -1
- package/src/curiosity-dispatch.js +20 -0
- package/src/curiosity-humor.js +157 -0
- package/src/heartbeat.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 0.3.2
|
|
2
|
+
- update changelog
|
|
3
|
+
- add list_pending_events tool to dispatch and humor passes to prevent duplicates
|
|
4
|
+
|
|
5
|
+
## 0.3.1
|
|
6
|
+
- update changelog
|
|
7
|
+
- add curiosity humor pass with puns, inside jokes, and link support
|
|
8
|
+
|
|
1
9
|
## 0.3.0
|
|
2
10
|
- add tests for memory-self and analysis helpers
|
|
3
11
|
- fix proactivity and memory bugs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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": {
|
|
@@ -36,6 +36,17 @@ async function runCuriosityDispatch(client, selfMemory, users) {
|
|
|
36
36
|
required: ['user_id'],
|
|
37
37
|
},
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
name: 'list_pending_events',
|
|
41
|
+
description: 'List already-scheduled pending events for a user — check this before scheduling to avoid duplicates',
|
|
42
|
+
input_schema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
user_id: { type: 'string', description: 'The user ID to list events for' },
|
|
46
|
+
},
|
|
47
|
+
required: ['user_id'],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
39
50
|
{
|
|
40
51
|
name: 'schedule_insight',
|
|
41
52
|
description: 'Schedule a curiosity insight to be shared with a user at a future time',
|
|
@@ -112,6 +123,15 @@ async function handleTool(name, input, selfMemory, userMap) {
|
|
|
112
123
|
return parts.length ? parts.join('\n\n') : 'No context available';
|
|
113
124
|
}
|
|
114
125
|
|
|
126
|
+
if (name === 'list_pending_events') {
|
|
127
|
+
const user = userMap.get(String(input.user_id));
|
|
128
|
+
if (!user) return 'User not found';
|
|
129
|
+
if (!user.scheduler) return 'No scheduler';
|
|
130
|
+
const events = await user.scheduler.list({ status: 'pending', limit: 20 });
|
|
131
|
+
if (!events.length) return 'No pending events';
|
|
132
|
+
return events.map(e => `[${e.due_at}] ${e.title}${e.description ? `: ${e.description}` : ''}`).join('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
115
135
|
if (name === 'schedule_insight') {
|
|
116
136
|
const user = userMap.get(String(input.user_id));
|
|
117
137
|
if (!user) return 'User not found';
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const HUMOR_MODEL = 'claude-sonnet-4-6';
|
|
2
|
+
const MAX_ITERATIONS = 8;
|
|
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 runCuriosityHumor(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
|
+
{ type: 'web_search_20250305', name: 'web_search' },
|
|
19
|
+
{
|
|
20
|
+
name: 'list_curiosity_findings',
|
|
21
|
+
description: 'List recent findings from your curiosity cycle — things you researched, interests you developed, or reflections you had',
|
|
22
|
+
input_schema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
limit: { type: 'number', description: 'Max number of findings to return (default 20)' },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'get_user_context',
|
|
31
|
+
description: 'Get behavioral patterns and profile for a specific user — needed to craft inside jokes',
|
|
32
|
+
input_schema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
user_id: { type: 'string', description: 'The user ID to get context for' },
|
|
36
|
+
},
|
|
37
|
+
required: ['user_id'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'list_pending_events',
|
|
42
|
+
description: 'List already-scheduled pending events for a user — check this before scheduling to avoid duplicates',
|
|
43
|
+
input_schema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
user_id: { type: 'string', description: 'The user ID to list events for' },
|
|
47
|
+
},
|
|
48
|
+
required: ['user_id'],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'schedule_humor',
|
|
53
|
+
description: 'Schedule a humorous moment to be delivered to a user at a future time',
|
|
54
|
+
input_schema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
user_id: { type: 'string', description: 'The user ID to share with' },
|
|
58
|
+
hint: { type: 'string', description: 'The pun, funny connection, or inside joke — just the content itself. Can include a URL if a news article or link is part of what makes it funny.' },
|
|
59
|
+
delay: { type: 'string', description: 'When to drop it — e.g. "2h", "1d", "3d", "1w"' },
|
|
60
|
+
},
|
|
61
|
+
required: ['user_id', 'hint', 'delay'],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const userList = users.map(u => `- user_id: ${u.userId}`).join('\n');
|
|
67
|
+
const system = `You just finished a curiosity cycle. Now look at what you found and see if anything is funny — a pun you can make, a weird connection, something that'd land as an inside joke with a specific person based on who they are and what you know about them.\n\nUsers:\n${userList}\n\nBe picky. Only schedule something if it's actually clever. Forced humor is worse than none.`;
|
|
68
|
+
|
|
69
|
+
const messages = [{ role: 'user', content: 'Take a look at what you found and see if anything is worth a laugh.' }];
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
72
|
+
const response = await client.messages.create({
|
|
73
|
+
model: HUMOR_MODEL,
|
|
74
|
+
max_tokens: 2000,
|
|
75
|
+
tools,
|
|
76
|
+
system,
|
|
77
|
+
messages,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
81
|
+
|
|
82
|
+
if (response.stop_reason === 'end_turn') break;
|
|
83
|
+
if (response.stop_reason !== 'tool_use') break;
|
|
84
|
+
|
|
85
|
+
const toolResults = [];
|
|
86
|
+
|
|
87
|
+
for (const block of response.content) {
|
|
88
|
+
if (block.type !== 'tool_use') continue;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const result = await handleTool(block.name, block.input, selfMemory, userMap);
|
|
92
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: result });
|
|
93
|
+
} catch (e) {
|
|
94
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: `Error: ${e.message}` });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (toolResults.length > 0) {
|
|
99
|
+
messages.push({ role: 'user', content: toolResults });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log('[curiosity-humor] Humor pass complete');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function handleTool(name, input, selfMemory, userMap) {
|
|
107
|
+
if (name === 'list_curiosity_findings') {
|
|
108
|
+
const limit = input.limit || 20;
|
|
109
|
+
const findings = await selfMemory.recent({ limit });
|
|
110
|
+
const shareable = findings.filter(f => SHAREABLE_CATEGORIES.has(f.category));
|
|
111
|
+
if (!shareable.length) return 'No findings yet';
|
|
112
|
+
return shareable.map(f => `[${f.category}] ${f.content}`).join('\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (name === 'get_user_context') {
|
|
116
|
+
const user = userMap.get(String(input.user_id));
|
|
117
|
+
if (!user) return 'User not found';
|
|
118
|
+
const parts = [];
|
|
119
|
+
if (user.userProfile) parts.push(`User profile:\n${user.userProfile}`);
|
|
120
|
+
if (user.patterns) parts.push(`Patterns:\n${user.patterns}`);
|
|
121
|
+
if (user.events?.length) {
|
|
122
|
+
parts.push(`Upcoming events:\n${user.events.map(e => `- ${e.title}${e.description ? `: ${e.description}` : ''}`).join('\n')}`);
|
|
123
|
+
}
|
|
124
|
+
return parts.length ? parts.join('\n\n') : 'No context available';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (name === 'list_pending_events') {
|
|
128
|
+
const user = userMap.get(String(input.user_id));
|
|
129
|
+
if (!user) return 'User not found';
|
|
130
|
+
if (!user.scheduler) return 'No scheduler';
|
|
131
|
+
const events = await user.scheduler.list({ status: 'pending', limit: 20 });
|
|
132
|
+
if (!events.length) return 'No pending events';
|
|
133
|
+
return events.map(e => `[${e.due_at}] ${e.title}${e.description ? `: ${e.description}` : ''}`).join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (name === 'schedule_humor') {
|
|
137
|
+
const user = userMap.get(String(input.user_id));
|
|
138
|
+
if (!user) return 'User not found';
|
|
139
|
+
if (!user.scheduler) return 'User has no scheduler';
|
|
140
|
+
|
|
141
|
+
const dueAt = resolveDelay(input.delay);
|
|
142
|
+
const instructions = `You spotted something funny during your own explorations: "${input.hint}". If the moment is right, drop it casually — a pun you just thought of, a funny connection, an inside reference. Don't explain it. Don't say it's a joke. Just let it land.`;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
await user.scheduler.add(user.chatId, 'Curiosity humor', dueAt, user.timezone, input.hint, null, null, null, instructions);
|
|
146
|
+
console.log(`[curiosity-humor] Scheduled humor for user ${input.user_id} at ${dueAt}`);
|
|
147
|
+
return 'Scheduled';
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.error(`[curiosity-humor] Failed to schedule humor for user ${input.user_id}:`, e.message);
|
|
150
|
+
return `Failed to schedule: ${e.message}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return 'Unknown tool';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = { runCuriosityHumor, resolveDelay, handleTool };
|
package/src/heartbeat.js
CHANGED
|
@@ -6,6 +6,7 @@ const { ensureUserDir } = require('./config');
|
|
|
6
6
|
const { runAnalysis } = require('./analysis');
|
|
7
7
|
const { runCuriosity } = require('./curiosity');
|
|
8
8
|
const { runCuriosityDispatch } = require('./curiosity-dispatch');
|
|
9
|
+
const { runCuriosityHumor } = require('./curiosity-humor');
|
|
9
10
|
const { createSelfMemory } = require('./memory-self');
|
|
10
11
|
|
|
11
12
|
|
|
@@ -127,6 +128,7 @@ async function runCuriosityOnce(config, allowedUsers) {
|
|
|
127
128
|
} catch { return null; }
|
|
128
129
|
}));
|
|
129
130
|
await runCuriosityDispatch(client, selfMemory, userDispatchData.filter(Boolean));
|
|
131
|
+
await runCuriosityHumor(client, selfMemory, userDispatchData.filter(Boolean));
|
|
130
132
|
} catch (e) {
|
|
131
133
|
console.error('[curiosity] Failed:', e.message);
|
|
132
134
|
} finally {
|