this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(tools): wire up Letta tool webhooks for local development

- Add TOOL_WEBHOOK_URL config for Docker→host communication
- Add POST /tools/:name webhook endpoint to handle Letta tool calls
- Register tools with Letta at startup (registerTools in letta.ts)
- Attach tool IDs to agents when created
- Update Python stubs to use embedded webhook URL
- Add human memory block with user ID for tool context

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

alice 9c2c9efc dfa6d714

+149 -12
+1
.beads/issues.jsonl
··· 20 20 {"id":"assistant-nno.4","title":"break_down_task tool (src/tools/breakdown.ts)","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:40.767448Z","updated_at":"2025-12-11T16:37:59.92983Z","closed_at":"2025-12-11T16:37:59.92983Z","dependencies":[{"issue_id":"assistant-nno.4","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:40.767905Z","created_by":"daemon"}]} 21 21 {"id":"assistant-nno.5","title":"save_item / update_item tools (src/tools/items.ts)","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:41.824017Z","updated_at":"2025-12-11T16:37:53.406355Z","closed_at":"2025-12-11T16:37:53.406355Z","dependencies":[{"issue_id":"assistant-nno.5","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:41.824488Z","created_by":"daemon"}]} 22 22 {"id":"assistant-nno.6","title":"get_open_items tool (src/tools/context.ts)","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:42.738005Z","updated_at":"2025-12-11T16:38:23.43056Z","closed_at":"2025-12-11T16:38:23.43056Z","dependencies":[{"issue_id":"assistant-nno.6","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:42.738547Z","created_by":"daemon"}]} 23 + {"id":"assistant-nno.7","title":"Wire up tool webhooks for Letta integration","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-11T16:47:18.951362Z","updated_at":"2025-12-11T16:50:50.46433Z","closed_at":"2025-12-11T16:50:50.46433Z","dependencies":[{"issue_id":"assistant-nno.7","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T16:47:18.951892Z","created_by":"daemon"}]} 23 24 {"id":"assistant-nw2","title":"Add LiteLLM proxy for OpenAI-compatible Anthropic access","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-11T15:37:07.384093Z","updated_at":"2025-12-11T16:17:46.683992Z","closed_at":"2025-12-11T16:17:46.683992Z"} 24 25 {"id":"assistant-pqh","title":"M1: E2E Chat","description":"","status":"closed","priority":0,"issue_type":"epic","created_at":"2025-12-11T13:43:34.073168Z","updated_at":"2025-12-11T14:23:40.572021Z","closed_at":"2025-12-11T14:23:40.572021Z","dependencies":[{"issue_id":"assistant-pqh","depends_on_id":"assistant-69t","type":"blocks","created_at":"2025-12-11T13:43:49.441521Z","created_by":"daemon"}]} 25 26 {"id":"assistant-pqh.1","title":"Webhook server (src/index.ts)","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:23.196257Z","updated_at":"2025-12-11T14:23:32.168811Z","closed_at":"2025-12-11T14:23:32.168811Z","dependencies":[{"issue_id":"assistant-pqh.1","depends_on_id":"assistant-pqh","type":"parent-child","created_at":"2025-12-11T13:44:23.196754Z","created_by":"daemon"}]}
+6
.env.example
··· 51 51 # Prod: /app/data/assistant.db (inside container) 52 52 DB_PATH=./data/assistant.db 53 53 54 + # === Tool Webhooks === 55 + # URL for Letta's Python tool stubs to call back to our handlers 56 + # Mac/Windows (Docker Desktop): http://host.docker.internal:3000 57 + # Linux: http://172.17.0.1:3000 (Docker bridge gateway) 58 + TOOL_WEBHOOK_URL=http://host.docker.internal:3000 59 + 54 60 # === Development === 55 61 # Set to 'development' for verbose logging 56 62 NODE_ENV=development
+18 -2
src/bot.ts
··· 11 11 import { Telegraf, type Context } from 'telegraf'; 12 12 import type { Update } from 'telegraf/types'; 13 13 import { config } from './config'; 14 - import { getLettaClient } from './letta'; 14 + import { getLettaClient, getRegisteredToolIds } from './letta'; 15 15 16 16 /** 17 17 * Create Telegraf bot instance ··· 49 49 const usernameOrUnknown = username ?? 'unknown'; 50 50 console.log(`Creating new agent for user ${userId.toString()} (${usernameOrUnknown})`); 51 51 52 + // Get registered tool IDs to attach to this agent 53 + const toolIds = getRegisteredToolIds(); 54 + console.log(`Attaching ${String(toolIds.length)} tools to new agent`); 55 + 52 56 // Workaround for Letta bug: openai-proxy/ handles are rejected during creation 53 57 // but work when set via llm_config modification. 54 58 // Step 1: Create agent with letta-free model ··· 57 61 description: `ADHD support agent for Telegram user ${userId.toString()}`, 58 62 model: 'letta/letta-free', 59 63 embedding: 'letta/letta-free', 64 + tool_ids: toolIds, 60 65 memory_blocks: [ 61 66 { 62 67 label: 'persona', ··· 67 72 - Building habits and routines 68 73 - Managing distractions 69 74 70 - Be supportive, understanding, and practical. Keep responses concise and actionable.`, 75 + Be supportive, understanding, and practical. Keep responses concise and actionable. 76 + 77 + You have access to tools for managing tasks and items. Use them to help users: 78 + - parse_brain_dump: Extract tasks from free-form text 79 + - break_down_task: Split complex tasks into subtasks 80 + - save_item: Save tasks, brain dumps, or subtasks 81 + - update_item: Update status, priority, or content 82 + - get_open_items: View open tasks and brain dumps`, 83 + }, 84 + { 85 + label: 'human', 86 + value: `Telegram user ID: ${userId.toString()}`, 71 87 }, 72 88 ], 73 89 });
+5
src/config.ts
··· 70 70 71 71 // === Database === 72 72 DB_PATH: optionalEnv('DB_PATH', './data/assistant.db'), 73 + 74 + // === Tool Webhooks === 75 + // URL for Letta's Python tool stubs to call back to our handlers 76 + // Use host.docker.internal on Mac/Windows, 172.17.0.1 on Linux 77 + TOOL_WEBHOOK_URL: optionalEnv('TOOL_WEBHOOK_URL', 'http://host.docker.internal:3000'), 73 78 } as const; 74 79 75 80 /**
+43
src/index.ts
··· 13 13 import { healthCheck, simpleHealthCheck } from './health'; 14 14 import { initializeLetta } from './letta'; 15 15 import { handleUpdate, startPolling } from './bot'; 16 + import { dispatchTool } from './tools'; 16 17 import type { Update } from 'telegraf/types'; 17 18 18 19 /** ··· 80 81 status: 200, 81 82 headers: { 'Content-Type': 'application/json' }, 82 83 }); 84 + } 85 + 86 + // POST /tools/:name - Letta tool webhook endpoint 87 + // Letta's Python tool stubs POST here to execute TypeScript handlers 88 + if (path.startsWith('/tools/') && req.method === 'POST') { 89 + const toolName = path.slice(7); // "/tools/save_item" → "save_item" 90 + 91 + if (toolName.length === 0) { 92 + return new Response(JSON.stringify({ error: 'Tool name required' }), { 93 + status: 400, 94 + headers: { 'Content-Type': 'application/json' }, 95 + }); 96 + } 97 + 98 + let args: Record<string, unknown>; 99 + try { 100 + args = (await req.json()) as Record<string, unknown>; 101 + } catch { 102 + return new Response(JSON.stringify({ error: 'Invalid JSON body' }), { 103 + status: 400, 104 + headers: { 'Content-Type': 'application/json' }, 105 + }); 106 + } 107 + 108 + // Extract user_id from args (passed by Letta agent context) 109 + const userId = typeof args['user_id'] === 'number' ? args['user_id'] : 0; 110 + 111 + try { 112 + console.log(`Tool webhook: ${toolName}`, { userId, args }); 113 + const result = await dispatchTool(toolName, args, { userId }); 114 + return new Response(JSON.stringify(result), { 115 + status: 200, 116 + headers: { 'Content-Type': 'application/json' }, 117 + }); 118 + } catch (error: unknown) { 119 + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 120 + console.error(`Tool webhook error (${toolName}):`, error); 121 + return new Response(JSON.stringify({ error: errorMessage }), { 122 + status: 500, 123 + headers: { 'Content-Type': 'application/json' }, 124 + }); 125 + } 83 126 } 84 127 85 128 // 404 for unknown routes
+71 -2
src/letta.ts
··· 9 9 10 10 import { Letta } from '@letta-ai/letta-client'; 11 11 import { config } from './config'; 12 + import { getAllLettaToolsCreate } from './tools'; 12 13 13 14 /** 14 15 * Singleton Letta client instance 15 16 */ 16 17 let lettaClient: Letta | null = null; 18 + 19 + /** 20 + * Map of tool name -> Letta tool ID 21 + * Populated during initialization 22 + */ 23 + const registeredToolIds = new Map<string, string>(); 24 + 25 + /** 26 + * Get all registered tool IDs for attaching to agents 27 + */ 28 + export function getRegisteredToolIds(): string[] { 29 + return Array.from(registeredToolIds.values()); 30 + } 17 31 18 32 /** 19 33 * Get or create the Letta client singleton ··· 87 101 } 88 102 89 103 /** 104 + * Register all tools with Letta 105 + * 106 + * Creates tools in Letta if they don't exist, or updates existing ones. 107 + * Tool IDs are stored for later attachment to agents. 108 + */ 109 + async function registerTools(): Promise<void> { 110 + const client = getLettaClient(); 111 + const toolDefs = getAllLettaToolsCreate(); 112 + 113 + console.log(`Registering ${String(toolDefs.length)} tools with Letta...`); 114 + 115 + for (const def of toolDefs) { 116 + try { 117 + // Extract tool name from the json_schema 118 + const toolName = 119 + def.json_schema && typeof def.json_schema === 'object' && 'function' in def.json_schema 120 + ? ((def.json_schema as { function?: { name?: string } }).function?.name ?? 'unknown') 121 + : 'unknown'; 122 + 123 + // Check if tool already exists by listing and filtering 124 + let existingToolId: string | null = null; 125 + for await (const tool of client.tools.list()) { 126 + if (tool.name === toolName) { 127 + existingToolId = tool.id; 128 + break; 129 + } 130 + } 131 + 132 + if (existingToolId !== null) { 133 + console.log(` Tool '${toolName}' already exists (${existingToolId})`); 134 + registeredToolIds.set(toolName, existingToolId); 135 + } else { 136 + // Create new tool 137 + const created = await client.tools.create(def); 138 + console.log(` Created tool '${toolName}' (${created.id})`); 139 + registeredToolIds.set(toolName, created.id); 140 + } 141 + } catch (error: unknown) { 142 + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 143 + console.error(` Failed to register tool:`, errorMessage); 144 + // Continue with other tools 145 + } 146 + } 147 + 148 + console.log(`Registered ${String(registeredToolIds.size)} tools`); 149 + } 150 + 151 + /** 90 152 * Get or create the ADHD assistant agent 91 153 * 92 154 * This is a placeholder for M1 implementation. ··· 122 184 throw error; 123 185 } 124 186 125 - // Placeholder: agent creation will be added in M1 126 - console.log('Letta initialization complete (agent creation deferred to M1)'); 187 + // Register tools with Letta 188 + try { 189 + await registerTools(); 190 + } catch (error) { 191 + console.error('Failed to register tools during initialization:', error); 192 + // Non-fatal - continue without tools 193 + } 194 + 195 + console.log('Letta initialization complete'); 127 196 }
+5 -8
src/tools/dispatcher.ts
··· 9 9 */ 10 10 11 11 import type { ToolCreateParams } from '@letta-ai/letta-client/resources/tools.js'; 12 + import { config } from '../config'; 12 13 13 14 /** 14 15 * Context passed to tool handlers ··· 147 148 */ 148 149 export function toLettaToolCreate(definition: ToolDefinition): ToolCreateParams { 149 150 // Generate Python source code that Letta can execute 150 - // For now, tools will be proxied through a webhook/API call 151 - // that dispatches back to our Node.js handlers 151 + // Tools are proxied through a webhook to our Bun handlers 152 + const webhookUrl = config.TOOL_WEBHOOK_URL; 153 + 152 154 const sourceCode = ` 153 155 def ${definition.name}(**kwargs): 154 156 """${definition.description}""" 155 - # This is a placeholder - actual execution happens via webhook 156 - # to the Node.js dispatcher 157 - import os 158 157 import requests 159 158 160 - webhook_url = os.environ.get('TOOL_WEBHOOK_URL') 161 - if not webhook_url: 162 - return {"error": "TOOL_WEBHOOK_URL not configured"} 159 + webhook_url = "${webhookUrl}" 163 160 164 161 try: 165 162 response = requests.post(