a tool to help your Letta AI agents navigate bluesky
1import { client } from "./utils/messageAgent.ts"; 2import { submitAutonomyDeclarationRecord } from "./utils/declaration.ts"; 3import { blueskyProtocolsValue } from "./memories/blueskyProtocolsValue.ts"; 4 5await submitAutonomyDeclarationRecord(); 6 7/** 8 * Memory block configurations for the Bluesky agent. 9 * These blocks will be created if they don't exist, or updated if they do. 10 */ 11const BLUESKY_MEMORY_BLOCKS = [ 12 { 13 label: "bluesky_operational_guide", 14 description: 15 "Operational rules for Bluesky platform behavior. Contains notification handling protocols, tool usage constraints, and spam prevention rules. Consult before processing any Bluesky notification or initiating platform actions.", 16 value: blueskyProtocolsValue, 17 limit: 20000, 18 }, 19]; 20 21/** 22 * Hardcoded tool names that should always be attached to the Bluesky agent. 23 * These are tools that already exist in the Letta registry (built-in or previously created). 24 */ 25const REQUIRED_TOOLS: string[] = [ 26 "archival_memory_insert", 27 "archival_memory_search", 28 "conversation_search", 29 "web_search", 30 "fetch_webpage", 31 "memory", 32]; 33 34/** 35 * Detects Python package imports in source code. 36 * Returns an array of package names that need to be installed. 37 */ 38function detectPipRequirements(sourceCode: string): Array<{ name: string }> { 39 const requirements: Array<{ name: string }> = []; 40 41 // Check for atproto import 42 if ( 43 sourceCode.includes("from atproto import") || 44 sourceCode.includes("import atproto") 45 ) { 46 requirements.push({ name: "atproto" }); 47 } 48 49 // Add more package detection patterns here as needed 50 // Example: if (sourceCode.includes("import requests")) { requirements.push({ name: "requests" }); } 51 52 return requirements; 53} 54 55/** 56 * Discovers Bluesky tools by scanning the tools/bluesky/ directory. 57 * Returns an array of tool names (without the .py extension). 58 */ 59async function discoverBlueskyTools(): Promise<string[]> { 60 const toolsDir = "./tools/bluesky"; 61 const tools: string[] = []; 62 63 try { 64 for await (const entry of Deno.readDir(toolsDir)) { 65 if (entry.isFile && entry.name.endsWith(".py")) { 66 // Extract tool name (remove .py extension) 67 const toolName = entry.name.slice(0, -3); 68 tools.push(toolName); 69 } 70 } 71 } catch (error) { 72 console.warn( 73 `Warning: Could not read tools directory (${toolsDir}):`, 74 error.message, 75 ); 76 } 77 78 return tools; 79} 80 81/** 82 * Prepares the Letta agent for Bluesky by ensuring all required memory blocks 83 * are present and configured correctly. If blocks exist, their content is updated. 84 * If blocks don't exist, they are created and attached to the agent. 85 */ 86export async function mount(): Promise<void> { 87 const agentId = Deno.env.get("LETTA_AGENT_ID"); 88 const agentName = Deno.env.get("LETTA_PROJECT_NAME"); 89 90 if (!agentId) { 91 console.error( 92 "LETTA_AGENT_ID not found in environment variables. make sure to run `Deno task setup` first, then update your `.env` file.", 93 ); 94 throw new Error("LETTA_AGENT_ID is required"); 95 } 96 97 console.log(`Preparing agent ${agentName} for Bluesky...`); 98 99 try { 100 // Get the current agent state 101 const agent = await client.agents.retrieve(agentId); 102 console.log(`Agent retrieved: ${agent.name}`); 103 104 // Get all existing blocks for this agent 105 const existingBlocks = await client.agents.blocks.list(agentId); 106 console.log(`Agent has ${existingBlocks.length} existing memory blocks`); 107 108 // Process each required memory block 109 for (const blockConfig of BLUESKY_MEMORY_BLOCKS) { 110 // Check if a block with this label already exists 111 const existingBlock = existingBlocks.find( 112 (block: any) => block.label === blockConfig.label, 113 ); 114 115 if (existingBlock && existingBlock.id) { 116 // Block exists - update its content 117 console.log(`Updating existing block: ${blockConfig.label}`); 118 await client.blocks.modify(existingBlock.id, { 119 value: blockConfig.value, 120 description: blockConfig.description, 121 limit: blockConfig.limit, 122 }); 123 console.log(`✓ Updated block: ${blockConfig.label}`); 124 } else { 125 // Block doesn't exist - create and attach it 126 console.log(`Creating new block: ${blockConfig.label}`); 127 const newBlock = await client.blocks.create({ 128 label: blockConfig.label, 129 description: blockConfig.description, 130 value: blockConfig.value, 131 limit: blockConfig.limit, 132 }); 133 console.log(`Created block with ID: ${newBlock.id}`); 134 135 // Attach the block to the agent 136 if (newBlock.id) { 137 await client.agents.blocks.attach(agentId, newBlock.id); 138 console.log(`✓ Attached block: ${blockConfig.label}`); 139 } else { 140 throw new Error(`Failed to create block: ${blockConfig.label}`); 141 } 142 } 143 } 144 145 // Configure tool environment variables for Bluesky credentials 146 console.log("Configuring tool environment variables..."); 147 148 const bskyUsername = Deno.env.get("BSKY_USERNAME"); 149 const bskyAppPassword = Deno.env.get("BSKY_APP_PASSWORD"); 150 const bskyServiceUrl = Deno.env.get("BSKY_SERVICE_URL"); 151 152 if (!bskyUsername || !bskyAppPassword) { 153 console.warn( 154 "Warning: BSKY_USERNAME or BSKY_APP_PASSWORD not found in environment variables. " + 155 "Bluesky tools may not function properly.", 156 ); 157 } 158 159 // Update agent with tool environment variables 160 await client.agents.modify(agentId, { 161 toolExecEnvironmentVariables: { 162 BSKY_USERNAME: bskyUsername || "", 163 BSKY_APP_PASSWORD: bskyAppPassword || "", 164 BSKY_SERVICE_URL: bskyServiceUrl || "https://bsky.social", 165 }, 166 }); 167 console.log("✓ Tool environment variables configured"); 168 169 // Configure and attach required Bluesky tools 170 console.log("\nConfiguring Bluesky tools..."); 171 172 // Discover tools from the filesystem 173 const discoveredTools = await discoverBlueskyTools(); 174 console.log( 175 `Discovered ${discoveredTools.length} tool(s) in tools/bluesky/`, 176 ); 177 console.log(`Hardcoded required tools: ${REQUIRED_TOOLS.length}`); 178 179 if (discoveredTools.length === 0 && REQUIRED_TOOLS.length === 0) { 180 console.log("No tools configured."); 181 console.log( 182 "Add .py files to tools/bluesky/ or update REQUIRED_TOOLS array.", 183 ); 184 } 185 186 // Get currently attached tools 187 const attachedTools = await client.agents.tools.list(agentId); 188 const attachedToolNames = attachedTools.map((tool: any) => tool.name); 189 console.log(`Agent has ${attachedTools.length} tools currently attached`); 190 191 let toolsAttached = 0; 192 let toolsCreated = 0; 193 const missingTools: string[] = []; 194 195 // Create a user-level client for tool operations 196 // Tools are user-level resources, not project-scoped 197 const { LettaClient } = await import("@letta-ai/letta-client"); 198 const userLevelClient = new LettaClient({ 199 token: Deno.env.get("LETTA_API_KEY"), 200 }); 201 202 // First, process hardcoded required tools 203 for (const toolName of REQUIRED_TOOLS) { 204 try { 205 // Check if already attached 206 if (attachedToolNames.includes(toolName)) { 207 console.log(`✓ Required tool already attached: ${toolName}`); 208 continue; 209 } 210 211 // Search for the tool in the global registry 212 const existingTools = await userLevelClient.tools.list({ 213 name: toolName, 214 }); 215 216 if (existingTools.length > 0) { 217 const tool = existingTools[0]; 218 if (tool.id) { 219 await client.agents.tools.attach(agentId, tool.id); 220 console.log(`✓ Attached required tool: ${toolName}`); 221 toolsAttached++; 222 } 223 } else { 224 console.warn(`⚠ Required tool not found in registry: ${toolName}`); 225 missingTools.push(toolName); 226 } 227 } catch (error: any) { 228 console.warn( 229 `⚠ Error processing required tool ${toolName}:`, 230 error.message, 231 ); 232 missingTools.push(toolName); 233 } 234 } 235 236 // Then, process tools discovered from files 237 for (const toolFileName of discoveredTools) { 238 const toolFilePath = `./tools/bluesky/${toolFileName}.py`; 239 240 try { 241 const toolSource = await Deno.readTextFile(toolFilePath); 242 243 // Detect required pip packages from the source code 244 const pipRequirements = detectPipRequirements(toolSource); 245 if (pipRequirements.length > 0) { 246 console.log( 247 ` Detected pip requirements for ${toolFileName}: ${ 248 pipRequirements.map((r) => r.name).join(", ") 249 }`, 250 ); 251 } 252 253 // Try to create or find the tool 254 let tool; 255 let wasCreated = false; 256 257 try { 258 // Attempt to create the tool - Letta will extract the function name from docstring 259 const createParams: any = { 260 sourceCode: toolSource, 261 }; 262 263 // Add pip requirements if any were detected 264 if (pipRequirements.length > 0) { 265 createParams.pipRequirements = pipRequirements; 266 } 267 268 tool = await userLevelClient.tools.create(createParams); 269 wasCreated = true; 270 } catch (createError: any) { 271 // Tool might already exist - try to find it 272 // The error doesn't give us the tool name, so we'll need to parse it from the source 273 const errorBody = typeof createError.body === "string" 274 ? createError.body 275 : JSON.stringify(createError.body || ""); 276 277 if ( 278 createError.statusCode === 409 || 279 errorBody.includes("already exists") 280 ) { 281 // Extract function name from Python source using regex 282 const funcMatch = toolSource.match(/^def\s+(\w+)\s*\(/m); 283 if (funcMatch) { 284 const functionName = funcMatch[1]; 285 const existingTools = await userLevelClient.tools.list({ 286 name: functionName, 287 }); 288 if (existingTools.length > 0) { 289 tool = existingTools[0]; 290 } 291 } 292 } 293 294 // If we still don't have the tool, rethrow the error 295 if (!tool) { 296 throw createError; 297 } 298 } 299 300 const toolName = tool.name || toolFileName; 301 302 // Check if tool is already attached to the agent 303 if (attachedToolNames.includes(toolName)) { 304 console.log(`✓ Tool already attached: ${toolName}`); 305 continue; 306 } 307 308 // Attach the tool to the agent 309 if (tool.id) { 310 await client.agents.tools.attach(agentId, tool.id); 311 if (wasCreated) { 312 console.log( 313 `✓ Created and attached tool: ${toolName} (from ${toolFileName}.py)`, 314 ); 315 toolsCreated++; 316 } else { 317 console.log( 318 `✓ Attached existing tool: ${toolName} (from ${toolFileName}.py)`, 319 ); 320 toolsAttached++; 321 } 322 } 323 } catch (error: any) { 324 console.warn(`⚠ Error processing tool ${toolFileName}:`, error.message); 325 console.warn(` Attempted path: ${toolFilePath}`); 326 missingTools.push(toolFileName); 327 } 328 } 329 330 // Summary 331 console.log("\n=== Tool Configuration Summary ==="); 332 console.log(`Attached existing tools: ${toolsAttached}`); 333 console.log(`Created new tools: ${toolsCreated}`); 334 console.log( 335 `Total required tools: ${REQUIRED_TOOLS.length + discoveredTools.length}`, 336 ); 337 338 if (missingTools.length > 0) { 339 console.log(`\n⚠ Missing tools:`); 340 missingTools.forEach((tool) => { 341 if (REQUIRED_TOOLS.includes(tool)) { 342 console.log(` - ${tool} (required tool - not found in registry)`); 343 } else { 344 console.log(` - ${tool} (from tools/bluesky/${tool}.py)`); 345 } 346 }); 347 console.log( 348 "\nFor file-based tools, see tools/README.md for instructions.", 349 ); 350 console.log( 351 "For required tools, ensure they exist in your Letta registry.", 352 ); 353 } 354 355 console.log("\n✓ Agent successfully prepared for Bluesky"); 356 console.log( 357 "✓ start checking for notifications with `deno task watch` during development or `deno task start` in production", 358 ); 359 } catch (error) { 360 console.error("Error preparing agent for Bluesky:", error); 361 throw error; 362 } 363} 364 365// Run the function when this file is executed directly 366if (import.meta.main) { 367 await mount(); 368}