


























1+import { describe, expect, it } from "vitest";
2+import type { ImageContent } from "../../llm.js";
3+import type { AgentMessage } from "../../types.js";
4+import type { SessionTreeEntry } from "../types.js";
5+import { estimateTokens, findCutPoint } from "./compaction.js";
6+7+const IMAGE_PAYLOAD = "a".repeat(1_500_000);
8+9+function imageBlock(): ImageContent {
10+return { type: "image", data: IMAGE_PAYLOAD, mimeType: "image/png" };
11+}
12+13+function userImage(timestamp: number): AgentMessage {
14+return { role: "user", content: [imageBlock()], timestamp };
15+}
16+17+function userText(text: string, timestamp: number): AgentMessage {
18+return { role: "user", content: [{ type: "text", text }], timestamp };
19+}
20+21+function toolResultImage(timestamp: number): AgentMessage {
22+return {
23+role: "toolResult",
24+toolCallId: "call-1",
25+toolName: "screenshot",
26+content: [imageBlock()],
27+isError: false,
28+ timestamp,
29+};
30+}
31+32+function assistantText(text: string, timestamp: number): AgentMessage {
33+return {
34+role: "assistant",
35+content: [{ type: "text", text }],
36+api: "anthropic-messages",
37+provider: "anthropic",
38+model: "claude-fable-5",
39+usage: {
40+input: 0,
41+output: 0,
42+cacheRead: 0,
43+cacheWrite: 0,
44+totalTokens: 0,
45+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
46+},
47+stopReason: "stop",
48+ timestamp,
49+};
50+}
51+52+function messageEntry(message: AgentMessage, index: number): SessionTreeEntry {
53+return {
54+type: "message",
55+id: `entry-${index}`,
56+parentId: index === 0 ? null : `entry-${index - 1}`,
57+timestamp: new Date(message.timestamp).toISOString(),
58+ message,
59+};
60+}
61+62+function buildTranscript(recentUserTurns: AgentMessage[]): SessionTreeEntry[] {
63+const messages: AgentMessage[] = [userText("start of the conversation", 1)];
64+let timestamp = 2;
65+for (const turn of recentUserTurns) {
66+messages.push(assistantText("ok", timestamp++));
67+messages.push(turn);
68+}
69+return messages.map((message, index) => messageEntry(message, index));
70+}
71+72+describe("estimateTokens image accounting", () => {
73+it("charges a user-message image block the same as a tool-result image block", () => {
74+const userTokens = estimateTokens(userImage(1));
75+const toolTokens = estimateTokens(toolResultImage(1));
76+77+expect(userTokens).toBe(toolTokens);
78+expect(userTokens).toBeGreaterThanOrEqual(1200);
79+});
80+});
81+82+describe("findCutPoint with image-heavy recent turns", () => {
83+it("trims image-dominated user turns instead of keeping the whole transcript", () => {
84+const entries = buildTranscript([userImage(10), userImage(20), userImage(30)]);
85+86+const result = findCutPoint(entries, 0, entries.length, 1500);
87+88+expect(result.firstKeptEntryIndex).toBeGreaterThan(0);
89+});
90+91+it("matches the cut point of an equivalent text-cost control", () => {
92+const equivalentText = "x".repeat(4800);
93+const imageEntries = buildTranscript([userImage(10), userImage(20), userImage(30)]);
94+const textEntries = buildTranscript([
95+userText(equivalentText, 10),
96+userText(equivalentText, 20),
97+userText(equivalentText, 30),
98+]);
99+100+const imageResult = findCutPoint(imageEntries, 0, imageEntries.length, 1500);
101+const textResult = findCutPoint(textEntries, 0, textEntries.length, 1500);
102+103+expect(textResult.firstKeptEntryIndex).toBeGreaterThan(0);
104+expect(imageResult.firstKeptEntryIndex).toBe(textResult.firstKeptEntryIndex);
105+});
106+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。