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}