diff --git a/bun.lock b/bun.lock index 3fc50f8..741eb46 100644 --- a/bun.lock +++ b/bun.lock @@ -1,11 +1,10 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "@letta-ai/letta-code", "dependencies": { - "@letta-ai/letta-client": "^1.4.0", + "@letta-ai/letta-client": "1.6.1", "glob": "^13.0.0", "ink-link": "^5.0.0", "open": "^10.2.0", @@ -37,7 +36,7 @@ "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], - "@letta-ai/letta-client": ["@letta-ai/letta-client@1.4.0", "", {}, "sha512-7n59bRxjlcoRsDvE/jNDO1otqdF/m//DwGLKkx3uuf6wHwqt3qzJ7/zFVASl1by8oWsiEYUvJnQ634uA1+6q/g=="], + "@letta-ai/letta-client": ["@letta-ai/letta-client@1.6.1", "", {}, "sha512-kCRnEKpeTj3e1xqRd58xvoCp28p/wuJUptrIlJ8cT2GiYkrOESlKmp6lc3f246VusrowdGeB9hSXePXZgd7rAA=="], "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], diff --git a/package.json b/package.json index e088837..7f4f41b 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "access": "public" }, "dependencies": { - "@letta-ai/letta-client": "^1.4.0", + "@letta-ai/letta-client": "1.6.1", "glob": "^13.0.0", "ink-link": "^5.0.0", "open": "^10.2.0" diff --git a/src/cli/App.tsx b/src/cli/App.tsx index b18296e..9fb7f4b 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -1859,12 +1859,19 @@ export default function App({ // Exit after a brief delay to show the message setTimeout(() => process.exit(0), 500); } catch (error) { - const errorDetails = formatErrorDetails(error, agentId); + let errorOutput = formatErrorDetails(error, agentId); + + // Add helpful tip for summarization failures + if (errorOutput.includes("Summarization failed")) { + errorOutput += + "\n\nTip: Use /clear instead to clear the current message buffer."; + } + buffersRef.current.byId.set(cmdId, { kind: "command", id: cmdId, input: msg, - output: `Failed: ${errorDetails}`, + output: `Failed: ${errorOutput}`, phase: "finished", success: false, }); @@ -1986,6 +1993,91 @@ export default function App({ return { submitted: true }; } + // Special handling for /compact command - summarize conversation history + if (msg.trim() === "/compact") { + const cmdId = uid("cmd"); + buffersRef.current.byId.set(cmdId, { + kind: "command", + id: cmdId, + input: msg, + output: "Compacting conversation history...", + phase: "running", + }); + buffersRef.current.order.push(cmdId); + refreshDerived(); + + setCommandRunning(true); + + try { + const client = await getClient(); + // SDK types are out of date - compact returns CompactionResponse, not void + const result = (await client.agents.messages.compact( + agentId, + )) as unknown as { + num_messages_before: number; + num_messages_after: number; + summary: string; + }; + + // Format success message with before/after counts and summary + const outputLines = [ + `Compaction completed. Message buffer length reduced from ${result.num_messages_before} to ${result.num_messages_after}.`, + "", + `Summary: ${result.summary}`, + ]; + + // Update command with success + buffersRef.current.byId.set(cmdId, { + kind: "command", + id: cmdId, + input: msg, + output: outputLines.join("\n"), + phase: "finished", + success: true, + }); + refreshDerived(); + } catch (error) { + let errorOutput: string; + + // Check for summarization failure - format it cleanly + const apiError = error as { + status?: number; + error?: { detail?: string }; + }; + const detail = apiError?.error?.detail; + if ( + apiError?.status === 400 && + detail?.includes("Summarization failed") + ) { + // Clean format for this specific error, but preserve raw JSON + const cleanDetail = detail.replace(/^\d{3}:\s*/, ""); + const rawJson = JSON.stringify(apiError.error); + errorOutput = [ + `Request failed (code=400)`, + `Raw: ${rawJson}`, + `Detail: ${cleanDetail}`, + "", + "Tip: Use /clear instead to clear the current message buffer.", + ].join("\n"); + } else { + errorOutput = formatErrorDetails(error, agentId); + } + + buffersRef.current.byId.set(cmdId, { + kind: "command", + id: cmdId, + input: msg, + output: `Failed: ${errorOutput}`, + phase: "finished", + success: false, + }); + refreshDerived(); + } finally { + setCommandRunning(false); + } + return { submitted: true }; + } + // Special handling for /rename command - rename the agent if (msg.trim().startsWith("/rename")) { const parts = msg.trim().split(/\s+/); diff --git a/src/cli/commands/registry.ts b/src/cli/commands/registry.ts index 4dd0ac5..a984275 100644 --- a/src/cli/commands/registry.ts +++ b/src/cli/commands/registry.ts @@ -38,6 +38,13 @@ export const commands: Record = { return "Clearing messages..."; }, }, + "/compact": { + desc: "Summarize conversation history (compaction)", + handler: () => { + // Handled specially in App.tsx to access client and agent ID + return "Compacting conversation..."; + }, + }, "/logout": { desc: "Clear credentials and exit", handler: () => { diff --git a/src/cli/helpers/backfill.ts b/src/cli/helpers/backfill.ts index b8de77d..79c2110 100644 --- a/src/cli/helpers/backfill.ts +++ b/src/cli/helpers/backfill.ts @@ -19,6 +19,33 @@ function clip(s: string, limit: number): string { return s.length > limit ? `${s.slice(0, limit)}…` : s; } +/** + * Check if a user message is a compaction summary (system_alert with summary content). + * Returns the summary text if found, null otherwise. + */ +function extractCompactionSummary(text: string): string | null { + try { + const parsed = JSON.parse(text); + if ( + parsed.type === "system_alert" && + typeof parsed.message === "string" && + parsed.message.includes("prior messages have been hidden") + ) { + // Extract the summary part after the header + const summaryMatch = parsed.message.match( + /The following is a summary of the previous messages:\s*([\s\S]*)/, + ); + if (summaryMatch?.[1]) { + return summaryMatch[1].trim(); + } + return parsed.message; + } + } catch { + // Not JSON, not a compaction summary + } + return null; +} + function renderAssistantContentParts( parts: string | LettaAssistantMessageContentUnion[], ): string { @@ -71,11 +98,32 @@ export function backfillBuffers(buffers: Buffers, history: Message[]): void { switch (msg.message_type) { // user message - content parts may include text and image parts case "user_message": { + const rawText = renderUserContentParts(msg.content); + + // Check if this is a compaction summary message (system_alert with summary) + const compactionSummary = extractCompactionSummary(rawText); + if (compactionSummary) { + // Render as a synthetic tool call showing the compaction + const exists = buffers.byId.has(lineId); + buffers.byId.set(lineId, { + kind: "tool_call", + id: lineId, + toolCallId: `compaction-${lineId}`, + name: "Compact", + argsText: "messages[...]", + resultText: compactionSummary, + resultOk: true, + phase: "finished", + }); + if (!exists) buffers.order.push(lineId); + break; + } + const exists = buffers.byId.has(lineId); buffers.byId.set(lineId, { kind: "user", id: lineId, - text: renderUserContentParts(msg.content), + text: rawText, }); if (!exists) buffers.order.push(lineId); break;