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