this repo has no description
0
fork

Configure Feed

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

fix(tools): correct Letta tool registration with json_schema

Consolidates multiple debugging iterations into working tool registration.

## What was fixed

1. **json_schema format**: Letta uses a flat format, NOT OpenAI's nested format
- Wrong: `{type: 'function', function: {name, parameters}}`
- Right: `{name, description, parameters}`

2. **Schema must be explicit**: Letta's "auto-extraction from Python source"
doesn't work reliably. Always pass json_schema explicitly in both
create AND update calls.

3. **Python function signatures**: Generate explicit typed parameters
`def tool(arg: str)` instead of `def tool(**kwargs)` so Letta knows
what arguments to pass.

4. **Tool attachment timing**: Tools must be attached AFTER agent creation
when using letta-free model workaround (tool_ids in create doesn't work).

5. **Update existing tools**: Check for existing tools by name and update
them instead of failing on duplicate registration.

## Files changed

- src/tools/dispatcher.ts: Generate proper Python signatures + json_schema
- src/letta.ts: Update existing tools, include json_schema in PATCH
- src/bot.ts: Attach tools after agent creation

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

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

alice 6f77c284 903d0130

+93 -33
+12 -2
src/bot.ts
··· 55 55 56 56 // Workaround for Letta bug: openai-proxy/ handles are rejected during creation 57 57 // but work when set via llm_config modification. 58 - // Step 1: Create agent with letta-free model 58 + // Step 1: Create agent with letta-free model (tools attached separately in step 3) 59 59 const agentState = await client.agents.create({ 60 60 name: `user-${userId.toString()}-${usernameOrUnknown}`, 61 61 description: `ADHD support agent for Telegram user ${userId.toString()}`, 62 62 model: 'letta/letta-free', 63 63 embedding: 'letta/letta-free', 64 - tool_ids: toolIds, 65 64 memory_blocks: [ 66 65 { 67 66 label: 'persona', ··· 103 102 }); 104 103 105 104 console.log(`Agent ${agentState.id} configured with Claude Opus 4.5`); 105 + 106 + // Step 3: Attach tools to agent (tool_ids in create doesn't work with letta-free) 107 + // SDK signature: attach(toolID, {agent_id}) 108 + for (const toolId of toolIds) { 109 + try { 110 + await client.agents.tools.attach(toolId, { agent_id: agentState.id }); 111 + } catch (attachErr) { 112 + console.warn(`Failed to attach tool ${toolId}:`, attachErr); 113 + } 114 + } 115 + console.log(`Attached ${String(toolIds.length)} tools to agent`); 106 116 107 117 // Store the mapping 108 118 userAgentMap.set(userId, agentState.id);
+26 -20
src/letta.ts
··· 112 112 113 113 console.log(`Registering ${String(toolDefs.length)} tools with Letta...`); 114 114 115 + // Build a set of existing tool names to avoid duplicates 116 + const existingTools = new Map<string, string>(); 117 + for await (const tool of client.tools.list()) { 118 + if (tool.name !== null && tool.name !== undefined) { 119 + existingTools.set(tool.name, tool.id); 120 + } 121 + } 122 + 115 123 for (const def of toolDefs) { 116 124 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 - } 125 + // Extract expected tool name from Python source code (function name) 126 + const funcMatch = /^def\s+(\w+)\s*\(/m.exec(def.source_code); 127 + const expectedName = funcMatch?.[1] ?? 'unknown'; 131 128 132 - if (existingToolId !== null) { 133 - console.log(` Tool '${toolName}' already exists (${existingToolId})`); 134 - registeredToolIds.set(toolName, existingToolId); 129 + // Check if tool already exists 130 + const existingId = existingTools.get(expectedName); 131 + if (existingId !== undefined) { 132 + // Update existing tool with new source code and json_schema 133 + await client.tools.update(existingId, { 134 + source_code: def.source_code, 135 + description: def.description ?? null, 136 + json_schema: def.json_schema ?? null, 137 + }); 138 + console.log(` Updated tool '${expectedName}' (${existingId})`); 139 + registeredToolIds.set(expectedName, existingId); 135 140 } else { 136 - // Create new tool 141 + // Create new tool - Letta extracts name from source_code 137 142 const created = await client.tools.create(def); 138 - console.log(` Created tool '${toolName}' (${created.id})`); 139 - registeredToolIds.set(toolName, created.id); 143 + const createdName = created.name ?? expectedName; 144 + console.log(` Created tool '${createdName}' (${created.id})`); 145 + registeredToolIds.set(createdName, created.id); 140 146 } 141 147 } catch (error: unknown) { 142 148 const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+55 -11
src/tools/dispatcher.ts
··· 146 146 * @param definition - Tool definition 147 147 * @returns Letta tool creation parameters 148 148 */ 149 + /** 150 + * Generate Python function signature from JSON schema parameters 151 + */ 152 + function generatePythonParams(parameters: Record<string, unknown>): { 153 + signature: string; 154 + docParams: string; 155 + argsDict: string; 156 + } { 157 + const propsRaw = parameters['properties']; 158 + const props = (propsRaw as Record<string, Record<string, unknown>> | undefined) ?? {}; 159 + const requiredRaw = parameters['required']; 160 + const required = (requiredRaw as string[] | undefined) ?? []; 161 + 162 + const params: string[] = []; 163 + const docLines: string[] = []; 164 + const dictEntries: string[] = []; 165 + 166 + for (const [name, schema] of Object.entries(props)) { 167 + const pyType = schema['type'] === 'integer' ? 'int' : schema['type'] === 'boolean' ? 'bool' : 'str'; 168 + const isRequired = required.includes(name); 169 + const descRaw = schema['description']; 170 + const desc = typeof descRaw === 'string' ? descRaw : ''; 171 + 172 + if (isRequired) { 173 + params.push(`${name}: ${pyType}`); 174 + } else { 175 + params.push(`${name}: ${pyType} = None`); 176 + } 177 + 178 + docLines.push(` ${name}: ${desc}`); 179 + dictEntries.push(`"${name}": ${name}`); 180 + } 181 + 182 + return { 183 + signature: params.join(', '), 184 + docParams: docLines.length > 0 ? '\n\n Args:\n' + docLines.join('\n') : '', 185 + argsDict: dictEntries.join(', '), 186 + }; 187 + } 188 + 149 189 export function toLettaToolCreate(definition: ToolDefinition): ToolCreateParams { 150 190 // Generate Python source code that Letta can execute 151 191 // Tools are proxied through a webhook to our Bun handlers 152 192 const webhookUrl = config.TOOL_WEBHOOK_URL; 153 193 194 + // Generate explicit parameters from JSON schema so Letta knows what args to pass 195 + const { signature, docParams, argsDict } = generatePythonParams(definition.parameters); 196 + 154 197 const sourceCode = ` 155 - def ${definition.name}(**kwargs): 156 - """${definition.description}""" 198 + def ${definition.name}(${signature}): 199 + """${definition.description}${docParams} 200 + """ 157 201 import requests 158 202 159 203 webhook_url = "${webhookUrl}" 204 + args = {${argsDict}} 160 205 161 206 try: 162 207 response = requests.post( 163 208 f"{webhook_url}/tools/${definition.name}", 164 - json=kwargs, 209 + json=args, 165 210 timeout=30 166 211 ) 167 212 response.raise_for_status() ··· 170 215 return {"error": str(e)} 171 216 `.trim(); 172 217 218 + // Letta's json_schema format: flat object with name, description, parameters 219 + // (different from OpenAI's nested {type: 'function', function: {...}} format) 173 220 return { 174 221 source_code: sourceCode, 175 222 description: definition.description, 176 - json_schema: { 177 - type: 'function', 178 - function: { 179 - name: definition.name, 180 - description: definition.description, 181 - parameters: definition.parameters, 182 - }, 183 - }, 184 223 source_type: 'python', 185 224 pip_requirements: [{ name: 'requests' }], 225 + json_schema: { 226 + name: definition.name, 227 + description: definition.description, 228 + parameters: definition.parameters, 229 + }, 186 230 }; 187 231 } 188 232