import Letta from "@letta-ai/letta-client"; import { agentContext } from "./agentContext.ts"; // Helper function to format tool arguments as inline key-value pairs const formatArgsInline = (args: unknown, maxValueLength = 50): string => { try { const parsed = typeof args === "string" ? JSON.parse(args) : args; if (typeof parsed !== "object" || parsed === null) { return String(parsed); } return Object.entries(parsed) .map(([key, value]) => { let valueStr = typeof value === "object" ? JSON.stringify(value) : String(value); // Truncate long values at word boundaries if (valueStr.length > maxValueLength) { const truncated = valueStr.slice(0, maxValueLength); const lastSpace = truncated.lastIndexOf(" "); // If we found a space in the last 30% of the truncated string, use it valueStr = lastSpace > maxValueLength * 0.7 ? truncated.slice(0, lastSpace) + "..." : truncated + "..."; } return `${key}=${valueStr}`; }) .join(", "); } catch { return String(args); } }; // Helper function to truncate long strings to 500 characters const truncateString = (str: string, maxLength = 140): string => { if (str.length <= maxLength) { return str; } return `${str.slice(0, maxLength)}... (truncated, ${str.length} total chars)`; }; // Helper function to extract tool return value from wrapper structure const extractToolReturn = (toolReturns: unknown): string => { try { // If it's already a string, return it if (typeof toolReturns === "string") { return toolReturns; } // If it's an array, extract the tool_return from first element if (Array.isArray(toolReturns) && toolReturns.length > 0) { const firstReturn = toolReturns[0]; if ( typeof firstReturn === "object" && firstReturn !== null && "tool_return" in firstReturn ) { const toolReturn = firstReturn.tool_return; // If tool_return is already a string, return it if (typeof toolReturn === "string") { return toolReturn; } // Otherwise stringify it return JSON.stringify(toolReturn); } } // Fallback: return stringified version of the whole thing return JSON.stringify(toolReturns); } catch { return String(toolReturns); } }; // Helper function to select important params for logging const selectImportantParams = (args: unknown): unknown => { try { const parsed = typeof args === "string" ? JSON.parse(args) : args; if (typeof parsed !== "object" || parsed === null) { return parsed; } const entries = Object.entries(parsed); // Filter out URIs/DIDs and very long values const filtered = entries.filter(([key, value]) => { const str = String(value); // Skip AT URIs and DIDs if (str.includes("at://") || str.includes("did:plc:")) return false; // Skip very long values if (str.length > 60) return false; return true; }); // Take first 3, or first entry if none pass filters const selected = filtered.length > 0 ? filtered.slice(0, 3) : entries.slice(0, 1); return Object.fromEntries(selected); } catch { return args; } }; // Helper function to format tool response for logging const formatToolResponse = (returnValue: string): string => { try { // Try to parse as JSON - handle both JSON and Python dict syntax let parsed; try { // First try standard JSON parsed = JSON.parse(returnValue); } catch { // Try to parse Python-style dict (with single quotes and None/True/False) const pythonToJson = returnValue .replace(/'/g, '"') .replace(/\bNone\b/g, "null") .replace(/\bTrue\b/g, "true") .replace(/\bFalse\b/g, "false"); parsed = JSON.parse(pythonToJson); } // Handle arrays - show count and sample first item if (Array.isArray(parsed)) { const count = parsed.length; if (count === 0) return "[]"; const firstItem = parsed[0]; if (typeof firstItem === "object" && firstItem !== null) { // Format first item's key fields (use 30 for samples to keep concise) const sample = formatArgsInline(firstItem, 30); return `[${count} items] ${sample}`; } return `[${count} items]`; } // If parsed successfully and it's an object, format as key=value pairs if (typeof parsed === "object" && parsed !== null) { return `(${formatArgsInline(parsed, 50)})`; } // If it's a primitive, return the original string return returnValue; } catch { // If parsing fails, return as-is (it's a simple string) return returnValue; } }; export const client = new Letta({ apiKey: Deno.env.get("LETTA_API_KEY"), // @ts-ignore: Letta SDK type definition might be slightly off or expecting different casing projectId: Deno.env.get("LETTA_PROJECT_ID"), }); export const messageAgent = async (prompt: string) => { const agent = Deno.env.get("LETTA_AGENT_ID"); if (agent) { const reachAgent = await client.agents.messages.stream(agent, { messages: [ { role: "system", content: prompt, }, ], stream_tokens: true, }); let lastToolName = ""; for await (const response of reachAgent) { if (response.message_type === "reasoning_message") { // console.log(`💭 reasoning…`); } else if (response.message_type === "assistant_message") { console.log(`💬 ${agentContext.agentBskyName}: ${response.content}`); } else if (response.message_type === "tool_call_message") { // Use tool_call (singular) or tool_calls (both are objects, not arrays) const toolCall = response.tool_call || response.tool_calls; if (toolCall && toolCall.name) { lastToolName = toolCall.name; const importantParams = selectImportantParams(toolCall.arguments); const formattedArgs = formatArgsInline(importantParams); console.log(`🔧 tool called: ${toolCall.name} (${formattedArgs})`); } } else if (response.message_type === "tool_return_message") { const extractedReturn = extractToolReturn(response.tool_returns); const formattedResponse = formatToolResponse(extractedReturn); // Determine separator based on format const separator = formattedResponse.startsWith("(") ? " " : ": "; const logMessage = `↩️ tool response: ${lastToolName}${separator}${formattedResponse}`; console.log(truncateString(logMessage, 300)); } else if (response.message_type === "usage_statistics") { console.log(`🔢 total steps: ${response.step_count}`); } else if (response.message_type === "hidden_reasoning_message") { console.log(`hidden reasoning…`); } } } else { console.log( "🔹 Letta agent ID was not a set variable, skipping notification processing…", ); } };