feat: add /compact command (#278)
Co-authored-by: cpacker <packercharles@gmail.com> Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
5
bun.lock
5
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=="],
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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+/);
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user