import { describe, it, expect } from "vitest"; import { fold, FoldState, Tier, incompressibleTokens, cutBytes, cutBytesFromEnd, type AgentMessage } from "./fold.js"; import { byteLength, Estimator, fixedBytes, messagesBytes } from "./estimate.js"; import { limitsFor, DEFAULT_KEEP_FRACTION, DEFAULT_HEADROOM_TOKENS, DEFAULT_MAX_PROMPT_TOKENS, type PromptcapSettings } from "./limits.js"; const call = (id: string, name: string, args: Record, thinking?: string): AgentMessage => ({ role: "assistant", content: [ ...(thinking ? [{ type: "thinking", thinking, thinkingSignature: "sig" }] : []), { type: "toolCall", id, name, arguments: args }, ], }); const result = (id: string, name: string, text: string, isError = false): AgentMessage => ({ role: "toolResult", toolCallId: id, toolName: name, content: [{ type: "text", text }], isError, }); const user = (text: string): AgentMessage => ({ role: "user", content: [{ type: "text", text }] }); const prose = (text: string): AgentMessage => ({ role: "assistant", content: [{ type: "text", text }] }); /** A turn the provider cut off mid-thought: reasoning, and no call to reach it by. */ const truncated = (thinking: string, signature: string): AgentMessage => ({ role: "assistant", content: [{ type: "thinking", thinking, thinkingSignature: signature }], }); /** A conversation of `n` answered calls, each carrying a `size`-byte result. */ const conversation = (n: number, size: number): AgentMessage[] => { const messages: AgentMessage[] = [user("go")]; for (let i = 0; i < n; i++) { messages.push(call(`t${i}`, "read", { path: `/f${i}`, body: "x".repeat(300) }, `thought ${i}`)); messages.push(result(`t${i}`, "read", "o".repeat(size))); } return messages; }; /** A conversation of `n` answered calls carrying no reasoning. */ const plain = (n: number, size: number): AgentMessage[] => { const messages: AgentMessage[] = [user("go")]; for (let i = 0; i < n; i++) { const name = i % 3 === 0 ? "bash" : "read"; messages.push(call(`t${i}`, name, { path: `/f${i}` })); messages.push(result(`t${i}`, name, "o".repeat(size))); } return messages; }; const settings = (over: Partial = {}): PromptcapSettings => ({ enabled: true, perModel: {}, ...over, }); describe("fold", () => { it("leaves a prompt under the ceiling untouched", () => { const messages = conversation(3, 400); const before = JSON.parse(JSON.stringify(messages)); const out = fold(messages, 0, { ceiling: 1_000_000, lowWater: 500_000 }, new FoldState()); expect(messages).toEqual(before); expect(out.folded).toBe(0); }); it("folds the oldest calls first and stops at the low-water mark", () => { const messages = conversation(10, 4000); const total = Math.floor(messagesBytes(messages) / 4); const out = fold(messages, 0, { ceiling: Math.floor(total * 0.6), lowWater: Math.floor(total * 0.4) }, new FoldState()); expect(out.tokens).toBeLessThanOrEqual(Math.floor(total * 0.4)); // The newest call keeps its output; the oldest does not. expect(messages[2].content[0].text).toBe("[omitted: 4000B]"); expect(messages[20].content[0].text).toBe("o".repeat(4000)); }); it("leaves every argument exactly as it was, however big", () => { const body = "y".repeat(50_000); const edits = [{ oldText: "o".repeat(4000), newText: "n".repeat(4000) }]; const messages = [ user("go"), call("t0", "write", { path: "/a", body }), result("t0", "write", "ok".repeat(9000)), call("t1", "edit", { path: "/b", edits }), result("t1", "edit", "ok".repeat(9000)), ]; const state = new FoldState(); fold(messages, 0, { ceiling: 10, lowWater: 5 }, state); expect(state.tierOf("t0")).toBe(Tier.Folded); expect(messages[1].content[0].arguments).toEqual({ path: "/a", body }); expect(messages[3].content[0].arguments).toEqual({ path: "/b", edits }); }); it("keeps both ends of a folded error", () => { const failure = "HEAD".padEnd(1200, "-") + "TAIL".padStart(600, "-"); const messages = [ user("go"), call("t0", "bash", { cmd: "make" }), result("t0", "bash", failure, true), call("t1", "read", { path: "/a" }), result("t1", "read", "o".repeat(9000)), ]; const state = new FoldState(); fold(messages, 0, { ceiling: 700, lowWater: 600 }, state); expect(state.tierOf("t0")).toBe(Tier.Folded); const folded = messages[2].content[0].text; expect(folded.startsWith("HEAD")).toBe(true); expect(folded.endsWith("TAIL")).toBe(true); expect(folded).toContain("[omitted: 300B]"); }); it("never touches user or assistant prose, even when it alone overflows", () => { const long = "p".repeat(40_000); const messages = [user(long), prose(long), call("t0", "read", { p: "/a" }), result("t0", "read", "o".repeat(100))]; fold(messages, 0, { ceiling: 10, lowWater: 5 }, new FoldState()); expect(messages[0].content[0].text).toBe(long); expect(messages[1].content[0].text).toBe(long); }); it("never folds a call still awaiting its result", () => { const messages = [...conversation(6, 4000), call("pending", "bash", { cmd: "sleep 1" })]; fold(messages, 0, { ceiling: 10, lowWater: 5 }, new FoldState()); expect(messages[messages.length - 1].content[0].arguments).toEqual({ cmd: "sleep 1" }); }); it("drops thinking that belongs to a folded call but not after the last user message", () => { const messages = [ user("first"), call("t0", "read", { p: "/a" }, "old reasoning"), result("t0", "read", "o".repeat(9000)), user("second"), call("t1", "read", { p: "/b" }, "fresh reasoning"), result("t1", "read", "o".repeat(9000)), ]; fold(messages, 0, { ceiling: 10, lowWater: 5 }, new FoldState()); expect(messages[1].content[0].thinking).toBe(""); expect(messages[4].content[0].thinking).toBe("fresh reasoning"); }); it("drops the reasoning of a turn that made no call at all", () => { const messages = [ user("first"), call("t0", "read", { p: "/a" }), result("t0", "read", "o".repeat(9000)), truncated("cut off mid-thought", "s".repeat(9000)), user("second"), call("t1", "read", { p: "/b" }), result("t1", "read", "o".repeat(9000)), user("third"), ]; fold(messages, 0, { ceiling: 10, lowWater: 5 }, new FoldState()); expect(messages[3].content[0]).toEqual({ type: "thinking", thinking: "", thinkingSignature: "" }); }); it("keeps a callless turn's reasoning while the conversation still fits", () => { const messages = [ user("first"), call("t0", "read", { p: "/a" }), result("t0", "read", "o".repeat(9000)), truncated("cut off mid-thought", "s".repeat(9000)), user("second"), call("t1", "read", { p: "/b" }), result("t1", "read", "o".repeat(9000)), user("third"), ]; fold(messages, 0, { ceiling: 1_000_000, lowWater: 500_000 }, new FoldState()); expect(messages[3].content[0].thinking).toBe("cut off mid-thought"); }); it("leaves a callless turn's reasoning alone after the last user message", () => { const messages = [ user("first"), call("t0", "read", { p: "/a" }), result("t0", "read", "o".repeat(9000)), user("second"), truncated("fresh reasoning", "s".repeat(9000)), call("t1", "read", { p: "/b" }), result("t1", "read", "o".repeat(9000)), ]; fold(messages, 0, { ceiling: 10, lowWater: 5 }, new FoldState()); expect(messages[4].content[0].thinking).toBe("fresh reasoning"); }); it("counts a callless turn's reasoning as foldable in the floor", () => { const withReasoning = [ user("go"), call("t0", "read", { p: "/a" }), result("t0", "read", "o".repeat(400)), truncated("cut off mid-thought", "s".repeat(40_000)), call("t1", "read", { p: "/b" }), result("t1", "read", "o".repeat(400)), user("next"), ]; const without = withReasoning.filter((m) => m.content[0]?.type !== "thinking"); expect(incompressibleTokens(withReasoning, 0)).toBe(incompressibleTokens(without, 0)); }); it("keeps redacted thinking intact", () => { const messages = [ user("go"), { role: "assistant", content: [ { type: "thinking", thinking: "[Reasoning redacted]", thinkingSignature: "opaque", redacted: true }, { type: "toolCall", id: "t0", name: "read", arguments: { p: "/a" } }, ] }, result("t0", "read", "o".repeat(9000)), ]; fold(messages, 0, { ceiling: 10, lowWater: 5 }, new FoldState()); expect(messages[1].content[0].thinking).toBe("[Reasoning redacted]"); }); it("is monotonic: a call folded once stays folded when the budget widens", () => { const state = new FoldState(); const tight = conversation(10, 4000); fold(tight, 0, { ceiling: 100, lowWater: 50 }, state); expect(state.size).toBeGreaterThan(0); const roomy = conversation(10, 4000); const out = fold(roomy, 0, { ceiling: 10_000_000, lowWater: 5_000_000 }, state); expect(out.folded).toBe(10); expect(roomy[2].content[0].text).toBe("[omitted: 4000B]"); }); it("keeps the prefix byte-identical as the conversation grows", () => { const state = new FoldState(); const first = conversation(10, 4000); fold(first, 0, { ceiling: 4000, lowWater: 2000 }, state); const prefix = JSON.stringify(first.slice(0, 11)); const second = [...conversation(10, 4000).map((m) => JSON.parse(JSON.stringify(m)))]; second.push(call("t10", "read", { path: "/f10" }), result("t10", "read", "o".repeat(4000))); fold(second, 0, { ceiling: 4000, lowWater: 2000 }, state); expect(JSON.stringify(second.slice(0, 11))).toBe(prefix); }); it("reaches the incompressible floor and no further", () => { const messages = conversation(10, 4000); const floor = incompressibleTokens(messages, 0); const out = fold(messages, 0, { ceiling: 1, lowWater: 1 }, new FoldState()); expect(out.tokens).toBe(floor); }); it("drops the oldest calls outright, naming what went in one line", () => { const messages = plain(200, 500); const state = new FoldState(); fold(messages, 0, { ceiling: 100, lowWater: 50 }, state); expect(state.tierOf("t0")).toBe(Tier.Drop); expect(messages[1].content[0]).toEqual({ type: "text", text: "[dropped 50 earlier calls: read \u00d733, bash \u00d717]" }); }); it("keeps the newest calls whatever the budget, trimming them as before", () => { const messages = plain(200, 500); const state = new FoldState(); fold(messages, 0, { ceiling: 1, lowWater: 1 }, state); expect(state.tierOf("t49")).toBe(Tier.Drop); expect(state.tierOf("t50")).toBe(Tier.Folded); expect(state.tierOf("t199")).toBe(Tier.Folded); }); it("leaves neither half of a dropped call behind", () => { const messages = plain(200, 500); fold(messages, 0, { ceiling: 100, lowWater: 50 }, new FoldState()); const calls = new Set(); const answers = new Set(); for (const message of messages) { if (message.role === "toolResult") answers.add(message.toolCallId); for (const part of (message.content ?? [])) { if (part?.type === "toolCall") calls.add(part.id); } } expect(calls.has("t0")).toBe(false); expect(answers.has("t0")).toBe(false); expect(calls.has("t50")).toBe(true); // Every surviving result still has the call it answers, which is the // invariant that stopped anything below Drop from removing either half. expect([...answers].every((id) => calls.has(id))).toBe(true); }); it("never leaves two assistant turns adjacent when a turn is dropped", () => { const messages = plain(200, 500); fold(messages, 0, { ceiling: 100, lowWater: 50 }, new FoldState()); const roles = messages.map((m: AgentMessage) => m.role); const adjacent = roles.filter((role, i) => i > 0 && role === "assistant" && roles[i - 1] === "assistant"); expect(adjacent).toEqual([]); }); it("takes the reasoning with a call dropped on an earlier request", () => { const state = new FoldState(); const first = conversation(200, 500); first.push(user("next")); fold(first, 0, { ceiling: 100, lowWater: 50 }, state); // The host hands over a fresh copy of the conversation every request, so a // call dropped earlier arrives whole and has to be removed again — this // time without the tier ladder having run over it on the way. const second = conversation(200, 500); second.push(user("next")); fold(second, 0, { ceiling: 1e9, lowWater: 1e9 }, state); const parts = second.flatMap((m: AgentMessage) => m.content ?? []); expect(parts.filter((p: any) => p?.type === "toolCall" && p.id === "t0")).toEqual([]); expect(parts.filter((p: any) => p?.type === "thinking" && p.thinking)).toEqual([]); }); it("keeps a turn whose call it drops but whose prose it cannot", () => { // An assistant turn that both said something and called something: dropping // the call alone would leave the turn standing with its result gone, next // to the turn after it. const messages: AgentMessage[] = [user("go")]; for (let i = 0; i < 200; i++) { const content: any[] = [{ type: "toolCall", id: `t${i}`, name: "read", arguments: { path: `/f${i}` } }]; if (i % 20 === 0) content.unshift({ type: "text", text: `note ${i}` }); messages.push({ role: "assistant", content }); messages.push(result(`t${i}`, "read", "o".repeat(500))); } fold(messages, 0, { ceiling: 100, lowWater: 50 }, new FoldState()); const roles = messages.map((m: AgentMessage) => m.role); expect(roles.filter((role, i) => i > 0 && role === "assistant" && roles[i - 1] === "assistant")).toEqual([]); // The prose survives, and so does the call that shared its turn. const parts = messages.flatMap((m: AgentMessage) => m.content ?? []); expect(parts.some((p: any) => p?.type === "text" && p.text === "note 0")).toBe(true); expect(parts.some((p: any) => p?.type === "toolCall" && p.id === "t0")).toBe(true); // Its neighbours, carrying nothing but calls, still went. expect(parts.some((p: any) => p?.type === "toolCall" && p.id === "t1")).toBe(false); const answered = new Set(parts.filter((p: any) => p?.type === "toolCall").map((p: any) => p.id)); for (const m of messages) { if (m.role === "toolResult") expect(answered.has(m.toolCallId)).toBe(true); } }); it("stops the floor climbing with the number of calls made", () => { const shorter = incompressibleTokens(plain(200, 500), 0, 1); const longer = incompressibleTokens(plain(400, 500), 0, 1); // Twice the calls, the same floor: what the conversation has finished with // can leave, so only the recent history it is still working from is fixed. expect(longer).toBeLessThan(shorter * 1.1); }); it("scales the estimate by the calibration ratio", () => { const messages = conversation(4, 1000); const plain = fold([...messages.map((m) => JSON.parse(JSON.stringify(m)))], 0, { ceiling: 1e9, lowWater: 1e9 }, new FoldState()); const scaled = fold(messages, 0, { ceiling: 1e9, lowWater: 1e9 }, new FoldState(), 2); expect(scaled.tokens).toBe(plain.tokens * 2); }); it("counts an image by what an image costs, not by the payload carrying it", () => { const small = [user("go"), { role: "assistant", content: [{ type: "image", data: "b".repeat(4_000), mimeType: "image/png" }] }]; const large = [user("go"), { role: "assistant", content: [{ type: "image", data: "b".repeat(1_700_000), mimeType: "image/png" }] }]; expect(messagesBytes(large)).toBe(messagesBytes(small)); expect(messagesBytes(large)).toBeLessThan(20_000); }); it("counts an image at what the model it is bound for charges", () => { const messages = [user("go"), { role: "assistant", content: [{ type: "image", data: "b".repeat(4_000), mimeType: "image/png" }] }]; expect(messagesBytes(messages, 25_000) - messagesBytes(messages, 1_600)).toBe((25_000 - 1_600) * 4); }); it("counts an image result's payload in the notice it leaves behind", () => { const messages = [ user("go"), call("t0", "read", { path: "/a.png" }), { role: "toolResult", toolCallId: "t0", toolName: "read", content: [{ type: "text", text: "1024x768" }, { type: "image", data: "b".repeat(40_000), mimeType: "image/png" }], isError: false, }, ]; fold(messages, 0, { ceiling: 10, lowWater: 5 }, new FoldState()); expect(messages[2].content[0].text).toBe(`[omitted: ${40_000 + 8}B]`); }); it("never folds the newest load of a skill", () => { const messages = [ user("go"), call("s0", "load_skill", { name: "repository-work" }), result("s0", "load_skill", "OLD skill text ".repeat(500)), call("s1", "load_skill", { name: "repository-work" }), result("s1", "load_skill", "NEW skill text ".repeat(500)), call("t0", "read", { path: "/a" }), result("t0", "read", "o".repeat(9000)), ]; fold(messages, 0, { ceiling: 10, lowWater: 5 }, new FoldState()); expect(messages[4].content[0].text).toBe("NEW skill text ".repeat(500)); expect(messages[2].content[0].text).toMatch(/^\[omitted: /); expect(messages[6].content[0].text).toMatch(/^\[omitted: /); }); it("refuses a fold that would free too little to pay for its cache miss", () => { const messages = [user("p".repeat(40_000)), ...plain(10, 200).slice(1)]; const before = JSON.parse(JSON.stringify(messages)); const state = new FoldState(); const out = fold(messages, 0, { ceiling: 10_000, lowWater: 8_000 }, state); expect(messages).toEqual(before); expect(out.folded).toBe(0); expect(state.size).toBe(0); }); it("folds anyway once the prompt is past the point of no return", () => { const messages = [user("p".repeat(40_000)), ...plain(10, 200).slice(1)]; const state = new FoldState(); fold(messages, 0, { ceiling: 5_000, lowWater: 4_000 }, state); expect(state.size).toBeGreaterThan(0); }); it("takes any saving once the window itself is at risk", () => { const messages = [user("p".repeat(40_000)), ...plain(10, 200).slice(1)]; const state = new FoldState(); // Over the ceiling but under the usual margin above it; only the hard // limit says this prompt has to give something up. fold(messages, 0, { ceiling: 10_000, lowWater: 9_000, urgent: 10_500 }, state); expect(state.size).toBeGreaterThan(0); }); it("leaves a newly unprotected turn's reasoning alone when nothing else folds", () => { const state = new FoldState(); const first: AgentMessage[] = [ user("go"), truncated("a long unreachable thought ".repeat(200), "sig"), call("t0", "read", { path: "/a" }), result("t0", "read", "o".repeat(9000)), ]; fold(first, 0, { ceiling: 1_000, lowWater: 500 }, state); // Nothing may go while the turn that made it is the current one. expect(first[1].content[0].thinking).not.toBe(""); expect(state.tierOf("t0")).toBeGreaterThan(Tier.Verbatim); // A new user turn leaves that reasoning unprotected. Nothing new is // foldable, so nothing may move: the bytes are not worth the cache miss. const second: AgentMessage[] = [...first.slice(0, 4).map((m) => JSON.parse(JSON.stringify(m))), user("next")]; second[3].content[0].text = "o".repeat(9000); const out = fold(second, 0, { ceiling: 1_000, lowWater: 500 }, state); expect(second[1].content[0].thinking).not.toBe(""); expect(out.rewroteFrom).toBe(-1); }); it("reports the oldest message it moved, reasoning included", () => { const messages: AgentMessage[] = [ user("go"), call("t0", "read", { path: "/a" }), result("t0", "read", "o".repeat(9000)), truncated("stray thought ".repeat(300), "sig"), call("t1", "read", { path: "/b" }), result("t1", "read", "o".repeat(9000)), user("next"), ]; const state = new FoldState(); state.promote("t0", Tier.Folded); const out = fold(messages, 0, { ceiling: 1_000, lowWater: 500 }, state); // t1 sits at message 4, but the orphan reasoning at 3 went with it. expect(out.rewroteFrom).toBe(3); expect(messages[3].content[0].thinking).toBe(""); }); it("pins each distinct skill separately", () => { const messages = [ user("go"), call("s0", "load_skill", { name: "repository-work" }), result("s0", "load_skill", "repo skill ".repeat(500)), call("s1", "load_skill", { name: "software-engineering" }), result("s1", "load_skill", "eng skill ".repeat(500)), ]; fold(messages, 0, { ceiling: 10, lowWater: 5 }, new FoldState()); expect(messages[2].content[0].text).toBe("repo skill ".repeat(500)); expect(messages[4].content[0].text).toBe("eng skill ".repeat(500)); }); it("never folds an ask, however old", () => { const question = { question: "which?", options: ["a".repeat(600), "b".repeat(600)] }; const messages = [ user("go"), call("a0", "ask_user", question), result("a0", "ask_user", "User answered: " + "a".repeat(600)), call("a1", "ask_user", { question: "and then?" }), result("a1", "ask_user", "User answered: later"), ...conversation(8, 9000).slice(1), ]; const state = new FoldState(); fold(messages, 0, { ceiling: 10, lowWater: 5 }, state); expect(messages[1].content[0].arguments).toEqual(question); expect(messages[2].content[0].text).toBe("User answered: " + "a".repeat(600)); expect(messages[4].content[0].text).toBe("User answered: later"); expect(state.tierOf("a0")).toBe(Tier.Verbatim); expect(messages.some((m) => m.role === "toolResult" && m.toolName === "read")).toBe(true); }); }); describe("cutBytes", () => { it("does not split a multi-byte character", () => { expect(cutBytes("привет", 5)).toBe("пр"); expect(cutBytesFromEnd("привет", 5)).toBe("ет"); }); it("returns the whole string when it already fits", () => { expect(cutBytes("abc", 10)).toBe("abc"); expect(cutBytesFromEnd("abc", 10)).toBe("abc"); }); }); describe("Estimator", () => { it("counts bytes, not characters", () => { const cyrillic = [user("привет")]; const latin = [user("privet")]; expect(messagesBytes(cyrillic)).toBe(12); expect(messagesBytes(latin)).toBe(6); }); it("counts the system prompt and the tool schemas", () => { expect(fixedBytes("hello", [{ name: "read" }])).toBe(5 + byteLength(JSON.stringify({ name: "read" }))); }); it("returns the raw estimate before any calibration", () => { const estimator = new Estimator(); expect(estimator.tokens([user("x".repeat(400))], 0, "m")).toBe(100); }); it("adopts the first reading whole and smooths later ones", () => { const estimator = new Estimator(); estimator.observe("m", 100, 200); expect(estimator.ratioFor("m")).toBe(2); estimator.observe("m", 100, 100); expect(estimator.ratioFor("m")).toBeCloseTo(1.75, 5); }); it("converges on what the provider really charges", () => { // Every reading is the raw estimate against a charge twice its size, which // is what the guard hands over. Feeding it a prediction the ratio had // already scaled would make each reading describe the last correction // instead, and settle the ratio at the square root of the truth. const estimator = new Estimator(); for (let i = 0; i < 40; i++) estimator.observe("m", 1000, 2000); expect(estimator.ratioFor("m")).toBeCloseTo(2, 3); }); it("ignores a reading with nothing to learn from", () => { const estimator = new Estimator(); estimator.observe("m", 0, 100); estimator.observe("m", 100, 0); estimator.observe("", 100, 100); expect(estimator.ratioFor("m")).toBeUndefined(); }); }); describe("thinking signatures", () => { it("counts a signature as foldable, so it does not sit in the floor", () => { // A signature is several times the size of the text it certifies, and the // adapter drops a block whose text is empty — so leaving it out of what // folding can reach put the larger half of every thought in the floor. const withSignature: AgentMessage[] = [ user("go"), { role: "assistant", content: [ { type: "thinking", thinking: "мысль", thinkingSignature: "s".repeat(8000) }, { type: "toolCall", id: "t0", name: "read", arguments: { path: "/f" } }, ]}, result("t0", "read", "o".repeat(100)), user("дальше"), ]; const bare: AgentMessage[] = JSON.parse(JSON.stringify(withSignature)); (bare[1].content as any[])[0].thinkingSignature = ""; const gap = incompressibleTokens(withSignature, 0, 1) - incompressibleTokens(bare, 0, 1); expect(gap).toBeLessThan(200); }); it("drops the signature with the text it certifies", () => { const messages: AgentMessage[] = [ user("go"), { role: "assistant", content: [ { type: "thinking", thinking: "мысль", thinkingSignature: "s".repeat(8000) }, { type: "toolCall", id: "t0", name: "read", arguments: { path: "/f" } }, ]}, result("t0", "read", "o".repeat(8000)), user("дальше"), ]; fold(messages, 0, { ceiling: 100, lowWater: 50 }, new FoldState(), 1); const block = (messages[1].content as any[])[0]; expect(block.thinking).toBe(""); expect(block.thinkingSignature).toBe(""); }); it("leaves redacted reasoning alone, signature and all", () => { // There the signature is the payload the provider replays, not a // certificate attached to text. const messages: AgentMessage[] = [ user("go"), { role: "assistant", content: [ { type: "thinking", thinking: "", thinkingSignature: "opaque", redacted: true }, { type: "toolCall", id: "t0", name: "read", arguments: { path: "/f" } }, ]}, result("t0", "read", "o".repeat(8000)), user("дальше"), ]; fold(messages, 0, { ceiling: 100, lowWater: 50 }, new FoldState(), 1); expect((messages[1].content as any[])[0].thinkingSignature).toBe("opaque"); }); }); describe("limitsFor", () => { it("uses the default ceiling when no window is known", () => { const { ceiling, lowWater } = limitsFor(settings(), "some/model", 0); expect(ceiling).toBe(DEFAULT_MAX_PROMPT_TOKENS); expect(lowWater).toBe(Math.floor(DEFAULT_MAX_PROMPT_TOKENS * DEFAULT_KEEP_FRACTION)); }); it("leaves the configured ceiling alone when no window bounds the climb", () => { const { ceiling } = limitsFor(settings({ maxPromptTokens: 150_000 }), "m", 400_000); expect(ceiling).toBe(150_000); }); it("climbs the ceiling with the incompressible floor when a window is known", () => { const roomy = limitsFor(settings(), "m", 0, 1_000_000); const grown = limitsFor(settings(), "m", 400_000, 1_000_000); expect(grown.ceiling).toBeGreaterThan(roomy.ceiling); expect(grown.ceiling).toBe(400_000 + DEFAULT_HEADROOM_TOKENS); }); it("keeps the headroom absolute, so the window does not scale it", () => { const small = limitsFor(settings(), "m", 100_000, 400_000); const large = limitsFor(settings(), "m", 100_000, 1_000_000); expect(large.ceiling).toBe(100_000 + DEFAULT_HEADROOM_TOKENS); expect(small.ceiling).toBe(large.ceiling); }); it("never lets the ceiling reach the window", () => { const { ceiling } = limitsFor(settings(), "m", 900_000, 200_000); expect(ceiling).toBe(200_000 - 40_000 - 10_000); }); it("keeps a fixed share of the headroom per fold, whatever the prose", () => { for (const floor of [10_000, 100_000, 400_000]) { const { ceiling, lowWater } = limitsFor(settings(), "m", floor, 1_000_000); expect(lowWater - floor).toBe(Math.floor(DEFAULT_HEADROOM_TOKENS * DEFAULT_KEEP_FRACTION)); expect(ceiling - lowWater).toBe(DEFAULT_HEADROOM_TOKENS - Math.floor(DEFAULT_HEADROOM_TOKENS * DEFAULT_KEEP_FRACTION)); } }); it("aims at the floor when the floor already sits above the ceiling", () => { const { ceiling, lowWater } = limitsFor(settings(), "m", 900_000, 200_000); expect(lowWater).toBe(900_000); expect(lowWater).toBeGreaterThan(ceiling); }); it("honours a configured headroom and fold depth", () => { // The floor clears the default ceiling, so the configured headroom is what // decides where folding starts rather than that default. const { ceiling, lowWater } = limitsFor(settings({ headroomTokens: 60_000, keepFraction: 0.5 }), "m", 200_000, 1_000_000); expect(ceiling).toBe(260_000); expect(lowWater).toBe(230_000); }); it("prefers a declared window over the one the host reports", () => { const declared = limitsFor(settings({ perModel: { m: { contextWindow: 1_000_000 } } }), "m", 0, 200_000); expect(declared.ceiling).toBe(DEFAULT_HEADROOM_TOKENS); }); it("honours a low-water mark of zero rather than reading it as absent", () => { const { imageLowWater } = limitsFor(settings({ imageBytesLowWater: 0 }), "m", 0); expect(imageLowWater).toBe(0); }); it("holds the low-water mark under the ceiling it was given", () => { const { imageCeiling, imageLowWater } = limitsFor(settings({ imageBytesCeiling: 100_000, imageBytesLowWater: 900_000 }), "m", 0); expect(imageCeiling).toBe(100_000); expect(imageLowWater).toBe(50_000); }); it("matches a per-model override on the bare id", () => { const { ceiling } = limitsFor(settings({ perModel: { "claude-opus-4-8": { maxPromptTokens: 42_000 } } }), "anthropic/claude-opus-4-8", 0); expect(ceiling).toBe(42_000); }); }); const screenshot = (id: string, size: number, text?: string): AgentMessage => ({ role: "toolResult", toolCallId: id, toolName: "read", content: [ ...(text ? [{ type: "text", text }] : []), { type: "image", data: "i".repeat(size), mimeType: "image/png" }, ], isError: false, }); /** A conversation of `n` answered calls, each returning one `size`-byte image. */ const captures = (n: number, size: number): AgentMessage[] => { const messages: AgentMessage[] = [user("go")]; for (let i = 0; i < n; i++) { messages.push(call(`t${i}`, "read", { path: `/shot${i}.png` })); messages.push(screenshot(`t${i}`, size)); } return messages; }; const roomy = { ceiling: 100_000_000, lowWater: 90_000_000 }; describe("the image budget", () => { it("leaves a payload under the ceiling alone", () => { const messages = captures(3, 1000); const before = JSON.parse(JSON.stringify(messages)); const out = fold(messages, 0, { ...roomy, imageCeiling: 10_000, imageLowWater: 5_000 }, new FoldState()); expect(out.imagesFolded).toBe(0); expect(out.imageBytes).toBe(3000); expect(messages).toEqual(before); }); it("takes the oldest images off until the payload is under the low-water mark", () => { const messages = captures(10, 1000); const out = fold(messages, 0, { ...roomy, imageCeiling: 6_000, imageLowWater: 3_000 }, new FoldState()); expect(out.imagesFolded).toBe(7); expect(out.imageBytes).toBe(3000); expect(messages[2].content[0]).toEqual({ type: "text", text: "[omitted: 1000B]" }); expect(messages[14].content[0]).toEqual({ type: "text", text: "[omitted: 1000B]" }); // The three newest captures are what the model is still comparing against. expect(messages[16].content[0].type).toBe("image"); expect(messages[20].content[0].type).toBe("image"); }); it("folds on the payload even when the prompt is nowhere near its token ceiling", () => { const messages = captures(10, 500_000); const out = fold(messages, 0, { ...roomy, imageCeiling: 3_500_000, imageLowWater: 1_750_000 }, new FoldState()); // Ten images weigh 5MB on the wire and 16K tokens in the estimate, so the // token axis has no reason to act and the payload axis is the only thing // standing between the session and the body limit. expect(out.tokens).toBeLessThan(roomy.ceiling); expect(out.folded).toBe(0); expect(out.imageBytes).toBeLessThanOrEqual(1_750_000); }); it("keeps the text that came with a folded image", () => { const messages = [user("go"), call("t0", "read", { path: "/a.png" }), screenshot("t0", 9000, "1080x1920 capture")]; fold(messages, 0, { ...roomy, imageCeiling: 1000, imageLowWater: 0 }, new FoldState()); expect(messages[2].content).toEqual([ { type: "text", text: "1080x1920 capture" }, { type: "text", text: "[omitted: 9000B]" }, ]); }); it("counts an image a user attached but never takes it", () => { const messages: AgentMessage[] = [ { role: "user", content: [{ type: "text", text: "look" }, { type: "image", data: "u".repeat(4000), mimeType: "image/png" }] }, call("t0", "read", { path: "/a.png" }), screenshot("t0", 1000), ]; const out = fold(messages, 0, { ...roomy, imageCeiling: 2000, imageLowWater: 1500 }, new FoldState()); expect(out.imagesFolded).toBe(1); expect(out.imageBytes).toBe(4000); expect(messages[0].content[1].type).toBe("image"); }); it("keeps a stripped image stripped once the payload is back under the ceiling", () => { const state = new FoldState(); const limits = { ...roomy, imageCeiling: 6_000, imageLowWater: 3_000 }; const messages = captures(10, 1000); fold(messages, 0, limits, state); // The host hands over a fresh copy of the stored conversation every // request: without the memory, every image folded so far comes back. const replayed = captures(10, 1000); const out = fold(replayed, 0, limits, state); expect(out.imagesFolded).toBe(0); expect(out.imageBytes).toBe(3000); expect(replayed[2].content[0]).toEqual({ type: "text", text: "[omitted: 1000B]" }); // Nothing moved, so the pass costs no cache miss. expect(out.rewroteFrom).toBe(-1); }); it("takes every image when nothing is kept under the mark", () => { const messages = captures(4, 1000); const out = fold(messages, 0, { ...roomy, imageCeiling: 1, imageLowWater: 0 }, new FoldState()); expect(out.imagesFolded).toBe(4); expect(out.imageBytes).toBe(0); }); it("names the result message it rewrote, not the call it answered", () => { const messages = captures(4, 1000); const out = fold(messages, 0, { ...roomy, imageCeiling: 2_000, imageLowWater: 1_000 }, new FoldState()); expect(out.rewroteFrom).toBe(2); }); it("does not spend the budget on images of calls that are about to be removed", () => { const state = new FoldState(); for (const id of ["t0", "t1", "t2", "t3"]) state.promote(id, Tier.Drop); const messages = captures(6, 1000); const out = fold(messages, 0, { ...roomy, imageCeiling: 3_000, imageLowWater: 2_000 }, state); // Four of the six captures leave with the calls they answered, so the two // that remain are already under the ceiling and neither is touched. expect(out.imagesFolded).toBe(0); expect(out.imageBytes).toBe(2000); }); it("leaves a folded call alone, its images having gone with its output", () => { const messages = captures(6, 1000); const state = new FoldState(); state.promote("t0", Tier.Folded); const out = fold(messages, 0, { ...roomy, imageCeiling: 3_000, imageLowWater: 2_000 }, state); expect(messages[2].content[0].text).toBe("[omitted: 1000B]"); expect(out.imagesFolded).toBe(3); expect(out.imageBytes).toBe(2000); }); }); describe("what a folded call still says for itself", () => { it("keeps the call whole, so the notice never has to describe it", () => { const args = { command: "cargo build" }; const messages = [user("go"), call("t0", "bash", args), result("t0", "bash", "o".repeat(4000))]; fold(messages, 0, { ceiling: 10, lowWater: 5 }, new FoldState()); expect(messages[1].content[0].arguments).toEqual(args); expect(messages[2].content[0].text).toBe("[omitted: 4000B]"); }); it("leaves the id the model needs to recall by on both blocks of the pair", () => { const messages = [user("go"), call("t0", "bash", { command: "cargo build" }), result("t0", "bash", "o".repeat(4000))]; fold(messages, 0, { ceiling: 10, lowWater: 5 }, new FoldState()); expect(messages[1].content[0].id).toBe("t0"); expect(messages[2].toolCallId).toBe("t0"); }); });