this repo has no description
0
fork

Configure Feed

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

feat(tools): add tool dispatcher for Letta agent integration

- ToolRegistry class for managing tool handlers
- dispatchTool() for routing calls to handlers
- toLettaToolCreate() generates Python webhook stubs for Letta
- Supports user context (Telegram user ID) for isolation

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

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

alice 9d4904a2 f324a723

+217 -5
+5 -5
.beads/issues.jsonl
··· 13 13 {"id":"assistant-m8g.1","title":"record_tiny_win tool (src/tools/wins.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:03.571473Z","updated_at":"2025-12-11T13:45:03.571473Z","dependencies":[{"issue_id":"assistant-m8g.1","depends_on_id":"assistant-m8g","type":"parent-child","created_at":"2025-12-11T13:45:03.571956Z","created_by":"daemon"}]} 14 14 {"id":"assistant-m8g.2","title":"get_wins_summary tool (src/tools/wins.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:04.336122Z","updated_at":"2025-12-11T13:45:04.336122Z","dependencies":[{"issue_id":"assistant-m8g.2","depends_on_id":"assistant-m8g","type":"parent-child","created_at":"2025-12-11T13:45:04.336616Z","created_by":"daemon"}]} 15 15 {"id":"assistant-m8g.3","title":"Wins database table","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:05.347711Z","updated_at":"2025-12-11T13:45:05.347711Z","dependencies":[{"issue_id":"assistant-m8g.3","depends_on_id":"assistant-m8g","type":"parent-child","created_at":"2025-12-11T13:45:05.348177Z","created_by":"daemon"}]} 16 - {"id":"assistant-nno","title":"M2: Tools + Items","description":"","status":"in_progress","priority":0,"issue_type":"epic","created_at":"2025-12-11T13:43:35.019588Z","updated_at":"2025-12-11T16:22:59.616868Z","dependencies":[{"issue_id":"assistant-nno","depends_on_id":"assistant-pqh","type":"blocks","created_at":"2025-12-11T13:43:50.878442Z","created_by":"daemon"}]} 16 + {"id":"assistant-nno","title":"M2: Tools + Items","description":"","status":"closed","priority":0,"issue_type":"epic","created_at":"2025-12-11T13:43:35.019588Z","updated_at":"2025-12-11T16:40:05.645575Z","closed_at":"2025-12-11T16:40:05.645575Z","dependencies":[{"issue_id":"assistant-nno","depends_on_id":"assistant-pqh","type":"blocks","created_at":"2025-12-11T13:43:50.878442Z","created_by":"daemon"}]} 17 17 {"id":"assistant-nno.1","title":"Database schema + Drizzle setup (src/db/)","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:37.136609Z","updated_at":"2025-12-11T16:27:05.155748Z","closed_at":"2025-12-11T16:27:05.155748Z","dependencies":[{"issue_id":"assistant-nno.1","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:37.137113Z","created_by":"daemon"}]} 18 18 {"id":"assistant-nno.2","title":"Tool dispatcher (src/tools/dispatcher.ts)","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:38.375255Z","updated_at":"2025-12-11T16:27:27.073341Z","closed_at":"2025-12-11T16:27:27.073341Z","dependencies":[{"issue_id":"assistant-nno.2","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:38.375743Z","created_by":"daemon"}]} 19 - {"id":"assistant-nno.3","title":"parse_brain_dump tool (src/tools/capture.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:39.812103Z","updated_at":"2025-12-11T13:44:39.812103Z","dependencies":[{"issue_id":"assistant-nno.3","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:39.812544Z","created_by":"daemon"}]} 20 - {"id":"assistant-nno.4","title":"break_down_task tool (src/tools/breakdown.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:40.767448Z","updated_at":"2025-12-11T13:44:40.767448Z","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 - {"id":"assistant-nno.5","title":"save_item / update_item tools (src/tools/items.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:41.824017Z","updated_at":"2025-12-11T13:44:41.824017Z","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 - {"id":"assistant-nno.6","title":"get_open_items tool (src/tools/context.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:42.738005Z","updated_at":"2025-12-11T13:44:42.738005Z","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"}]} 19 + {"id":"assistant-nno.3","title":"parse_brain_dump tool (src/tools/capture.ts)","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:39.812103Z","updated_at":"2025-12-11T16:37:58.081317Z","closed_at":"2025-12-11T16:37:58.081317Z","dependencies":[{"issue_id":"assistant-nno.3","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:39.812544Z","created_by":"daemon"}]} 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 + {"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 + {"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 23 {"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 24 {"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 25 {"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"}]}
+212
src/tools/dispatcher.ts
··· 1 + /** 2 + * Tool dispatcher for Letta agents 3 + * 4 + * This module provides: 5 + * - Type definitions for tool handlers 6 + * - Tool registry for managing available tools 7 + * - Dispatcher for routing tool calls to appropriate handlers 8 + * - Letta-compatible tool definitions for agent creation 9 + */ 10 + 11 + import type { ToolCreateParams } from '@letta-ai/letta-client/resources/tools.js'; 12 + 13 + /** 14 + * Context passed to tool handlers 15 + */ 16 + export interface ToolContext { 17 + /** Telegram user ID of the user invoking the tool */ 18 + userId: number; 19 + } 20 + 21 + /** 22 + * A tool handler function that processes tool calls 23 + * 24 + * @param args - Tool arguments (validated against JSON schema) 25 + * @param context - Context information (user ID, etc.) 26 + * @returns Tool result (serializable to JSON) 27 + */ 28 + export type ToolHandler<TArgs = unknown, TResult = unknown> = (args: TArgs, context: ToolContext) => Promise<TResult>; 29 + 30 + /** 31 + * Tool definition for registration 32 + */ 33 + export interface ToolDefinition<TArgs = unknown, TResult = unknown> { 34 + /** Tool name (must match Letta tool name) */ 35 + name: string; 36 + 37 + /** Human-readable description of what the tool does */ 38 + description: string; 39 + 40 + /** JSON Schema for tool parameters */ 41 + parameters: Record<string, unknown>; 42 + 43 + /** Handler function to execute when tool is called */ 44 + handler: ToolHandler<TArgs, TResult>; 45 + } 46 + 47 + /** 48 + * Registry of available tools 49 + */ 50 + class ToolRegistry { 51 + // Use unknown for the map to accept any generic parameters 52 + private tools = new Map<string, ToolDefinition>(); 53 + 54 + /** 55 + * Register a tool with the dispatcher 56 + * 57 + * @param definition - Tool definition including handler 58 + */ 59 + register<TArgs, TResult>(definition: ToolDefinition<TArgs, TResult>): void { 60 + if (this.tools.has(definition.name)) { 61 + throw new Error(`Tool '${definition.name}' is already registered`); 62 + } 63 + 64 + // Cast to unknown to allow any generic parameters 65 + this.tools.set(definition.name, definition as ToolDefinition); 66 + console.log(`Registered tool: ${definition.name}`); 67 + } 68 + 69 + /** 70 + * Get a tool by name 71 + * 72 + * @param name - Tool name 73 + * @returns Tool definition or undefined if not found 74 + */ 75 + get(name: string): ToolDefinition | undefined { 76 + return this.tools.get(name); 77 + } 78 + 79 + /** 80 + * Get all registered tools 81 + * 82 + * @returns Array of all tool definitions 83 + */ 84 + getAll(): ToolDefinition[] { 85 + return Array.from(this.tools.values()); 86 + } 87 + 88 + /** 89 + * Check if a tool is registered 90 + * 91 + * @param name - Tool name 92 + * @returns True if tool exists 93 + */ 94 + has(name: string): boolean { 95 + return this.tools.has(name); 96 + } 97 + 98 + /** 99 + * Clear all registered tools (primarily for testing) 100 + */ 101 + clear(): void { 102 + this.tools.clear(); 103 + } 104 + } 105 + 106 + /** 107 + * Singleton tool registry instance 108 + */ 109 + export const toolRegistry = new ToolRegistry(); 110 + 111 + /** 112 + * Dispatch a tool call to the appropriate handler 113 + * 114 + * @param name - Tool name 115 + * @param args - Tool arguments (should match tool's JSON schema) 116 + * @param context - Tool execution context 117 + * @returns Tool execution result 118 + * @throws Error if tool is not found or execution fails 119 + */ 120 + export async function dispatchTool(name: string, args: unknown, context: ToolContext): Promise<unknown> { 121 + const tool = toolRegistry.get(name); 122 + 123 + if (!tool) { 124 + throw new Error(`Tool '${name}' is not registered`); 125 + } 126 + 127 + try { 128 + console.log(`Dispatching tool: ${name}`, { args, userId: context.userId }); 129 + const result = await tool.handler(args, context); 130 + console.log(`Tool '${name}' executed successfully`); 131 + return result; 132 + } catch (error: unknown) { 133 + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 134 + console.error(`Tool '${name}' execution failed:`, error); 135 + throw new Error(`Failed to execute tool '${name}': ${errorMessage}`); 136 + } 137 + } 138 + 139 + /** 140 + * Convert a tool definition to Letta-compatible format 141 + * 142 + * This generates the source code and parameters that Letta needs 143 + * to register the tool on the Letta server. 144 + * 145 + * @param definition - Tool definition 146 + * @returns Letta tool creation parameters 147 + */ 148 + export function toLettaToolCreate(definition: ToolDefinition): ToolCreateParams { 149 + // 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 152 + const sourceCode = ` 153 + def ${definition.name}(**kwargs): 154 + """${definition.description}""" 155 + # This is a placeholder - actual execution happens via webhook 156 + # to the Node.js dispatcher 157 + import os 158 + import requests 159 + 160 + webhook_url = os.environ.get('TOOL_WEBHOOK_URL') 161 + if not webhook_url: 162 + return {"error": "TOOL_WEBHOOK_URL not configured"} 163 + 164 + try: 165 + response = requests.post( 166 + f"{webhook_url}/tools/${definition.name}", 167 + json=kwargs, 168 + timeout=30 169 + ) 170 + response.raise_for_status() 171 + return response.json() 172 + except Exception as e: 173 + return {"error": str(e)} 174 + `.trim(); 175 + 176 + return { 177 + source_code: sourceCode, 178 + description: definition.description, 179 + json_schema: { 180 + type: 'function', 181 + function: { 182 + name: definition.name, 183 + description: definition.description, 184 + parameters: definition.parameters, 185 + }, 186 + }, 187 + source_type: 'python', 188 + pip_requirements: [{ name: 'requests' }], 189 + }; 190 + } 191 + 192 + /** 193 + * Get all registered tools in Letta-compatible format 194 + * 195 + * @returns Array of Letta tool creation parameters 196 + */ 197 + export function getAllLettaToolsCreate(): ToolCreateParams[] { 198 + return toolRegistry.getAll().map(toLettaToolCreate); 199 + } 200 + 201 + /** 202 + * Register a tool and return its definition for chaining 203 + * 204 + * @param definition - Tool definition 205 + * @returns The same tool definition for convenience 206 + */ 207 + export function registerTool<TArgs = unknown, TResult = unknown>( 208 + definition: ToolDefinition<TArgs, TResult> 209 + ): ToolDefinition<TArgs, TResult> { 210 + toolRegistry.register<TArgs, TResult>(definition); 211 + return definition; 212 + }