this repo has no description
0
fork

Configure Feed

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

refactor(bot): use single agent, add /reset command and cleanup script

Changes:
- Single agent named 'adhd-support-agent' instead of per-user agents
- Agent is found by name on startup, created only if not found
- Added /reset command to delete and recreate the agent
- Added scripts/cleanup-agents.ts to bulk delete old agents

Usage:
bun run scripts/cleanup-agents.ts # Dry run - list agents
bun run scripts/cleanup-agents.ts --delete # Delete all except adhd-support-agent

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

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

alice f6c0481b d7a7bca6

+132 -38
+75
scripts/cleanup-agents.ts
··· 1 + /** 2 + * Cleanup script to delete old/stale agents from Letta 3 + * 4 + * Usage: 5 + * bun run scripts/cleanup-agents.ts # List agents (dry run) 6 + * bun run scripts/cleanup-agents.ts --delete # Delete all except 'adhd-support-agent' 7 + */ 8 + 9 + import { Letta } from '@letta-ai/letta-client'; 10 + 11 + const LETTA_BASE_URL = process.env['LETTA_BASE_URL'] ?? 'http://localhost:8283'; 12 + const KEEP_AGENT_NAME = 'adhd-support-agent'; 13 + 14 + async function main() { 15 + const deleteMode = process.argv.includes('--delete'); 16 + 17 + console.log(`Connecting to Letta at ${LETTA_BASE_URL}...`); 18 + const client = new Letta({ baseURL: LETTA_BASE_URL }); 19 + 20 + const agents: { id: string; name: string | null; createdAt: Date | null }[] = []; 21 + 22 + console.log('Fetching agents...\n'); 23 + for await (const agent of client.agents.list()) { 24 + agents.push({ 25 + id: agent.id, 26 + name: agent.name, 27 + createdAt: agent.created_at, 28 + }); 29 + } 30 + 31 + console.log(`Found ${String(agents.length)} agent(s):\n`); 32 + 33 + const toDelete: string[] = []; 34 + const toKeep: string[] = []; 35 + 36 + for (const agent of agents) { 37 + const name = agent.name ?? '(unnamed)'; 38 + const createdRaw = agent.createdAt; 39 + const created = typeof createdRaw === 'string' ? createdRaw.slice(0, 16) : 'unknown'; 40 + 41 + if (agent.name === KEEP_AGENT_NAME) { 42 + console.log(` ✓ KEEP: ${name} (${agent.id.slice(0, 12)}...) - ${created}`); 43 + toKeep.push(agent.id); 44 + } else { 45 + console.log(` ✗ DELETE: ${name} (${agent.id.slice(0, 12)}...) - ${created}`); 46 + toDelete.push(agent.id); 47 + } 48 + } 49 + 50 + console.log(`\nSummary: ${String(toKeep.length)} to keep, ${String(toDelete.length)} to delete`); 51 + 52 + if (toDelete.length === 0) { 53 + console.log('\nNothing to delete!'); 54 + return; 55 + } 56 + 57 + if (!deleteMode) { 58 + console.log('\nDry run - no agents deleted. Run with --delete to actually delete.'); 59 + return; 60 + } 61 + 62 + console.log('\nDeleting agents...'); 63 + for (const id of toDelete) { 64 + try { 65 + await client.agents.delete(id); 66 + console.log(` Deleted: ${id}`); 67 + } catch (err) { 68 + console.error(` Failed to delete ${id}:`, err); 69 + } 70 + } 71 + 72 + console.log('\nDone!'); 73 + } 74 + 75 + main().catch(console.error);
+57 -38
src/bot.ts
··· 19 19 export const bot = new Telegraf(config.TELEGRAM_BOT_TOKEN); 20 20 21 21 /** 22 - * In-memory map of Telegram user ID -> Letta agent ID 23 - * In production, this would be stored in a database 22 + * Single agent ID for the bot (found or created on startup) 24 23 */ 25 - const userAgentMap = new Map<number, string>(); 24 + let agentId: string | null = null; 25 + 26 + const AGENT_NAME = 'adhd-support-agent'; 26 27 27 28 /** 28 - * Get or create a Letta agent for the given Telegram user 29 + * Get or create the single ADHD support agent 29 30 * 30 - * For M1, this is a simple implementation that creates one agent per user. 31 - * Later milestones will add more sophisticated agent management. 31 + * Searches for existing agent by name, creates if not found. 32 + * This ensures we reuse the same agent across restarts. 32 33 * 33 - * @param userId - Telegram user ID 34 - * @param username - Telegram username (for logging/debugging) 35 34 * @returns Agent ID 36 35 */ 37 - async function getOrCreateAgentForUser(userId: number, username?: string): Promise<string> { 38 - // Check if we already have an agent for this user 39 - const existingAgentId = userAgentMap.get(userId); 40 - if (existingAgentId !== undefined) { 41 - console.log(`Using existing agent ${existingAgentId} for user ${userId.toString()}`); 42 - return existingAgentId; 36 + async function getOrCreateAgent(): Promise<string> { 37 + if (agentId !== null) { 38 + return agentId; 43 39 } 44 40 45 - // Create a new agent for this user 46 41 const client = getLettaClient(); 47 42 48 43 try { 49 - const usernameOrUnknown = username ?? 'unknown'; 50 - console.log(`Creating new agent for user ${userId.toString()} (${usernameOrUnknown})`); 44 + // Search for existing agent by name 45 + console.log(`Looking for existing agent '${AGENT_NAME}'...`); 46 + for await (const agent of client.agents.list()) { 47 + if (agent.name === AGENT_NAME) { 48 + console.log(`Found existing agent: ${agent.id}`); 49 + agentId = agent.id; 50 + return agentId; 51 + } 52 + } 53 + 54 + // No existing agent found, create one 55 + console.log(`No existing agent found, creating '${AGENT_NAME}'...`); 51 56 52 57 // Get registered tool IDs to attach to this agent 53 58 const toolIds = getRegisteredToolIds(); 54 - console.log(`Attaching ${String(toolIds.length)} tools to new agent`); 59 + console.log(`Will attach ${String(toolIds.length)} tools to new agent`); 55 60 56 61 // Workaround for Letta bug: openai-proxy/ handles are rejected during creation 57 62 // but work when set via llm_config modification. 58 63 // Step 1: Create agent with letta-free model (tools attached separately in step 3) 59 64 const agentState = await client.agents.create({ 60 - name: `user-${userId.toString()}-${usernameOrUnknown}`, 61 - description: `ADHD support agent for Telegram user ${userId.toString()}`, 65 + name: AGENT_NAME, 66 + description: 'ADHD support agent for task management and gentle accountability', 62 67 model: 'letta/letta-free', 63 68 embedding: 'letta/letta-free', 64 69 memory_blocks: [ ··· 82 87 }, 83 88 { 84 89 label: 'human', 85 - value: `Telegram user ID: ${userId.toString()}`, 90 + value: 'A person with ADHD who needs help managing tasks and staying focused.', 86 91 }, 87 92 ], 88 93 }); ··· 114 119 } 115 120 console.log(`Attached ${String(toolIds.length)} tools to agent`); 116 121 117 - // Store the mapping 118 - userAgentMap.set(userId, agentState.id); 119 - 120 - return agentState.id; 122 + agentId = agentState.id; 123 + return agentId; 121 124 } catch (err: unknown) { 122 125 const errorMessage = err instanceof Error ? err.message : 'Unknown error'; 123 - console.error(`Failed to create agent for user ${userId.toString()}:`, err); 126 + console.error('Failed to create agent:', err); 124 127 throw new Error(`Failed to create agent: ${errorMessage}`); 125 128 } 126 129 } ··· 270 273 }); 271 274 272 275 /** 276 + * Handle /reset command - delete and recreate the agent 277 + */ 278 + bot.command('reset', async (ctx: Context) => { 279 + try { 280 + await ctx.sendChatAction('typing'); 281 + 282 + const client = getLettaClient(); 283 + 284 + if (agentId !== null) { 285 + console.log(`Deleting agent ${agentId}...`); 286 + await client.agents.delete(agentId); 287 + agentId = null; 288 + console.log('Agent deleted'); 289 + } 290 + 291 + // Create fresh agent 292 + const newAgentId = await getOrCreateAgent(); 293 + await ctx.reply(`Agent reset successfully. New agent ID: ${newAgentId.slice(0, 12)}...`); 294 + } catch (err: unknown) { 295 + console.error('Error resetting agent:', err); 296 + await ctx.reply('Failed to reset agent. Check logs for details.'); 297 + } 298 + }); 299 + 300 + /** 273 301 * Handle text messages 274 302 */ 275 303 bot.on('message', async (ctx: Context) => { ··· 280 308 281 309 const messageText = ctx.message.text; 282 310 283 - // Check if ctx.from exists (it should always exist for messages, but TypeScript requires the check) 284 - if (!ctx.from) { 285 - console.error('Message received without sender information'); 286 - return; 287 - } 288 - 289 - const userId = ctx.from.id; 290 - const username = ctx.from.username; 291 - 292 311 // Skip if it's a command (already handled by command handlers) 293 312 if (messageText.startsWith('/')) { 294 313 return; ··· 298 317 // Show typing indicator while processing 299 318 await ctx.sendChatAction('typing'); 300 319 301 - // Get or create agent for this user 302 - const agentId = await getOrCreateAgentForUser(userId, username); 320 + // Get or create the single agent 321 + const currentAgentId = await getOrCreateAgent(); 303 322 304 323 // Send message to agent and get response 305 - const response = await sendMessageToAgent(agentId, messageText); 324 + const response = await sendMessageToAgent(currentAgentId, messageText); 306 325 307 326 // Reply to user 308 327 await ctx.reply(response);