diff --git a/changelog.d/fixes/15037-lite-current-turn.md b/changelog.d/fixes/15037-lite-current-turn.md new file mode 100644 index 00000000000..13194aaff93 --- /dev/null +++ b/changelog.d/fixes/15037-lite-current-turn.md @@ -0,0 +1 @@ +- **fix(compression):** Lite no longer truncates the results of the latest tool calls, so OpenAI-format agents (Cursor IDE, Kilo/OpenCode) stop re-reading a file whose content was cut to 2000 characters every time ([#15037](https://github.com/diegosouzapw/OmniRoute/pull/15037)). diff --git a/open-sse/services/compression/lite.ts b/open-sse/services/compression/lite.ts index e4f270339d2..fcefe77c369 100644 --- a/open-sse/services/compression/lite.ts +++ b/open-sse/services/compression/lite.ts @@ -154,8 +154,14 @@ export function compressToolResults( } { if (!body.messages) return { body, applied: false }; const MAX_TOOL_LENGTH = resolveLiteMaxToolLength(options.maxToolLength); + // Results of the latest tool calls (after the last assistant message) are + // what the model asked for this turn. Cutting them makes it re-read the same + // file forever, since every re-read is cut again. + const lastAssistant = body.messages.findLastIndex((msg) => msg.role === "assistant"); + const currentTurnStart = lastAssistant === -1 ? body.messages.length : lastAssistant + 1; let applied = false; - const messages = body.messages.map((msg) => { + const messages = body.messages.map((msg, index) => { + if (index >= currentTurnStart) return msg; if (msg.role !== "tool" || typeof msg.content !== "string") return msg; if (msg.content.length <= MAX_TOOL_LENGTH) return msg; applied = true; diff --git a/tests/unit/compression/lite-current-turn.test.ts b/tests/unit/compression/lite-current-turn.test.ts new file mode 100644 index 00000000000..074f20037d0 --- /dev/null +++ b/tests/unit/compression/lite-current-turn.test.ts @@ -0,0 +1,87 @@ +/** + * Lite must not truncate the results of the latest tool calls (the tool + * messages after the last assistant message). The model asked for that content + * this turn; cutting it to 2000 chars left it re-reading the same file forever, + * because every re-read was cut (or deduped against the cut copy) again. + * Run: node --import tsx/esm --test tests/unit/compression/lite-current-turn.test.ts + */ + +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +import { compressToolResults } from "../../../open-sse/services/compression/lite.ts"; +import { applyCompression } from "../../../open-sse/services/compression/strategySelector.ts"; + +const FILE = Array.from({ length: 400 }, (_, i) => `${i + 1}: def metric_${i}(x): return x`).join( + "\n" +); + +type Msg = { role: string; content: string | null; [key: string]: unknown }; + +function readCall(id: string): Msg { + return { + role: "assistant", + content: null, + tool_calls: [ + { id, type: "function", function: { name: "read", arguments: '{"filePath":"/s.py"}' } }, + ], + }; +} + +describe("lite tool-result truncation and the current turn", () => { + it("keeps the newest tool result whole", () => { + const { body } = compressToolResults({ + messages: [ + { role: "user", content: "read it" }, + readCall("c1"), + { role: "tool", tool_call_id: "c1", content: FILE }, + ], + }); + assert.equal(body.messages![2].content, FILE); + }); + + it("still truncates tool results from earlier turns", () => { + const { body, applied } = compressToolResults({ + messages: [ + { role: "user", content: "read it" }, + readCall("c1"), + { role: "tool", tool_call_id: "c1", content: FILE }, + readCall("c2"), + { role: "tool", tool_call_id: "c2", content: FILE }, + ], + }); + assert.equal(applied, true); + assert.match(String(body.messages![2].content), /\.\.\.\[truncated\]$/); + assert.equal(body.messages![4].content, FILE); + }); + + it("keeps a re-read whole through the default session-dedup + lite pipeline", () => { + const result = applyCompression( + { + messages: [ + { role: "user", content: "read it" }, + readCall("c1"), + { role: "tool", tool_call_id: "c1", content: FILE }, + readCall("c2"), + { role: "tool", tool_call_id: "c2", content: FILE }, + ], + }, + "stacked", + { + config: { + enabled: true, + defaultMode: "off", + autoTriggerTokens: 0, + cacheMinutes: 5, + preserveSystemPrompt: true, + comboOverrides: {}, + engines: {}, + activeComboId: null, + stackedPipeline: [{ engine: "session-dedup" }, { engine: "lite" }], + }, + } + ); + const messages = result.body.messages as Msg[]; + assert.equal(messages[4].content, FILE); + }); +});