From fe61c8869f73e28d01cbfe97c21ea1bfd5b887fe Mon Sep 17 00:00:00 2001 From: Cameron Date: Thu, 29 Jan 2026 21:58:02 -0800 Subject: [PATCH] Fix message bubbles and tool approval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Separate message bubbles when stream message type changes (e.g., assistant → tool_call → assistant now sends as separate messages) - Track sentAnyMessage to avoid spurious "(No response from agent)" - Add canUseTool workaround for SDK v0.0.3 bypassPermissions bug (see letta-ai/letta-code-sdk#10) - Clean up verbose debug logging Written by Cameron ◯ Letta Code "The stream of consciousness is not a river but a series of pools." - William James --- package-lock.json | 25 ++++++------ src/core/bot.ts | 98 +++++++++++++++++++++-------------------------- 2 files changed, 56 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2362fb1..0e3838f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "bin": { "lettabot": "dist/cli.js", "lettabot-message": "dist/cli/message.js", + "lettabot-react": "dist/cli/react.js", "lettabot-schedule": "dist/cron/cli.js" }, "optionalDependencies": { @@ -44,9 +45,9 @@ "extraneous": true }, "node_modules/@alcalzone/ansi-tokenize": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.3.tgz", - "integrity": "sha512-jsElTJ0sQ4wHRz+C45tfect76BwbTbgkgKByOzpCN9xG61N5V6u/glvg1CsNJhq2xJIFpKHSwG3D2wPPuEYOrQ==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.4.tgz", + "integrity": "sha512-HTgrrTgZ9Jgeo6Z3oqbQ7lifOVvRR14vaDuBGPPUxk9Thm+vObaO4QfYYYWw4Zo5CWQDBEfsinFA6Gre+AqwNQ==", "license": "MIT", "peer": true, "dependencies": { @@ -1263,9 +1264,9 @@ "license": "Apache-2.0" }, "node_modules/@letta-ai/letta-code": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/@letta-ai/letta-code/-/letta-code-0.13.11.tgz", - "integrity": "sha512-L1zQ+Pvn2FNzxNdgCXR5pQxS1Yhnbc+Mm/VyfcwrxCQ4QlIewv1q5MSexP0qZHTHC4ZnOjdbaDPMJWMXBMPeBw==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@letta-ai/letta-code/-/letta-code-0.14.1.tgz", + "integrity": "sha512-4XQQxqDUlFNo7uKBilyIJ4KKHC8QrFoeMfZXmr9LgtNMtXYqB+I5AYuCnG9BBueeZgO1hlDN2ekJZELyJUqrPQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -2263,9 +2264,9 @@ } }, "node_modules/cli-truncate/node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", + "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", "license": "MIT", "peer": true, "dependencies": { @@ -3558,9 +3559,9 @@ "peer": true }, "node_modules/ink/node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", + "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", "license": "MIT", "peer": true, "dependencies": { diff --git a/src/core/bot.ts b/src/core/bot.ts index 67def88..1332ddf 100644 --- a/src/core/bot.ts +++ b/src/core/bot.ts @@ -166,15 +166,15 @@ export class LettaBot { // Create or resume session let session: Session; // Base options for all sessions (model only included for new agents) + // Note: canUseTool workaround for SDK v0.0.3 bug - can be removed after letta-ai/letta-code-sdk#10 is released const baseOptions = { permissionMode: 'bypassPermissions' as const, allowedTools: this.config.allowedTools, cwd: this.config.workingDir, systemPrompt: SYSTEM_PROMPT, + canUseTool: () => ({ allow: true }), }; - console.log('[Bot] Session options:', JSON.stringify(baseOptions, null, 2)); - try { if (this.store.agentId) { process.env.LETTA_AGENT_ID = this.store.agentId; @@ -188,35 +188,6 @@ export class LettaBot { // Only pass model when creating a new agent session = createSession({ ...baseOptions, model: this.config.model, memory: loadMemoryBlocks(this.config.agentName) }); } - console.log(`[Bot] Session object:`, Object.keys(session)); - console.log(`[Bot] Session initialized:`, (session as any).initialized); - console.log(`[Bot] Session _agentId:`, (session as any)._agentId); - console.log(`[Bot] Session options.permissionMode:`, (session as any).options?.permissionMode); - - // Hook into transport errors and stdout - const transport = (session as any).transport; - if (transport?.process) { - console.log('[Bot] Transport process PID:', transport.process.pid); - transport.process.stdout?.on('data', (data: Buffer) => { - console.log('[Bot] CLI stdout:', data.toString().slice(0, 500)); - }); - transport.process.stderr?.on('data', (data: Buffer) => { - console.error('[Bot] CLI stderr:', data.toString()); - }); - transport.process.on('exit', (code: number) => { - console.log('[Bot] CLI process exited with code:', code); - }); - transport.process.on('error', (err: Error) => { - console.error('[Bot] CLI process error:', err); - }); - } else { - console.log('[Bot] No transport process found'); - } - - // Initialize session explicitly (so we can log timing/failures) - console.log('[Bot] About to initialize session...'); - console.log('[Bot] LETTA_API_KEY in env:', process.env.LETTA_API_KEY ? `${process.env.LETTA_API_KEY.slice(0, 30)}...` : 'NOT SET'); - console.log('[Bot] LETTA_CLI_PATH:', process.env.LETTA_CLI_PATH || 'not set (will use default)'); const initTimeoutMs = 30000; // Increased to 30s const withTimeout = async (promise: Promise, label: string): Promise => { @@ -233,22 +204,15 @@ export class LettaBot { } }; - console.log('[Bot] Initializing session...'); const initInfo = await withTimeout(session.initialize(), 'Session initialize'); - console.log('[Bot] Session initialized:', initInfo); + console.log('[Bot] Session initialized, agent:', initInfo.agentId); // Send message to agent with metadata envelope const formattedMessage = formatMessageEnvelope(msg); - console.log('[Bot] Formatted message:', formattedMessage.slice(0, 200)); - console.log('[Bot] Target server:', process.env.LETTA_BASE_URL || 'https://api.letta.com (default)'); - console.log('[Bot] API key:', process.env.LETTA_API_KEY ? `${process.env.LETTA_API_KEY.slice(0, 20)}...` : '(not set)'); - console.log('[Bot] Agent ID:', this.store.agentId || '(new agent)'); - console.log('[Bot] Sending message to session...'); try { await withTimeout(session.send(formattedMessage), 'Session send'); - console.log('[Bot] Message sent successfully, starting stream...'); } catch (sendError) { - console.error('[Bot] Error in session.send():', sendError); + console.error('[Bot] Error sending message:', sendError); throw sendError; } @@ -256,6 +220,28 @@ export class LettaBot { let response = ''; let lastUpdate = Date.now(); let messageId: string | null = null; + let lastMsgType: string | null = null; + let sentAnyMessage = false; + + // Helper to finalize and send current accumulated response + const finalizeMessage = async () => { + if (response.trim()) { + try { + if (messageId) { + await adapter.editMessage(msg.chatId, messageId, response); + } else { + await adapter.sendMessage({ chatId: msg.chatId, text: response, threadId: msg.threadId }); + } + sentAnyMessage = true; + } catch { + // Ignore send errors + } + } + // Reset for next message bubble + response = ''; + messageId = null; + lastUpdate = Date.now(); + }; // Keep typing indicator alive const typingInterval = setInterval(() => { @@ -264,6 +250,13 @@ export class LettaBot { try { for await (const streamMsg of session.stream()) { + // When message type changes, finalize the current message + // This ensures different message types appear as separate bubbles + if (lastMsgType && lastMsgType !== streamMsg.type && response.trim()) { + await finalizeMessage(); + } + lastMsgType = streamMsg.type; + if (streamMsg.type === 'assistant') { response += streamMsg.content; @@ -308,44 +301,37 @@ export class LettaBot { clearInterval(typingInterval); } - console.log(`[Bot] Stream complete. Response length: ${response.length}`); - console.log(`[Bot] Response preview: ${response.slice(0, 100)}...`); - // Send final response - if (response) { - console.log(`[Bot] Sending final response (messageId=${messageId})`); + if (response.trim()) { try { if (messageId) { await adapter.editMessage(msg.chatId, messageId, response); - console.log('[Bot] Edited existing message'); } else { await adapter.sendMessage({ chatId: msg.chatId, text: response, threadId: msg.threadId }); - console.log('[Bot] Sent new message'); } + sentAnyMessage = true; } catch (sendError) { - console.error('[Bot] Error sending final message:', sendError); - // If we already sent a streamed message, don't duplicate — the user already saw it. + console.error('[Bot] Error sending response:', sendError); if (!messageId) { await adapter.sendMessage({ chatId: msg.chatId, text: response, threadId: msg.threadId }); + sentAnyMessage = true; } } - } else { - console.log('[Bot] No response from agent, sending placeholder'); + } + + // Only show "no response" if we never sent anything + if (!sentAnyMessage) { await adapter.sendMessage({ chatId: msg.chatId, text: '(No response from agent)', threadId: msg.threadId }); } } catch (error) { console.error('[Bot] Error processing message:', error); - if (error instanceof Error) { - console.error('[Bot] Error stack:', error.stack); - } await adapter.sendMessage({ chatId: msg.chatId, text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, threadId: msg.threadId, }); } finally { - console.log('[Bot] Closing session'); session!?.close(); } } @@ -365,11 +351,13 @@ export class LettaBot { _context?: TriggerContext ): Promise { // Base options (model only for new agents) + // Note: canUseTool workaround for SDK v0.0.3 bug - can be removed after letta-ai/letta-code-sdk#10 is released const baseOptions = { permissionMode: 'bypassPermissions' as const, allowedTools: this.config.allowedTools, cwd: this.config.workingDir, systemPrompt: SYSTEM_PROMPT, + canUseTool: () => ({ allow: true }), }; let session: Session;