feat: add /compact command (#278)

Co-authored-by: cpacker <packercharles@gmail.com>
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Sarah Wooders
2025-12-18 12:47:05 -08:00
committed by GitHub
parent 02aa8c38c6
commit 1087ccc2c2
5 changed files with 153 additions and 7 deletions

View File

@@ -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=="],

View File

@@ -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"

View File

@@ -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+/);

View File

@@ -38,6 +38,13 @@ export const commands: Record<string, Command> = {
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: () => {

View File

@@ -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;