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}