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