a tool to help your Letta AI agents navigate bluesky
1import Letta from "@letta-ai/letta-client";
2import { agentContext } from "./agentContext.ts";
3// Helper function to format tool arguments as inline key-value pairs
4const formatArgsInline = (args: unknown, maxValueLength = 50): string => {
5 try {
6 const parsed = typeof args === "string" ? JSON.parse(args) : args;
7 if (typeof parsed !== "object" || parsed === null) {
8 return String(parsed);
9 }
10 return Object.entries(parsed)
11 .map(([key, value]) => {
12 let valueStr = typeof value === "object"
13 ? JSON.stringify(value)
14 : String(value);
15 // Truncate long values at word boundaries
16 if (valueStr.length > maxValueLength) {
17 const truncated = valueStr.slice(0, maxValueLength);
18 const lastSpace = truncated.lastIndexOf(" ");
19 // If we found a space in the last 30% of the truncated string, use it
20 valueStr = lastSpace > maxValueLength * 0.7
21 ? truncated.slice(0, lastSpace) + "..."
22 : truncated + "...";
23 }
24 return `${key}=${valueStr}`;
25 })
26 .join(", ");
27 } catch {
28 return String(args);
29 }
30};
31
32// Helper function to truncate long strings to 500 characters
33const truncateString = (str: string, maxLength = 140): string => {
34 if (str.length <= maxLength) {
35 return str;
36 }
37 return `${str.slice(0, maxLength)}... (truncated, ${str.length} total chars)`;
38};
39
40// Helper function to extract tool return value from wrapper structure
41const extractToolReturn = (toolReturns: unknown): string => {
42 try {
43 // If it's already a string, return it
44 if (typeof toolReturns === "string") {
45 return toolReturns;
46 }
47
48 // If it's an array, extract the tool_return from first element
49 if (Array.isArray(toolReturns) && toolReturns.length > 0) {
50 const firstReturn = toolReturns[0];
51 if (
52 typeof firstReturn === "object" &&
53 firstReturn !== null &&
54 "tool_return" in firstReturn
55 ) {
56 const toolReturn = firstReturn.tool_return;
57 // If tool_return is already a string, return it
58 if (typeof toolReturn === "string") {
59 return toolReturn;
60 }
61 // Otherwise stringify it
62 return JSON.stringify(toolReturn);
63 }
64 }
65
66 // Fallback: return stringified version of the whole thing
67 return JSON.stringify(toolReturns);
68 } catch {
69 return String(toolReturns);
70 }
71};
72
73// Helper function to select important params for logging
74const selectImportantParams = (args: unknown): unknown => {
75 try {
76 const parsed = typeof args === "string" ? JSON.parse(args) : args;
77 if (typeof parsed !== "object" || parsed === null) {
78 return parsed;
79 }
80
81 const entries = Object.entries(parsed);
82
83 // Filter out URIs/DIDs and very long values
84 const filtered = entries.filter(([key, value]) => {
85 const str = String(value);
86 // Skip AT URIs and DIDs
87 if (str.includes("at://") || str.includes("did:plc:")) return false;
88 // Skip very long values
89 if (str.length > 60) return false;
90 return true;
91 });
92
93 // Take first 3, or first entry if none pass filters
94 const selected = filtered.length > 0
95 ? filtered.slice(0, 3)
96 : entries.slice(0, 1);
97
98 return Object.fromEntries(selected);
99 } catch {
100 return args;
101 }
102};
103
104// Helper function to format tool response for logging
105const formatToolResponse = (returnValue: string): string => {
106 try {
107 // Try to parse as JSON - handle both JSON and Python dict syntax
108 let parsed;
109 try {
110 // First try standard JSON
111 parsed = JSON.parse(returnValue);
112 } catch {
113 // Try to parse Python-style dict (with single quotes and None/True/False)
114 const pythonToJson = returnValue
115 .replace(/'/g, '"')
116 .replace(/\bNone\b/g, "null")
117 .replace(/\bTrue\b/g, "true")
118 .replace(/\bFalse\b/g, "false");
119 parsed = JSON.parse(pythonToJson);
120 }
121
122 // Handle arrays - show count and sample first item
123 if (Array.isArray(parsed)) {
124 const count = parsed.length;
125 if (count === 0) return "[]";
126
127 const firstItem = parsed[0];
128 if (typeof firstItem === "object" && firstItem !== null) {
129 // Format first item's key fields (use 30 for samples to keep concise)
130 const sample = formatArgsInline(firstItem, 30);
131 return `[${count} items] ${sample}`;
132 }
133 return `[${count} items]`;
134 }
135
136 // If parsed successfully and it's an object, format as key=value pairs
137 if (typeof parsed === "object" && parsed !== null) {
138 return `(${formatArgsInline(parsed, 50)})`;
139 }
140
141 // If it's a primitive, return the original string
142 return returnValue;
143 } catch {
144 // If parsing fails, return as-is (it's a simple string)
145 return returnValue;
146 }
147};
148
149export const client = new Letta({
150 apiKey: Deno.env.get("LETTA_API_KEY"),
151 // @ts-ignore: Letta SDK type definition might be slightly off or expecting different casing
152 projectId: Deno.env.get("LETTA_PROJECT_ID"),
153});
154
155export const messageAgent = async (prompt: string) => {
156 const agent = Deno.env.get("LETTA_AGENT_ID");
157
158 if (agent) {
159 const reachAgent = await client.agents.messages.stream(agent, {
160 messages: [
161 {
162 role: "system",
163 content: prompt,
164 },
165 ],
166 stream_tokens: true,
167 });
168
169 let lastToolName = "";
170
171 for await (const response of reachAgent) {
172 if (response.message_type === "reasoning_message") {
173 // console.log(`💭 reasoning…`);
174 } else if (response.message_type === "assistant_message") {
175 console.log(`💬 ${agentContext.agentBskyName}: ${response.content}`);
176 } else if (response.message_type === "tool_call_message") {
177 // Use tool_call (singular) or tool_calls (both are objects, not arrays)
178 const toolCall = response.tool_call || response.tool_calls;
179 if (toolCall && toolCall.name) {
180 lastToolName = toolCall.name;
181 const importantParams = selectImportantParams(toolCall.arguments);
182 const formattedArgs = formatArgsInline(importantParams);
183 console.log(`🔧 tool called: ${toolCall.name} (${formattedArgs})`);
184 }
185 } else if (response.message_type === "tool_return_message") {
186 const extractedReturn = extractToolReturn(response.tool_returns);
187 const formattedResponse = formatToolResponse(extractedReturn);
188
189 // Determine separator based on format
190 const separator = formattedResponse.startsWith("(") ? " " : ": ";
191 const logMessage = `↩️ tool response: ${lastToolName}${separator}${formattedResponse}`;
192
193 console.log(truncateString(logMessage, 300));
194 } else if (response.message_type === "usage_statistics") {
195 console.log(`🔢 total steps: ${response.step_count}`);
196 } else if (response.message_type === "hidden_reasoning_message") {
197 console.log(`hidden reasoning…`);
198 }
199 }
200 } else {
201 console.log(
202 "🔹 Letta agent ID was not a set variable, skipping notification processing…",
203 );
204 }
205};