fix: hide system-reminder blocks in backfill + literal tag rendering (#935)

This commit is contained in:
Charles Packer
2026-02-12 14:27:49 -08:00
committed by GitHub
parent 28e67dadae
commit fa783fd2c8
4 changed files with 120 additions and 69 deletions

View File

@@ -49,7 +49,7 @@ const COMPACT_PAD = 1;
* System-reminder blocks are identified by <system-reminder>...</system-reminder> tags.
* Returns array of { text, isSystemReminder } objects in order.
*/
function splitSystemReminderBlocks(
export function splitSystemReminderBlocks(
text: string,
): Array<{ text: string; isSystemReminder: boolean }> {
const blocks: Array<{ text: string; isSystemReminder: boolean }> = [];
@@ -69,6 +69,17 @@ function splitSystemReminderBlocks(
break;
}
// Find the closing tag
const closeIdx = remaining.indexOf(tagClose, openIdx);
if (closeIdx === -1) {
// Malformed/incomplete tag - treat the whole remainder as literal user text.
const literal = remaining.trim();
if (literal) {
blocks.push({ text: literal, isSystemReminder: false });
}
break;
}
// Content before the tag is user content
if (openIdx > 0) {
const before = remaining.slice(0, openIdx).trim();
@@ -77,17 +88,6 @@ function splitSystemReminderBlocks(
}
}
// Find the closing tag
const closeIdx = remaining.indexOf(tagClose, openIdx);
if (closeIdx === -1) {
// Malformed - no closing tag, treat rest as system-reminder
blocks.push({
text: remaining.slice(openIdx).trim(),
isSystemReminder: true,
});
break;
}
// Extract the full system-reminder block (including tags)
const sysBlock = remaining.slice(openIdx, closeIdx + tagClose.length);
blocks.push({ text: sysBlock, isSystemReminder: true });

View File

@@ -41,41 +41,16 @@ function normalizeLineEndings(s: string): string {
return s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}
/**
* Truncate system-reminder content while preserving opening/closing tags.
* Removes the middle content and replaces with [...] to keep the message compact
* but with proper tag structure.
*/
function truncateSystemReminder(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
const openIdx = text.indexOf(SYSTEM_REMINDER_OPEN);
const closeIdx = text.lastIndexOf(SYSTEM_REMINDER_CLOSE);
if (openIdx === -1 || closeIdx === -1 || closeIdx <= openIdx) {
// Malformed, just use regular clip
return clip(text, maxLength);
}
const openEnd = openIdx + SYSTEM_REMINDER_OPEN.length;
const ellipsis = "\n...\n";
// Calculate available space for content (split between start and end)
const overhead =
SYSTEM_REMINDER_OPEN.length +
SYSTEM_REMINDER_CLOSE.length +
ellipsis.length;
const availableContent = maxLength - overhead;
if (availableContent <= 0) {
// Not enough space, just show tags with ellipsis
return `${SYSTEM_REMINDER_OPEN}${ellipsis}${SYSTEM_REMINDER_CLOSE}`;
}
const halfContent = Math.floor(availableContent / 2);
const contentStart = text.slice(openEnd, openEnd + halfContent);
const contentEnd = text.slice(closeIdx - halfContent, closeIdx);
return `${SYSTEM_REMINDER_OPEN}${contentStart}${ellipsis}${contentEnd}${SYSTEM_REMINDER_CLOSE}`;
function removeSystemReminderBlocks(text: string): string {
return text
.replace(
new RegExp(
`${SYSTEM_REMINDER_OPEN}[\\s\\S]*?${SYSTEM_REMINDER_CLOSE}`,
"g",
),
"",
)
.trim();
}
/**
@@ -119,25 +94,16 @@ function renderAssistantContentParts(
return out;
}
/**
* Check if text is purely a system-reminder block (no user content before/after).
*/
function isOnlySystemReminder(text: string): boolean {
const trimmed = text.trim();
return (
trimmed.startsWith(SYSTEM_REMINDER_OPEN) &&
trimmed.endsWith(SYSTEM_REMINDER_CLOSE)
);
}
function renderUserContentParts(
parts: string | LettaUserMessageContentUnion[],
): string {
// UserContent can be a string or an array of text OR image parts
// Pure system-reminder parts are truncated (middle) to preserve tags
// Mixed content or user text uses simple end truncation
// UserContent can be a string or an array of text OR image parts.
// Backfill should hide system-reminder blocks entirely.
// Parts are joined with newlines so each appears as a separate line
if (typeof parts === "string") return parts;
if (typeof parts === "string") {
const normalized = normalizeLineEndings(parts);
return clip(removeSystemReminderBlocks(normalized), CLIP_CHAR_LIMIT_TEXT);
}
const rendered: string[] = [];
for (const p of parts) {
@@ -145,13 +111,9 @@ function renderUserContentParts(
const text = p.text || "";
// Normalize line endings (\r\n and \r -> \n) to prevent terminal garbling
const normalized = normalizeLineEndings(text);
if (isOnlySystemReminder(normalized)) {
// Pure system-reminder: truncate middle to preserve tags
rendered.push(truncateSystemReminder(normalized, CLIP_CHAR_LIMIT_TEXT));
} else {
// User content or mixed: simple end truncation
rendered.push(clip(normalized, CLIP_CHAR_LIMIT_TEXT));
}
const withoutSystemReminders = removeSystemReminderBlocks(normalized);
if (!withoutSystemReminders) continue;
rendered.push(clip(withoutSystemReminders, CLIP_CHAR_LIMIT_TEXT));
} else if (p.type === "image") {
rendered.push("[Image]");
}

View File

@@ -0,0 +1,68 @@
import { describe, expect, test } from "bun:test";
import type { Message } from "@letta-ai/letta-client/resources/agents/messages";
import { createBuffers } from "../../cli/helpers/accumulator";
import { backfillBuffers } from "../../cli/helpers/backfill";
import { SYSTEM_REMINDER_CLOSE, SYSTEM_REMINDER_OPEN } from "../../constants";
function userMessage(
id: string,
content: string | Array<{ type: "text"; text: string }>,
): Message {
return {
id,
message_type: "user_message",
content,
} as unknown as Message;
}
describe("backfill system-reminder handling", () => {
test("hides pure system-reminder content parts", () => {
const buffers = createBuffers();
const history = [
userMessage("u1", [
{
type: "text",
text: `${SYSTEM_REMINDER_OPEN}\nInjected context\n${SYSTEM_REMINDER_CLOSE}`,
},
{ type: "text", text: "Real user message" },
]),
];
backfillBuffers(buffers, history);
const line = buffers.byId.get("u1");
expect(line?.kind).toBe("user");
expect(line && "text" in line ? line.text : "").toBe("Real user message");
});
test("removes system-reminder blocks from string content while preserving user text", () => {
const buffers = createBuffers();
const history = [
userMessage(
"u2",
`${SYSTEM_REMINDER_OPEN}\nInjected context\n${SYSTEM_REMINDER_CLOSE}\n\nKeep this text`,
),
];
backfillBuffers(buffers, history);
const line = buffers.byId.get("u2");
expect(line?.kind).toBe("user");
expect(line && "text" in line ? line.text : "").toBe("Keep this text");
});
test("drops user rows that are only system-reminder content", () => {
const buffers = createBuffers();
const history = [
userMessage(
"u3",
`${SYSTEM_REMINDER_OPEN}\nInjected context\n${SYSTEM_REMINDER_CLOSE}`,
),
];
backfillBuffers(buffers, history);
expect(buffers.byId.get("u3")).toBeUndefined();
expect(buffers.order).toHaveLength(0);
});
});

View File

@@ -0,0 +1,21 @@
import { describe, expect, test } from "bun:test";
import { splitSystemReminderBlocks } from "../../cli/components/UserMessageRich";
describe("splitSystemReminderBlocks", () => {
test("treats unmatched system-reminder opener as literal user text", () => {
const text = "like the <system-reminder> etc included.";
const blocks = splitSystemReminderBlocks(text);
expect(blocks).toEqual([{ text, isSystemReminder: false }]);
});
test("still detects well-formed system-reminder blocks", () => {
const blocks = splitSystemReminderBlocks(
"before\n<system-reminder>\ncontext\n</system-reminder>\nafter",
);
expect(blocks.some((b) => b.isSystemReminder)).toBe(true);
expect(blocks.some((b) => b.text.includes("before"))).toBe(true);
expect(blocks.some((b) => b.text.includes("after"))).toBe(true);
});
});