a tool to help your Letta AI agents navigate bluesky
1import { client } from "./utils/messageAgent.ts"; 2import { submitAutonomyDeclarationRecord } from "./utils/declaration.ts"; 3import type { configAgentTool, memoryBlock, notifType } from "./utils/types.ts"; 4import { receivingLikesMemory } from "./memories/receivingLikes.ts"; 5import { receivingMentionsMemory } from "./memories/receivingMentions.ts"; 6import { receivingNewFollowersMemory } from "./memories/receivingNewFollower.ts"; 7import { receivingQuotesMemory } from "./memories/receivingQuotes.ts"; 8import { receivingRepliesMemory } from "./memories/receivingReplies.ts"; 9import { receivingRepostsMemory } from "./memories/receivingReposts.ts"; 10 11import { sendingLikesMemory } from "./memories/sendingLikes.ts"; 12import { sendingPostsMemory } from "./memories/sendingPosts.ts"; 13import { sendingProfileUpdatesMemory } from "./memories/sendingProfileUpdates.ts"; 14import { sendingQuotesMemory } from "./memories/sendingQuotes.ts"; 15import { sendingRelationshipUpdatesMemory } from "./memories/sendingRelationshipUpdates.ts"; 16import { sendingRepostsMemory } from "./memories/sendingReposts.ts"; 17 18import { archivalMemoryUseMemory } from "./memories/archivalMemoryUse.ts"; 19import { blueskyBaselineMemory } from "./memories/blueskyBaseline.ts"; 20import { blueskyReflectionMemory } from "./memories/blueskyReflection.ts"; 21import { blueskyProactiveMemory } from "./memories/blueskyProactive.ts"; 22import { maintainerContactMemory } from "./memories/maintainerContact.ts"; 23import { coreMemoryUseMemory } from "./memories/coreMemoryUse.ts"; 24import { searchingBlueskyMemory } from "./memories/searchingBluesky.ts"; 25import { toolUseMemory } from "./memories/toolUse.ts"; 26 27await submitAutonomyDeclarationRecord(); 28 29/** 30 * Core memory blocks that are ALWAYS attached to the agent. 31 * These provide foundational guidance regardless of configuration. 32 */ 33const CORE_MEMORY_BLOCKS: memoryBlock[] = [ 34 archivalMemoryUseMemory, 35 blueskyBaselineMemory, 36 blueskyReflectionMemory, 37 blueskyProactiveMemory, 38 maintainerContactMemory, 39 coreMemoryUseMemory, 40 searchingBlueskyMemory, 41 toolUseMemory, 42]; 43 44/** 45 * Notification-specific memory blocks. 46 * These are CONDITIONALLY attached based on the agent's supportedNotifTypes. 47 */ 48const NOTIFICATION_MEMORY_BLOCKS: Partial<Record<notifType, memoryBlock>> = { 49 "like": receivingLikesMemory, 50 "mention": receivingMentionsMemory, 51 "follow": receivingNewFollowersMemory, 52 "quote": receivingQuotesMemory, 53 "reply": receivingRepliesMemory, 54 "repost": receivingRepostsMemory, 55}; 56 57/** 58 * Tool-specific memory blocks. 59 * These are CONDITIONALLY attached based on the agent's supportedTools. 60 * Only configurable tools need memory blocks (required tools are always available). 61 */ 62const TOOL_MEMORY_BLOCKS: Partial<Record<configAgentTool, memoryBlock>> = { 63 "create_bluesky_post": sendingPostsMemory, 64 "like_bluesky_post": sendingLikesMemory, 65 "quote_bluesky_post": sendingQuotesMemory, 66 "repost_bluesky_post": sendingRepostsMemory, 67 "update_bluesky_connection": sendingRelationshipUpdatesMemory, 68 "update_bluesky_profile": sendingProfileUpdatesMemory, 69}; 70 71/** 72 * Hardcoded tool names that should always be attached to the Bluesky agent. 73 * These are tools that already exist in the Letta registry (built-in or previously created). 74 */ 75const REQUIRED_TOOLS: string[] = [ 76 "archival_memory_insert", 77 "archival_memory_search", 78 "conversation_search", 79 "web_search", 80 "fetch_webpage", 81 "memory", 82]; 83 84/** 85 * Detects Python package imports in source code. 86 * Returns an array of package names that need to be installed. 87 */ 88function detectPipRequirements(sourceCode: string): Array<{ name: string }> { 89 const requirements: Array<{ name: string }> = []; 90 91 // Check for atproto import 92 if ( 93 sourceCode.includes("from atproto import") || 94 sourceCode.includes("import atproto") 95 ) { 96 requirements.push({ name: "atproto" }); 97 } 98 99 // Add more package detection patterns here as needed 100 // Example: if (sourceCode.includes("import requests")) { requirements.push({ name: "requests" }); } 101 102 return requirements; 103} 104 105/** 106 * Discovers Bluesky tools by scanning the tools/bluesky/ directory. 107 * Returns an array of tool names (without the .py extension). 108 */ 109async function discoverBlueskyTools(): Promise<string[]> { 110 const toolsDir = "./tools/bluesky"; 111 const tools: string[] = []; 112 113 try { 114 for await (const entry of Deno.readDir(toolsDir)) { 115 if (entry.isFile && entry.name.endsWith(".py")) { 116 // Extract tool name (remove .py extension) 117 const toolName = entry.name.slice(0, -3); 118 tools.push(toolName); 119 } 120 } 121 } catch (error) { 122 console.warn( 123 `Warning: Could not read tools directory (${toolsDir}):`, 124 error instanceof Error ? error.message : String(error), 125 ); 126 } 127 128 return tools; 129} 130 131/** 132 * Prepares the Letta agent for Bluesky by ensuring all required memory blocks 133 * are present and configured correctly. If blocks exist, their content is updated. 134 * If blocks don't exist, they are created and attached to the agent. 135 */ 136export async function mount(): Promise<void> { 137 const agentId = Deno.env.get("LETTA_AGENT_ID"); 138 const agentName = Deno.env.get("LETTA_PROJECT_NAME"); 139 140 if (!agentId) { 141 console.error( 142 "LETTA_AGENT_ID not found in environment variables. make sure to run `Deno task setup` first, then update your `.env` file.", 143 ); 144 throw new Error("LETTA_AGENT_ID is required"); 145 } 146 147 console.log(`Preparing agent ${agentName} for Bluesky...`); 148 149 try { 150 // Get the current agent state 151 const agent = await client.agents.retrieve(agentId); 152 console.log(`Agent retrieved: ${agent.name}`); 153 154 // Get all existing blocks for this agent 155 const existingBlocks = await client.agents.blocks.list(agentId); 156 console.log(`Agent has ${existingBlocks.length} existing memory blocks`); 157 158 // Build dynamic memory blocks array based on configuration 159 console.log("Building memory block configuration..."); 160 const { agentContext } = await import("./utils/agentContext.ts"); 161 const memoryBlocksToProcess: memoryBlock[] = []; 162 163 // 1. Always include core memory blocks 164 memoryBlocksToProcess.push(...CORE_MEMORY_BLOCKS); 165 console.log(`- Added ${CORE_MEMORY_BLOCKS.length} core memory blocks`); 166 167 // 2. Add notification-specific blocks based on supportedNotifTypes 168 let notifBlockCount = 0; 169 for (const notifType of agentContext.supportedNotifTypes) { 170 const notifBlock = NOTIFICATION_MEMORY_BLOCKS[notifType]; 171 if (notifBlock) { 172 memoryBlocksToProcess.push(notifBlock); 173 notifBlockCount++; 174 } 175 } 176 console.log( 177 `- Added ${notifBlockCount} notification-specific memory blocks for: ${ 178 agentContext.supportedNotifTypes.join(", ") 179 }`, 180 ); 181 182 // 3. Add tool-specific blocks based on supportedTools (only configurable tools) 183 let toolBlockCount = 0; 184 for (const tool of agentContext.supportedTools) { 185 // Type assertion needed because supportedTools includes both configurable and required tools 186 const toolBlock = TOOL_MEMORY_BLOCKS[tool as configAgentTool]; 187 if (toolBlock) { 188 memoryBlocksToProcess.push(toolBlock); 189 toolBlockCount++; 190 } 191 } 192 console.log( 193 `- Added ${toolBlockCount} tool-specific memory blocks`, 194 ); 195 196 console.log( 197 `Total memory blocks to process: ${memoryBlocksToProcess.length}`, 198 ); 199 200 // Process each required memory block 201 for (const blockConfig of memoryBlocksToProcess) { 202 // Check if a block with this label already exists 203 const existingBlock = existingBlocks.find( 204 (block: any) => block.label === blockConfig.label, 205 ); 206 207 if (existingBlock && existingBlock.id) { 208 // Block exists - update or preserve based on configuration 209 if (agentContext.preserveAgentMemory) { 210 console.log( 211 `✓ Preserving existing block: ${blockConfig.label} (PRESERVE_MEMORY_BLOCKS=true)`, 212 ); 213 } else { 214 console.log(`Updating existing block: ${blockConfig.label}`); 215 await client.blocks.modify(existingBlock.id, { 216 value: blockConfig.value, 217 description: blockConfig.description, 218 limit: blockConfig.limit, 219 }); 220 console.log(`✓ Updated block: ${blockConfig.label}`); 221 } 222 } else { 223 // Block doesn't exist - create and attach it 224 console.log(`Creating new block: ${blockConfig.label}`); 225 const newBlock = await client.blocks.create({ 226 label: blockConfig.label, 227 description: blockConfig.description, 228 value: blockConfig.value, 229 limit: blockConfig.limit, 230 }); 231 console.log(`Created block with ID: ${newBlock.id}`); 232 233 // Attach the block to the agent 234 if (newBlock.id) { 235 await client.agents.blocks.attach(agentId, newBlock.id); 236 console.log(`✓ Attached block: ${blockConfig.label}`); 237 } else { 238 throw new Error(`Failed to create block: ${blockConfig.label}`); 239 } 240 } 241 } 242 243 // Configure tool environment variables for Bluesky credentials 244 console.log("Configuring tool environment variables..."); 245 246 const bskyUsername = Deno.env.get("BSKY_USERNAME"); 247 const bskyAppPassword = Deno.env.get("BSKY_APP_PASSWORD"); 248 const bskyServiceUrl = Deno.env.get("BSKY_SERVICE_URL"); 249 250 if (!bskyUsername || !bskyAppPassword) { 251 console.warn( 252 "Warning: BSKY_USERNAME or BSKY_APP_PASSWORD not found in environment variables. " + 253 "Bluesky tools may not function properly.", 254 ); 255 } 256 257 // Update agent with tool environment variables 258 await client.agents.modify(agentId, { 259 toolExecEnvironmentVariables: { 260 BSKY_USERNAME: bskyUsername || "", 261 BSKY_APP_PASSWORD: bskyAppPassword || "", 262 BSKY_SERVICE_URL: bskyServiceUrl || "https://bsky.social", 263 }, 264 }); 265 console.log("✓ Tool environment variables configured"); 266 267 // Configure and attach required Bluesky tools 268 console.log("\nConfiguring Bluesky tools..."); 269 270 // Discover tools from the filesystem 271 const discoveredTools = await discoverBlueskyTools(); 272 console.log( 273 `Discovered ${discoveredTools.length} tool(s) in tools/bluesky/`, 274 ); 275 console.log(`Hardcoded required tools: ${REQUIRED_TOOLS.length}`); 276 277 if (discoveredTools.length === 0 && REQUIRED_TOOLS.length === 0) { 278 console.log("No tools configured."); 279 console.log( 280 "Add .py files to tools/bluesky/ or update REQUIRED_TOOLS array.", 281 ); 282 } 283 284 // Get currently attached tools 285 const attachedTools = await client.agents.tools.list(agentId); 286 const attachedToolNames = attachedTools.map((tool: any) => tool.name); 287 console.log(`Agent has ${attachedTools.length} tools currently attached`); 288 289 let toolsAttached = 0; 290 let toolsCreated = 0; 291 const missingTools: string[] = []; 292 293 // Create a user-level client for tool operations 294 // Tools are user-level resources, not project-scoped 295 const { LettaClient } = await import("@letta-ai/letta-client"); 296 const userLevelClient = new LettaClient({ 297 token: Deno.env.get("LETTA_API_KEY"), 298 }); 299 300 // First, process hardcoded required tools 301 for (const toolName of REQUIRED_TOOLS) { 302 try { 303 // Check if already attached 304 if (attachedToolNames.includes(toolName)) { 305 console.log(`✓ Required tool already attached: ${toolName}`); 306 continue; 307 } 308 309 // Search for the tool in the global registry 310 const existingTools = await userLevelClient.tools.list({ 311 name: toolName, 312 }); 313 314 if (existingTools.length > 0) { 315 const tool = existingTools[0]; 316 if (tool.id) { 317 await client.agents.tools.attach(agentId, tool.id); 318 console.log(`✓ Attached required tool: ${toolName}`); 319 toolsAttached++; 320 } 321 } else { 322 console.warn(`⚠ Required tool not found in registry: ${toolName}`); 323 missingTools.push(toolName); 324 } 325 } catch (error: any) { 326 console.warn( 327 `⚠ Error processing required tool ${toolName}:`, 328 error.message, 329 ); 330 missingTools.push(toolName); 331 } 332 } 333 334 // Then, process tools discovered from files 335 for (const toolFileName of discoveredTools) { 336 const toolFilePath = `./tools/bluesky/${toolFileName}.py`; 337 338 try { 339 const toolSource = await Deno.readTextFile(toolFilePath); 340 341 // Detect required pip packages from the source code 342 const pipRequirements = detectPipRequirements(toolSource); 343 if (pipRequirements.length > 0) { 344 console.log( 345 ` Detected pip requirements for ${toolFileName}: ${ 346 pipRequirements.map((r) => r.name).join(", ") 347 }`, 348 ); 349 } 350 351 // Try to create or find the tool 352 let tool; 353 let wasCreated = false; 354 355 try { 356 // Attempt to create the tool - Letta will extract the function name from docstring 357 const createParams: any = { 358 sourceCode: toolSource, 359 }; 360 361 // Add pip requirements if any were detected 362 if (pipRequirements.length > 0) { 363 createParams.pipRequirements = pipRequirements; 364 } 365 366 tool = await userLevelClient.tools.create(createParams); 367 wasCreated = true; 368 } catch (createError: any) { 369 // Tool might already exist - try to find it 370 // The error doesn't give us the tool name, so we'll need to parse it from the source 371 const errorBody = typeof createError.body === "string" 372 ? createError.body 373 : JSON.stringify(createError.body || ""); 374 375 if ( 376 createError.statusCode === 409 || 377 errorBody.includes("already exists") 378 ) { 379 // Extract function name from Python source using regex 380 const funcMatch = toolSource.match(/^def\s+(\w+)\s*\(/m); 381 if (funcMatch) { 382 const functionName = funcMatch[1]; 383 const existingTools = await userLevelClient.tools.list({ 384 name: functionName, 385 }); 386 if (existingTools.length > 0) { 387 tool = existingTools[0]; 388 } 389 } 390 } 391 392 // If we still don't have the tool, rethrow the error 393 if (!tool) { 394 throw createError; 395 } 396 } 397 398 const toolName = tool.name || toolFileName; 399 400 // Check if tool is already attached to the agent 401 if (attachedToolNames.includes(toolName)) { 402 console.log(`✓ Tool already attached: ${toolName}`); 403 continue; 404 } 405 406 // Attach the tool to the agent 407 if (tool.id) { 408 await client.agents.tools.attach(agentId, tool.id); 409 if (wasCreated) { 410 console.log( 411 `✓ Created and attached tool: ${toolName} (from ${toolFileName}.py)`, 412 ); 413 toolsCreated++; 414 } else { 415 console.log( 416 `✓ Attached existing tool: ${toolName} (from ${toolFileName}.py)`, 417 ); 418 toolsAttached++; 419 } 420 } 421 } catch (error: any) { 422 console.warn(`⚠ Error processing tool ${toolFileName}:`, error.message); 423 console.warn(` Attempted path: ${toolFilePath}`); 424 missingTools.push(toolFileName); 425 } 426 } 427 428 // Summary 429 console.log("\n=== Tool Configuration Summary ==="); 430 console.log(`Attached existing tools: ${toolsAttached}`); 431 console.log(`Created new tools: ${toolsCreated}`); 432 console.log( 433 `Total required tools: ${REQUIRED_TOOLS.length + discoveredTools.length}`, 434 ); 435 436 if (missingTools.length > 0) { 437 console.log(`\n⚠ Missing tools:`); 438 missingTools.forEach((tool) => { 439 if (REQUIRED_TOOLS.includes(tool)) { 440 console.log(` - ${tool} (required tool - not found in registry)`); 441 } else { 442 console.log(` - ${tool} (from tools/bluesky/${tool}.py)`); 443 } 444 }); 445 console.log( 446 "\nFor file-based tools, see tools/README.md for instructions.", 447 ); 448 console.log( 449 "For required tools, ensure they exist in your Letta registry.", 450 ); 451 } 452 453 console.log("\n✓ Agent successfully prepared for Bluesky"); 454 console.log( 455 "✓ start checking for notifications with `deno task watch` during development or `deno task start` in production", 456 ); 457 } catch (error) { 458 console.error("Error preparing agent for Bluesky:", error); 459 throw error; 460 } 461} 462 463// Run the function when this file is executed directly 464if (import.meta.main) { 465 await mount(); 466}