obol-ai 0.2.18 → 0.2.20

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.
@@ -3,7 +3,8 @@
3
3
  "allow": [
4
4
  "Bash(*)",
5
5
  "mcp__context7__query-docs",
6
- "mcp__context7__resolve-library-id"
6
+ "mcp__context7__resolve-library-id",
7
+ "Bash(gh repo clone:*)"
7
8
  ]
8
9
  }
9
- }
10
+ }
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.2.20
2
+ - increase tool iterations to 100 and max tokens to 128K
3
+ - update changelog
4
+
5
+ ## 0.2.19
6
+ - add location, venue, contact, poll message support
7
+ - update changelog
8
+
1
9
  ## 0.2.18
2
10
  - remove evolution progress bar from status UI
3
11
  - bidirectional bridge with reply button + memory_remove tool
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obol-ai",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
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": {
@@ -120,7 +120,7 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
120
120
  const toolDefs = runnableTools.map(({ run, ...def }) => def);
121
121
  const probe = await client.messages.create({
122
122
  model: activeModel,
123
- max_tokens: 4096,
123
+ max_tokens: 131072,
124
124
  system: systemPrompt,
125
125
  messages: withCacheBreakpoints([...history]),
126
126
  tools: toolDefs,
@@ -141,7 +141,7 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
141
141
 
142
142
  const runner = client.beta.messages.toolRunner({
143
143
  model: activeModel,
144
- max_tokens: 4096,
144
+ max_tokens: 131072,
145
145
  system: systemPrompt,
146
146
  messages: withCacheBreakpoints([...history]),
147
147
  tools: runnableTools.length > 0 ? runnableTools : undefined,
@@ -168,7 +168,7 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
168
168
  { type: 'text', text: 'You have used too many tool calls. Please provide a final response now based on what you have so far.' },
169
169
  ]);
170
170
  const bailoutResponse = await client.messages.create({
171
- model: activeModel, max_tokens: 4096, system: systemPrompt, messages: withCacheBreakpoints([...histories.get(chatId)]),
171
+ model: activeModel, max_tokens: 131072, system: systemPrompt, messages: withCacheBreakpoints([...histories.get(chatId)]),
172
172
  }, { signal: abortController.signal });
173
173
  histories.pushAssistant(chatId, bailoutResponse.content);
174
174
  trackUsage(bailoutResponse.usage);
@@ -182,7 +182,7 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
182
182
  vlog('[claude] No text in final response after tool use — forcing summary');
183
183
  histories.pushUser(chatId, 'Provide a concise response to the user based on the tool results above.');
184
184
  const summaryResponse = await client.messages.create({
185
- model: activeModel, max_tokens: 4096, system: systemPrompt, messages: withCacheBreakpoints([...histories.get(chatId)]),
185
+ model: activeModel, max_tokens: 131072, system: systemPrompt, messages: withCacheBreakpoints([...histories.get(chatId)]),
186
186
  }, { signal: abortController.signal });
187
187
  histories.pushAssistant(chatId, summaryResponse.content);
188
188
  trackUsage(summaryResponse.usage);
@@ -9,12 +9,17 @@ function createAnthropicClient(anthropicConfig, { useOAuth = true } = {}) {
9
9
  authToken: anthropicConfig.oauth.accessToken,
10
10
  defaultHeaders: {
11
11
  'anthropic-dangerous-direct-browser-access': 'true',
12
- 'anthropic-beta': 'claude-code-20250219,oauth-2025-04-20',
12
+ 'anthropic-beta': 'claude-code-20250219,oauth-2025-04-20,output-128k-2025-02-19',
13
13
  },
14
14
  });
15
15
  }
16
16
  if (anthropicConfig.apiKey) {
17
- return new Anthropic({ apiKey: anthropicConfig.apiKey });
17
+ return new Anthropic({
18
+ apiKey: anthropicConfig.apiKey,
19
+ defaultHeaders: {
20
+ 'anthropic-beta': 'output-128k-2025-02-19',
21
+ },
22
+ });
18
23
  }
19
24
  throw new Error('No Anthropic credentials configured. Run: obol config');
20
25
  }
@@ -1,5 +1,5 @@
1
1
  const MAX_EXEC_TIMEOUT = 120;
2
- let MAX_TOOL_ITERATIONS = 10;
2
+ let MAX_TOOL_ITERATIONS = 100;
3
3
 
