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_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}