a tool to help your Letta AI agents navigate bluesky
at main 17 kB view raw
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_ID"); 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 existingBlocksPage = await client.agents.blocks.list(agentId); 160 const existingBlocks = existingBlocksPage.items; 161 console.log(`Agent has ${existingBlocks.length} existing memory blocks`); 162 163 // Build dynamic memory blocks array based on configuration 164 console.log("Building memory block configuration..."); 165 const { agentContext } = await import("./utils/agentContext.ts"); 166 const memoryBlocksToProcess: memoryBlock[] = []; 167 168 // 1. Always include core memory blocks 169 memoryBlocksToProcess.push(...CORE_MEMORY_BLOCKS); 170 console.log(`- Added ${CORE_MEMORY_BLOCKS.length} core memory blocks`); 171 172 // 2. Add notification-specific blocks based on supportedNotifTypes 173 let notifBlockCount = 0; 174 for (const notifType of agentContext.supportedNotifTypes) { 175 const notifBlock = NOTIFICATION_MEMORY_BLOCKS[notifType]; 176 if (notifBlock) { 177 memoryBlocksToProcess.push(notifBlock); 178 notifBlockCount++; 179 } 180 } 181 console.log( 182 `- Added ${notifBlockCount} notification-specific memory blocks for: ${ 183 agentContext.supportedNotifTypes.join(", ") 184 }`, 185 ); 186 187 // 3. Add tool-specific blocks based on supportedTools (only configurable tools) 188 let toolBlockCount = 0; 189 for (const tool of agentContext.supportedTools) { 190 // Type assertion needed because supportedTools includes both configurable and required tools 191 const toolBlock = TOOL_MEMORY_BLOCKS[tool as configAgentTool]; 192 if (toolBlock) { 193 memoryBlocksToProcess.push(toolBlock); 194 toolBlockCount++; 195 } 196 } 197 console.log( 198 `- Added ${toolBlockCount} tool-specific memory blocks`, 199 ); 200 201 console.log( 202 `Total memory blocks to process: ${memoryBlocksToProcess.length}`, 203 ); 204 205 // Process each required memory block 206 for (const blockConfig of memoryBlocksToProcess) { 207 // Check if a block with this label already exists 208 const existingBlock = existingBlocks.find( 209 (block: any) => block.label === blockConfig.label, 210 ); 211 212 if (existingBlock && existingBlock.id) { 213 // Block exists - update or preserve based on configuration 214 if (agentContext.preserveAgentMemory) { 215 console.log( 216 `✓ Preserving existing block: ${blockConfig.label} (PRESERVE_MEMORY_BLOCKS=true)`, 217 ); 218 } else { 219 console.log(`Updating existing block: ${blockConfig.label}`); 220 await client.blocks.update(existingBlock.id, { 221 value: blockConfig.value, 222 description: blockConfig.description, 223 limit: blockConfig.limit, 224 }); 225 console.log(`✓ Updated block: ${blockConfig.label}`); 226 } 227 } else { 228 // Block doesn't exist - create and attach it 229 console.log(`Creating new block: ${blockConfig.label}`); 230 const newBlock = await client.blocks.create({ 231 label: blockConfig.label, 232 description: blockConfig.description, 233 value: blockConfig.value, 234 limit: blockConfig.limit, 235 }); 236 console.log(`Created block with ID: ${newBlock.id}`); 237 238 // Attach the block to the agent 239 if (newBlock.id) { 240 await client.agents.blocks.attach(newBlock.id, { agent_id: agentId }); 241 console.log(`✓ Attached block: ${blockConfig.label}`); 242 } else { 243 throw new Error(`Failed to create block: ${blockConfig.label}`); 244 } 245 } 246 } 247 248 // Configure tool environment variables for Bluesky credentials 249 console.log("Configuring tool environment variables..."); 250 251 const bskyUsername = Deno.env.get("BSKY_USERNAME"); 252 const bskyAppPassword = Deno.env.get("BSKY_APP_PASSWORD"); 253 const bskyServiceUrl = Deno.env.get("BSKY_SERVICE_URL"); 254 255 if (!bskyUsername || !bskyAppPassword) { 256 console.warn( 257 "Warning: BSKY_USERNAME or BSKY_APP_PASSWORD not found in environment variables. " + 258 "Bluesky tools may not function properly.", 259 ); 260 } 261 262 // Update agent with tool environment variables 263 await client.agents.update(agentId, { 264 secrets: { 265 BSKY_USERNAME: bskyUsername || "", 266 BSKY_APP_PASSWORD: bskyAppPassword || "", 267 BSKY_SERVICE_URL: bskyServiceUrl || "https://bsky.social", 268 }, 269 }); 270 console.log("✓ Tool environment variables configured"); 271 272 // Configure and attach required Bluesky tools 273 console.log("\nConfiguring Bluesky tools..."); 274 275 // Discover tools from the filesystem 276 const discoveredTools = await discoverBlueskyTools(); 277 console.log( 278 `Discovered ${discoveredTools.length} tool(s) in tools/bluesky/`, 279 ); 280 console.log(`Hardcoded required tools: ${REQUIRED_TOOLS.length}`); 281 282 if (discoveredTools.length === 0 && REQUIRED_TOOLS.length === 0) { 283 console.log("No tools configured."); 284 console.log( 285 "Add .py files to tools/bluesky/ or update REQUIRED_TOOLS array.", 286 ); 287 } 288 289 // Get currently attached tools 290 const attachedToolsPage = await client.agents.tools.list(agentId); 291 const attachedTools = attachedToolsPage.items; 292 const attachedToolNames = attachedTools.map((tool: any) => tool.name); 293 console.log(`Agent has ${attachedTools.length} tools currently attached`); 294 295 let toolsAttached = 0; 296 let toolsCreated = 0; 297 const missingTools: string[] = []; 298 299 // Create a user-level client for tool operations 300 // Tools are user-level resources, not project-scoped 301 const { default: Letta } = await import("@letta-ai/letta-client"); 302 const userLevelClient = new Letta({ 303 apiKey: Deno.env.get("LETTA_API_KEY"), 304 }); 305 306 // First, process hardcoded required tools 307 for (const toolName of REQUIRED_TOOLS) { 308 try { 309 // Check if already attached 310 if (attachedToolNames.includes(toolName)) { 311 console.log(`✓ Required tool already attached: ${toolName}`); 312 continue; 313 } 314 315 // Search for the tool in the global registry 316 const existingToolsPage = await userLevelClient.tools.list({ 317 name: toolName, 318 }); 319 const existingTools = existingToolsPage.items; 320 321 if (existingTools.length > 0) { 322 const tool = existingTools[0]; 323 if (tool.id) { 324 await client.agents.tools.attach(tool.id, { agent_id: agentId }); 325 console.log(`✓ Attached required tool: ${toolName}`); 326 toolsAttached++; 327 } 328 } else { 329 console.warn(`⚠ Required tool not found in registry: ${toolName}`); 330 missingTools.push(toolName); 331 } 332 } catch (error: any) { 333 console.warn( 334 `⚠ Error processing required tool ${toolName}:`, 335 error.message, 336 ); 337 missingTools.push(toolName); 338 } 339 } 340 341 // Then, process tools discovered from files 342 for (const toolFileName of discoveredTools) { 343 const toolFilePath = `./tools/bluesky/${toolFileName}.py`; 344 345 try { 346 const toolSource = await Deno.readTextFile(toolFilePath); 347 348 // Detect required pip packages from the source code 349 const pipRequirements = detectPipRequirements(toolSource); 350 if (pipRequirements.length > 0) { 351 console.log( 352 ` Detected pip requirements for ${toolFileName}: ${ 353 pipRequirements.map((r) => r.name).join(", ") 354 }`, 355 ); 356 } 357 358 // Try to create or find the tool 359 let tool; 360 let wasCreated = false; 361 362 try { 363 // Attempt to create the tool - Letta will extract the function name from docstring 364 const createParams: any = { 365 source_code: toolSource, 366 }; 367 368 // Add pip requirements if any were detected 369 if (pipRequirements.length > 0) { 370 createParams.pip_requirements = pipRequirements; 371 } 372 373 tool = await userLevelClient.tools.create(createParams); 374 wasCreated = true; 375 } catch (createError: any) { 376 // Tool might already exist - try to find it 377 // The error doesn't give us the tool name, so we'll need to parse it from the source 378 const errorBody = typeof createError.body === "string" 379 ? createError.body 380 : JSON.stringify(createError.body || ""); 381 382 if ( 383 createError.statusCode === 409 || 384 errorBody.includes("already exists") 385 ) { 386 // Extract function name from Python source using regex 387 const funcMatch = toolSource.match(/^def\s+(\w+)\s*\(/m); 388 if (funcMatch) { 389 const functionName = funcMatch[1]; 390 const existingToolsPage = await userLevelClient.tools.list({ 391 name: functionName, 392 }); 393 const existingTools = existingToolsPage.items; 394 if (existingTools.length > 0) { 395 tool = existingTools[0]; 396 } 397 } 398 } 399 400 // If we still don't have the tool, rethrow the error 401 if (!tool) { 402 throw createError; 403 } 404 } 405 406 const toolName = tool.name || toolFileName; 407 408 // Check if tool is already attached to the agent 409 if (attachedToolNames.includes(toolName)) { 410 console.log(`✓ Tool already attached: ${toolName}`); 411 continue; 412 } 413 414 // Attach the tool to the agent 415 if (tool.id) { 416 await client.agents.tools.attach(tool.id, { agent_id: agentId }); 417 if (wasCreated) { 418 console.log( 419 `✓ Created and attached tool: ${toolName} (from ${toolFileName}.py)`, 420 ); 421 toolsCreated++; 422 } else { 423 console.log( 424 `✓ Attached existing tool: ${toolName} (from ${toolFileName}.py)`, 425 ); 426 toolsAttached++; 427 } 428 } 429 } catch (error: any) { 430 console.warn(`⚠ Error processing tool ${toolFileName}:`, error.message); 431 console.warn(` Attempted path: ${toolFilePath}`); 432 missingTools.push(toolFileName); 433 } 434 } 435 436 // Summary 437 console.log("\n=== Tool Configuration Summary ==="); 438 console.log(`Attached existing tools: ${toolsAttached}`); 439 console.log(`Created new tools: ${toolsCreated}`); 440 console.log( 441 `Total required tools: ${REQUIRED_TOOLS.length + discoveredTools.length}`, 442 ); 443 444 if (missingTools.length > 0) { 445 console.log(`\n⚠ Missing tools:`); 446 missingTools.forEach((tool) => { 447 if (REQUIRED_TOOLS.includes(tool)) { 448 console.log(` - ${tool} (required tool - not found in registry)`); 449 } else { 450 console.log(` - ${tool} (from tools/bluesky/${tool}.py)`); 451 } 452 }); 453 console.log( 454 "\nFor file-based tools, see tools/README.md for instructions.", 455 ); 456 console.log( 457 "For required tools, ensure they exist in your Letta registry.", 458 ); 459 } 460 461 console.log("\n✓ Agent successfully prepared for Bluesky"); 462 console.log( 463 "✓ start checking for notifications with `deno task watch` during development or `deno task start` in production", 464 ); 465 } catch (error) { 466 console.error("Error preparing agent for Bluesky:", error); 467 throw error; 468 } 469} 470 471// Run the function when this file is executed directly 472if (import.meta.main) { 473 await mount(); 474}