import { client } from "./utils/messageAgent.ts"; import { submitAutonomyDeclarationRecord } from "./utils/declaration.ts"; import type { configAgentTool, memoryBlock, notifType } from "./utils/types.ts"; import { receivingLikesMemory } from "./memories/receivingLikes.ts"; import { receivingMentionsMemory } from "./memories/receivingMentions.ts"; import { receivingNewFollowersMemory } from "./memories/receivingNewFollower.ts"; import { receivingQuotesMemory } from "./memories/receivingQuotes.ts"; import { receivingRepliesMemory } from "./memories/receivingReplies.ts"; import { receivingRepostsMemory } from "./memories/receivingReposts.ts"; import { sendingLikesMemory } from "./memories/sendingLikes.ts"; import { sendingPostsMemory } from "./memories/sendingPosts.ts"; import { sendingProfileUpdatesMemory } from "./memories/sendingProfileUpdates.ts"; import { sendingQuotesMemory } from "./memories/sendingQuotes.ts"; import { sendingRelationshipUpdatesMemory } from "./memories/sendingRelationshipUpdates.ts"; import { sendingRepostsMemory } from "./memories/sendingReposts.ts"; import { archivalMemoryUseMemory } from "./memories/archivalMemoryUse.ts"; import { blueskyBaselineMemory } from "./memories/blueskyBaseline.ts"; import { blueskyReflectionMemory } from "./memories/blueskyReflection.ts"; import { blueskyProactiveMemory } from "./memories/blueskyProactive.ts"; import { maintainerContactMemory } from "./memories/maintainerContact.ts"; import { coreMemoryUseMemory } from "./memories/coreMemoryUse.ts"; import { searchingBlueskyMemory } from "./memories/searchingBluesky.ts"; import { toolUseMemory } from "./memories/toolUse.ts"; // Submit autonomy declaration record to the agent's PDS for transparency // This uses the studio.voyager.account.autonomy schema published by voyager.studio console.log("šŸ“‹ Creating AI autonomy declaration record..."); await submitAutonomyDeclarationRecord(); console.log(""); /** * Core memory blocks that are ALWAYS attached to the agent. * These provide foundational guidance regardless of configuration. */ const CORE_MEMORY_BLOCKS: memoryBlock[] = [ archivalMemoryUseMemory, blueskyBaselineMemory, blueskyReflectionMemory, blueskyProactiveMemory, maintainerContactMemory, coreMemoryUseMemory, searchingBlueskyMemory, toolUseMemory, ]; /** * Notification-specific memory blocks. * These are CONDITIONALLY attached based on the agent's supportedNotifTypes. */ const NOTIFICATION_MEMORY_BLOCKS: Partial> = { "like": receivingLikesMemory, "mention": receivingMentionsMemory, "follow": receivingNewFollowersMemory, "quote": receivingQuotesMemory, "reply": receivingRepliesMemory, "repost": receivingRepostsMemory, }; /** * Tool-specific memory blocks. * These are CONDITIONALLY attached based on the agent's supportedTools. * Only configurable tools need memory blocks (required tools are always available). */ const TOOL_MEMORY_BLOCKS: Partial> = { "create_bluesky_post": sendingPostsMemory, "like_bluesky_post": sendingLikesMemory, "quote_bluesky_post": sendingQuotesMemory, "repost_bluesky_post": sendingRepostsMemory, "update_bluesky_connection": sendingRelationshipUpdatesMemory, "update_bluesky_profile": sendingProfileUpdatesMemory, }; /** * Hardcoded tool names that should always be attached to the Bluesky agent. * These are tools that already exist in the Letta registry (built-in or previously created). */ const REQUIRED_TOOLS: string[] = [ "archival_memory_insert", "archival_memory_search", "conversation_search", "web_search", "fetch_webpage", "memory", ]; /** * Detects Python package imports in source code. * Returns an array of package names that need to be installed. */ function detectPipRequirements(sourceCode: string): Array<{ name: string }> { const requirements: Array<{ name: string }> = []; // Check for atproto import if ( sourceCode.includes("from atproto import") || sourceCode.includes("import atproto") ) { requirements.push({ name: "atproto" }); } // Add more package detection patterns here as needed // Example: if (sourceCode.includes("import requests")) { requirements.push({ name: "requests" }); } return requirements; } /** * Discovers Bluesky tools by scanning the tools/bluesky/ directory. * Returns an array of tool names (without the .py extension). */ async function discoverBlueskyTools(): Promise { const toolsDir = "./tools/bluesky"; const tools: string[] = []; try { for await (const entry of Deno.readDir(toolsDir)) { if (entry.isFile && entry.name.endsWith(".py")) { // Extract tool name (remove .py extension) const toolName = entry.name.slice(0, -3); tools.push(toolName); } } } catch (error) { console.warn( `Warning: Could not read tools directory (${toolsDir}):`, error instanceof Error ? error.message : String(error), ); } return tools; } /** * Prepares the Letta agent for Bluesky by ensuring all required memory blocks * are present and configured correctly. If blocks exist, their content is updated. * If blocks don't exist, they are created and attached to the agent. */ export async function mount(): Promise { const agentId = Deno.env.get("LETTA_AGENT_ID"); const agentName = Deno.env.get("LETTA_PROJECT_ID"); if (!agentId) { console.error( "LETTA_AGENT_ID not found in environment variables. make sure to run `Deno task setup` first, then update your `.env` file.", ); throw new Error("LETTA_AGENT_ID is required"); } console.log(`Preparing agent ${agentName} for Bluesky...`); try { // Get the current agent state const agent = await client.agents.retrieve(agentId); console.log(`Agent retrieved: ${agent.name}`); // Get all existing blocks for this agent const existingBlocksPage = await client.agents.blocks.list(agentId); const existingBlocks = existingBlocksPage.items; console.log(`Agent has ${existingBlocks.length} existing memory blocks`); // Build dynamic memory blocks array based on configuration console.log("Building memory block configuration..."); const { agentContext } = await import("./utils/agentContext.ts"); const memoryBlocksToProcess: memoryBlock[] = []; // 1. Always include core memory blocks memoryBlocksToProcess.push(...CORE_MEMORY_BLOCKS); console.log(`- Added ${CORE_MEMORY_BLOCKS.length} core memory blocks`); // 2. Add notification-specific blocks based on supportedNotifTypes let notifBlockCount = 0; for (const notifType of agentContext.supportedNotifTypes) { const notifBlock = NOTIFICATION_MEMORY_BLOCKS[notifType]; if (notifBlock) { memoryBlocksToProcess.push(notifBlock); notifBlockCount++; } } console.log( `- Added ${notifBlockCount} notification-specific memory blocks for: ${ agentContext.supportedNotifTypes.join(", ") }`, ); // 3. Add tool-specific blocks based on supportedTools (only configurable tools) let toolBlockCount = 0; for (const tool of agentContext.supportedTools) { // Type assertion needed because supportedTools includes both configurable and required tools const toolBlock = TOOL_MEMORY_BLOCKS[tool as configAgentTool]; if (toolBlock) { memoryBlocksToProcess.push(toolBlock); toolBlockCount++; } } console.log( `- Added ${toolBlockCount} tool-specific memory blocks`, ); console.log( `Total memory blocks to process: ${memoryBlocksToProcess.length}`, ); // Process each required memory block for (const blockConfig of memoryBlocksToProcess) { // Check if a block with this label already exists const existingBlock = existingBlocks.find( (block: any) => block.label === blockConfig.label, ); if (existingBlock && existingBlock.id) { // Block exists - update or preserve based on configuration if (agentContext.preserveAgentMemory) { console.log( `āœ“ Preserving existing block: ${blockConfig.label} (PRESERVE_MEMORY_BLOCKS=true)`, ); } else { console.log(`Updating existing block: ${blockConfig.label}`); await client.blocks.update(existingBlock.id, { value: blockConfig.value, description: blockConfig.description, limit: blockConfig.limit, }); console.log(`āœ“ Updated block: ${blockConfig.label}`); } } else { // Block doesn't exist - create and attach it console.log(`Creating new block: ${blockConfig.label}`); const newBlock = await client.blocks.create({ label: blockConfig.label, description: blockConfig.description, value: blockConfig.value, limit: blockConfig.limit, }); console.log(`Created block with ID: ${newBlock.id}`); // Attach the block to the agent if (newBlock.id) { await client.agents.blocks.attach(newBlock.id, { agent_id: agentId }); console.log(`āœ“ Attached block: ${blockConfig.label}`); } else { throw new Error(`Failed to create block: ${blockConfig.label}`); } } } // Configure tool environment variables for Bluesky credentials console.log("Configuring tool environment variables..."); const bskyUsername = Deno.env.get("BSKY_USERNAME"); const bskyAppPassword = Deno.env.get("BSKY_APP_PASSWORD"); const bskyServiceUrl = Deno.env.get("BSKY_SERVICE_URL"); if (!bskyUsername || !bskyAppPassword) { console.warn( "Warning: BSKY_USERNAME or BSKY_APP_PASSWORD not found in environment variables. " + "Bluesky tools may not function properly.", ); } // Update agent with tool environment variables await client.agents.update(agentId, { secrets: { BSKY_USERNAME: bskyUsername || "", BSKY_APP_PASSWORD: bskyAppPassword || "", BSKY_SERVICE_URL: bskyServiceUrl || "https://bsky.social", }, }); console.log("āœ“ Tool environment variables configured"); // Configure and attach required Bluesky tools console.log("\nConfiguring Bluesky tools..."); // Discover tools from the filesystem const discoveredTools = await discoverBlueskyTools(); console.log( `Discovered ${discoveredTools.length} tool(s) in tools/bluesky/`, ); console.log(`Hardcoded required tools: ${REQUIRED_TOOLS.length}`); if (discoveredTools.length === 0 && REQUIRED_TOOLS.length === 0) { console.log("No tools configured."); console.log( "Add .py files to tools/bluesky/ or update REQUIRED_TOOLS array.", ); } // Get currently attached tools const attachedToolsPage = await client.agents.tools.list(agentId); const attachedTools = attachedToolsPage.items; const attachedToolNames = attachedTools.map((tool: any) => tool.name); console.log(`Agent has ${attachedTools.length} tools currently attached`); let toolsAttached = 0; let toolsCreated = 0; const missingTools: string[] = []; // Create a user-level client for tool operations // Tools are user-level resources, not project-scoped const { default: Letta } = await import("@letta-ai/letta-client"); const userLevelClient = new Letta({ apiKey: Deno.env.get("LETTA_API_KEY"), }); // First, process hardcoded required tools for (const toolName of REQUIRED_TOOLS) { try { // Check if already attached if (attachedToolNames.includes(toolName)) { console.log(`āœ“ Required tool already attached: ${toolName}`); continue; } // Search for the tool in the global registry const existingToolsPage = await userLevelClient.tools.list({ name: toolName, }); const existingTools = existingToolsPage.items; if (existingTools.length > 0) { const tool = existingTools[0]; if (tool.id) { await client.agents.tools.attach(tool.id, { agent_id: agentId }); console.log(`āœ“ Attached required tool: ${toolName}`); toolsAttached++; } } else { console.warn(`⚠ Required tool not found in registry: ${toolName}`); missingTools.push(toolName); } } catch (error: any) { console.warn( `⚠ Error processing required tool ${toolName}:`, error.message, ); missingTools.push(toolName); } } // Then, process tools discovered from files for (const toolFileName of discoveredTools) { const toolFilePath = `./tools/bluesky/${toolFileName}.py`; try { const toolSource = await Deno.readTextFile(toolFilePath); // Detect required pip packages from the source code const pipRequirements = detectPipRequirements(toolSource); if (pipRequirements.length > 0) { console.log( ` Detected pip requirements for ${toolFileName}: ${ pipRequirements.map((r) => r.name).join(", ") }`, ); } // Try to create or find the tool let tool; let wasCreated = false; try { // Attempt to create the tool - Letta will extract the function name from docstring const createParams: any = { source_code: toolSource, }; // Add pip requirements if any were detected if (pipRequirements.length > 0) { createParams.pip_requirements = pipRequirements; } tool = await userLevelClient.tools.create(createParams); wasCreated = true; } catch (createError: any) { // Tool might already exist - try to find it // The error doesn't give us the tool name, so we'll need to parse it from the source const errorBody = typeof createError.body === "string" ? createError.body : JSON.stringify(createError.body || ""); if ( createError.statusCode === 409 || errorBody.includes("already exists") ) { // Extract function name from Python source using regex const funcMatch = toolSource.match(/^def\s+(\w+)\s*\(/m); if (funcMatch) { const functionName = funcMatch[1]; const existingToolsPage = await userLevelClient.tools.list({ name: functionName, }); const existingTools = existingToolsPage.items; if (existingTools.length > 0) { tool = existingTools[0]; } } } // If we still don't have the tool, rethrow the error if (!tool) { throw createError; } } const toolName = tool.name || toolFileName; // Check if tool is already attached to the agent if (attachedToolNames.includes(toolName)) { console.log(`āœ“ Tool already attached: ${toolName}`); continue; } // Attach the tool to the agent if (tool.id) { await client.agents.tools.attach(tool.id, { agent_id: agentId }); if (wasCreated) { console.log( `āœ“ Created and attached tool: ${toolName} (from ${toolFileName}.py)`, ); toolsCreated++; } else { console.log( `āœ“ Attached existing tool: ${toolName} (from ${toolFileName}.py)`, ); toolsAttached++; } } } catch (error: any) { console.warn(`⚠ Error processing tool ${toolFileName}:`, error.message); console.warn(` Attempted path: ${toolFilePath}`); missingTools.push(toolFileName); } } // Summary console.log("\n=== Tool Configuration Summary ==="); console.log(`Attached existing tools: ${toolsAttached}`); console.log(`Created new tools: ${toolsCreated}`); console.log( `Total required tools: ${REQUIRED_TOOLS.length + discoveredTools.length}`, ); if (missingTools.length > 0) { console.log(`\n⚠ Missing tools:`); missingTools.forEach((tool) => { if (REQUIRED_TOOLS.includes(tool)) { console.log(` - ${tool} (required tool - not found in registry)`); } else { console.log(` - ${tool} (from tools/bluesky/${tool}.py)`); } }); console.log( "\nFor file-based tools, see tools/README.md for instructions.", ); console.log( "For required tools, ensure they exist in your Letta registry.", ); } console.log("\nāœ“ Agent successfully prepared for Bluesky"); console.log( "āœ“ start checking for notifications with `deno task watch` during development or `deno task start` in production", ); } catch (error) { console.error("Error preparing agent for Bluesky:", error); throw error; } } // Run the function when this file is executed directly if (import.meta.main) { await mount(); }