fix(backend): word-boundary-safe tool-result truncation in lite compression mode (#8169) (#8239)

Co-authored-by: Probe Test <probe@example.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-23 00:46:01 -03:00
committed by GitHub
parent 917314156e
commit a37a2fe8c3
3 changed files with 107 additions and 1 deletions

View File

@@ -0,0 +1 @@
- fix(backend): word-boundary-safe tool-result truncation in lite compression mode (#8169)

View File

@@ -105,6 +105,47 @@ export function dedupSystemPrompt(
return { body: { ...body, messages }, applied };
}
// Adjust a hard cut index to the nearest whitespace within a small lookback/lookahead
// window, so a truncated tool result never garbles the word it lands in the middle of
// (#8169). Prefers backing off to the end of the previous word (keeps the result at or
// under the limit); if no whitespace precedes the cut within the window (e.g. the tail
// end of a very long unbroken run), looks forward to complete the current word instead.
// Falls back to the original hard cut index when neither direction finds a boundary.
const TOOL_TRUNCATION_LOOKBACK = 80;
function isWordChar(char: string | undefined): boolean {
return char !== undefined && /\S/.test(char);
}
function findWhitespaceBackward(content: string, cutIndex: number): number {
const windowStart = Math.max(0, cutIndex - TOOL_TRUNCATION_LOOKBACK);
for (let i = cutIndex; i > windowStart; i--) {
if (!isWordChar(content[i - 1])) return i - 1;
}
return -1;
}
function findWhitespaceForward(content: string, cutIndex: number): number {
const windowEnd = Math.min(content.length, cutIndex + TOOL_TRUNCATION_LOOKBACK);
for (let i = cutIndex; i < windowEnd; i++) {
if (!isWordChar(content[i])) return i;
}
return -1;
}
function backOffToWordBoundary(content: string, cutIndex: number): number {
const onWordBoundary = !isWordChar(content[cutIndex - 1]) || !isWordChar(content[cutIndex]);
if (onWordBoundary) return cutIndex;
const backward = findWhitespaceBackward(content, cutIndex);
if (backward !== -1) return backward;
const forward = findWhitespaceForward(content, cutIndex);
if (forward !== -1) return forward;
return cutIndex;
}
export function compressToolResults(body: ChatBody): {
body: ChatBody;
applied: boolean;
@@ -116,9 +157,10 @@ export function compressToolResults(body: ChatBody): {
if (msg.role !== "tool" || typeof msg.content !== "string") return msg;
if (msg.content.length <= MAX_TOOL_LENGTH) return msg;
applied = true;
const cutIndex = backOffToWordBoundary(msg.content, MAX_TOOL_LENGTH);
return {
...msg,
content: msg.content.slice(0, MAX_TOOL_LENGTH) + "\n...[truncated]",
content: msg.content.slice(0, cutIndex) + "\n...[truncated]",
};
});
return { body: { ...body, messages }, applied };

View File

@@ -0,0 +1,63 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { compressToolResults } from "../../open-sse/services/compression/lite.ts";
interface TestMessage {
role: string;
content: string;
}
interface TestChatBody {
messages: TestMessage[];
}
function toolBody(content: string): TestChatBody {
return { messages: [{ role: "tool", content }] };
}
function firstMessageContent(body: TestChatBody): string {
return body.messages[0].content;
}
test("#8169: lite compressToolResults must not cut a word in half", () => {
const prefix = "x".repeat(1990);
const word = "authentication"; // straddles the 2000-char cut point
const content = prefix + word + " rest of the message continues here.";
const { body: out } = compressToolResults(toolBody(content));
const resultContent = firstMessageContent(out as TestChatBody);
const cutPoint = resultContent.indexOf("\n...[truncated]");
assert.notEqual(cutPoint, -1);
const lastChar = resultContent[cutPoint - 1];
const charAfterWouldBe = content[cutPoint];
const isMidWord = /[a-zA-Z0-9]/.test(lastChar) && /[a-zA-Z0-9]/.test(charAfterWouldBe);
assert.equal(
isMidWord,
false,
`mid-word cut: "...${resultContent.slice(cutPoint - 20, cutPoint)}" next="${charAfterWouldBe}"`
);
});
test("#8169: compressToolResults still truncates content well over MAX_TOOL_LENGTH", () => {
const content = "word ".repeat(1000); // 5000 chars, plenty of whitespace boundaries
const { body: out, applied } = compressToolResults(toolBody(content));
const resultContent = firstMessageContent(out as TestChatBody);
assert.equal(applied, true);
assert.ok(resultContent.length < content.length);
assert.ok(resultContent.endsWith("\n...[truncated]"));
});
test("#8169: compressToolResults falls back to hard cut when no whitespace found in lookback window", () => {
const content = "a".repeat(2100); // no whitespace anywhere
const { body: out, applied } = compressToolResults(toolBody(content));
const resultContent = firstMessageContent(out as TestChatBody);
assert.equal(applied, true);
assert.ok(resultContent.endsWith("\n...[truncated]"));
});
test("#8169: compressToolResults leaves short tool content untouched", () => {
const content = "short content";
const { body: out, applied } = compressToolResults(toolBody(content));
const resultContent = firstMessageContent(out as TestChatBody);
assert.equal(applied, false);
assert.equal(resultContent, content);
});