fix: hide system-reminder blocks in backfill + literal tag rendering (#935)
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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]");
|
||||
}
|
||||
|
||||
68
src/tests/cli/backfill-system-reminder.test.ts
Normal file
68
src/tests/cli/backfill-system-reminder.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
21
src/tests/cli/userMessageRich.test.ts
Normal file
21
src/tests/cli/userMessageRich.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user