4
4
  const OPTIONAL_TOOLS = {
5
5
  text_to_speech: {
@@ -15,6 +15,7 @@ const secretsCommands = require('./commands/secrets');
15
15
  const toolsCommands = require('./commands/tools');
16
16
  const { registerTextHandler } = require('./handlers/text');
17
17
  const { registerMediaHandler } = require('./handlers/media');
18
+ const { registerSpecialHandler } = require('./handlers/special');
18
19
  const { registerCallbackHandler } = require('./handlers/callbacks');
19
20
 
20
21
  function createBot(telegramConfig, config) {
@@ -107,6 +108,7 @@ function createBot(telegramConfig, config) {
107
108
  const deps = { config, allowedUsers, bot, createAsk };
108
109
  registerTextHandler(bot, deps);
109
110
  registerMediaHandler(bot, telegramConfig, deps);
111
+ registerSpecialHandler(bot, deps);
110
112
  registerCallbackHandler(bot, { config, pendingAsks, getTenant });
111
113
 
112
114
  bot.catch((err) => {
@@ -0,0 +1,165 @@
1
+ const { getTenant } = require('../../tenant');
2
+ const { describeToolCall } = require('../../status');
3
+ const { sendHtml, startTyping, splitMessage } = require('../utils');
4
+ const { createChatContext, createStatusTracker } = require('./text');
5
+
6
+ /**
7
+ * @param {import('grammy').Context} ctx
8
+ * @returns {string}
9
+ */
10
+ function buildLocationPrompt(ctx) {
11
+ const { latitude, longitude, live_period, heading, horizontal_accuracy } = ctx.message.location;
12
+ const parts = [`[User shared their location: lat ${latitude}, lng ${longitude}`];
13
+ if (live_period) parts.push(`live for ${live_period}s`);
14
+ if (heading !== undefined) parts.push(`heading ${heading}°`);
15
+ if (horizontal_accuracy !== undefined) parts.push(`accuracy ±${horizontal_accuracy}m`);
16
+ return parts.join(', ') + ']';
17
+ }
18
+
19
+ /**
20
+ * @param {import('grammy').Context} ctx
21
+ * @returns {string}
22
+ */
23
+ function buildVenuePrompt(ctx) {
24
+ const { location, title, address, foursquare_id, google_place_id } = ctx.message.venue;
25
+ let prompt = `[User shared a venue: "${title}" at ${address} (${location.latitude}, ${location.longitude})`;
26
+ if (foursquare_id) prompt += `, Foursquare: ${foursquare_id}`;
27
+ if (google_place_id) prompt += `, Google: ${google_place_id}`;
28
+ return prompt + ']';
29
+ }
30
+
31
+ /**
32
+ * @param {import('grammy').Context} ctx
33
+ * @returns {string}
34
+ */
35
+ function buildContactPrompt(ctx) {
36
+ const { phone_number, first_name, last_name, vcard } = ctx.message.contact;
37
+ const name = [first_name, last_name].filter(Boolean).join(' ');
38
+ let prompt = `[User shared a contact: ${name}, ${phone_number}`;
39
+ if (vcard) prompt += ` (vCard data included)`;
40
+ return prompt + ']';
41
+ }
42
+
43
+ /**
44
+ * @param {import('grammy').Context} ctx
45
+ * @returns {string}
46
+ */
47
+ function buildPollPrompt(ctx) {
48
+ const { question, options, type, is_anonymous, allows_multiple_answers } = ctx.message.poll;
49
+ const opts = options.map((o, i) => `${i + 1}. ${o.text}`).join(', ');
50
+ let prompt = `[User shared a ${type || 'regular'} poll: "${question}" — Options: ${opts}`;
51
+ if (!is_anonymous) prompt += ', non-anonymous';
52
+ if (allows_multiple_answers) prompt += ', multiple answers allowed';
53
+ return prompt + ']';
54
+ }
55
+
56
+ /**
57
+ * @param {import('grammy').Context} ctx
58
+ * @returns {string | null}
59
+ */
60
+ function buildSpecialPrompt(ctx) {
61
+ if (ctx.message.location && !ctx.message.venue) return buildLocationPrompt(ctx);
62
+ if (ctx.message.venue) return buildVenuePrompt(ctx);
63
+ if (ctx.message.contact) return buildContactPrompt(ctx);
64
+ if (ctx.message.poll) return buildPollPrompt(ctx);
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * @param {import('grammy').Context} ctx
70
+ * @param {string} prompt
71
+ * @param {{ config: object, allowedUsers: Set<number>, bot: import('grammy').Bot, createAsk: Function }} deps
72
+ */
73
+ async function processSpecial(ctx, prompt, deps) {
74
+ if (!ctx.from) return;
75
+ const userId = ctx.from.id;
76
+ const stopTyping = startTyping(ctx);
77
+ const status = createStatusTracker(ctx);
78
+
79
+ try {
80
+ const tenant = await getTenant(userId, deps.config);
81
+ const chatCtx = createChatContext(ctx, tenant, deps.config, deps);
82
+
83
+ chatCtx._onRouteDecision = (info) => {
84
+ status.setRouteInfo(info);
85
+ status.start();
86
+ };
87
+ chatCtx._onRouteUpdate = (update) => {
88
+ const ri = status.routeInfo;
89
+ if (!ri) return;
90
+ if (update.memoryCount !== undefined) ri.memoryCount = update.memoryCount;
91
+ if (update.model) ri.model = update.model;
92
+ };
93
+ chatCtx._onToolStart = (toolName, inputSummary) => {
94
+ status.setStatusText('Processing');
95
+ describeToolCall(tenant.claude.client, toolName, inputSummary).then(desc => {
96
+ if (desc) status.setStatusText(desc);
97
+ });
98
+ status.start();
99
+ };
100
+
101
+ const { text: response, usage, model } = await tenant.claude.chat(prompt, chatCtx);
102
+
103
+ status.stopTimer();
104
+ status.updateFormatting();
105
+ stopTyping();
106
+
107
+ if (!response?.trim()) {
108
+ status.deleteMsg();
109
+ await ctx.reply('⏹ Stopped.').catch(() => {});
110
+ return;
111
+ }
112
+
113
+ tenant.messageLog?.log(ctx.chat.id, 'user', prompt);
114
+ tenant.messageLog?.log(ctx.chat.id, 'assistant', response, { model, tokensIn: usage?.input_tokens, tokensOut: usage?.output_tokens });
115
+
116
+ if (response.length > 4096) {
117
+ for (const chunk of splitMessage(response, 4096)) await sendHtml(ctx, chunk).catch(() => {});
118
+ } else {
119
+ await sendHtml(ctx, response).catch(() => {});
120
+ }
121
+
122
+ if (usage && model) {
123
+ const tag = model.includes('opus') ? 'opus' : model.includes('haiku') ? 'haiku' : 'sonnet';
124
+ const tokIn = usage.input_tokens >= 1000 ? `${(usage.input_tokens / 1000).toFixed(1)}k` : usage.input_tokens;
125
+ const tokOut = usage.output_tokens >= 1000 ? `${(usage.output_tokens / 1000).toFixed(1)}k` : usage.output_tokens;
126
+ const dur = status.statusStart ? ((Date.now() - status.statusStart) / 1000).toFixed(1) : null;
127
+ const parts = [`◈ ${tag}`, `${tokIn} in`, `${tokOut} out`];
128
+ if (dur) parts.push(`${dur}s`);
129
+ await ctx.reply(`<code>${parts.join(' ▪ ')}</code>`, { parse_mode: 'HTML' }).catch(() => {});
130
+ }
131
+
132
+ status.deleteMsg();
133
+ } catch (e) {
134
+ status.clear();
135
+ stopTyping();
136
+ console.error('Special message handling error:', e.message);
137
+ await ctx.reply('Failed to process that message. Check logs.').catch(() => {});
138
+ }
139
+ }
140
+
141
+ /**
142
+ * @param {import('grammy').Bot} bot
143
+ * @param {{ config: object, allowedUsers: Set<number>, bot: import('grammy').Bot, createAsk: Function }} deps
144
+ */
145
+ function registerSpecialHandler(bot, deps) {
146
+ async function handleSpecial(ctx) {
147
+ if (!ctx.from) return;
148
+ const userId = ctx.from.id;
149
+ const { createRateLimiter } = require('../rate-limit');
150
+ if (!bot._rateLimiter) bot._rateLimiter = createRateLimiter();
151
+ if (bot._rateLimiter.check(userId)) return;
152
+
153
+ const prompt = buildSpecialPrompt(ctx);
154
+ if (!prompt) return;
155
+
156
+ await processSpecial(ctx, prompt, deps);
157
+ }
158
+
159
+ bot.on('message:location', handleSpecial);
160
+ bot.on('message:venue', handleSpecial);
161
+ bot.on('message:contact', handleSpecial);
162
+ bot.on('message:poll', handleSpecial);
163
+ }
164
+
165
+ module.exports = { registerSpecialHandler };