From b461b8e7660da5d1a5ebcf95c028d676152f82f2 Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 24 Jul 2026 10:43:58 +0000 Subject: [PATCH] fix(agent): counted Anthropic server-tool blocks in token estimate estimateTokens now charges for serialized anthropicServerTool blocks so context maintenance sees the server-tool payload replayed on the wire; excluded from the compaction floor like other encrypted reasoning. --- packages/agent/src/compaction/compaction.ts | 8 ++++++ packages/agent/test/message-cache.test.ts | 28 +++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/packages/agent/src/compaction/compaction.ts b/packages/agent/src/compaction/compaction.ts index 7f0bf7980..b31015947 100644 --- a/packages/agent/src/compaction/compaction.ts +++ b/packages/agent/src/compaction/compaction.ts @@ -427,6 +427,14 @@ function computeMessageTokens(message: AgentMessage, options?: { excludeEncrypte // Encrypted reasoning blob the provider still bills for on replay; // excluded from the compaction floor for the same reason as above. if (!options?.excludeEncryptedReasoning) fragments.push(block.data); + } else if (block.type === "anthropicServerTool") { + // Native Anthropic server-tool call/result replayed verbatim on the + // wire (server_tool_use input, web_search_tool_result + // encrypted_content). Opaque provider-replay state the provider still + // bills for on same-provider replay; excluded from the compaction + // floor like other encrypted reasoning because its local byte size + // diverges from provider billing. + if (!options?.excludeEncryptedReasoning) fragments.push(stringifyJson(block.block) ?? "null"); } } break; diff --git a/packages/agent/test/message-cache.test.ts b/packages/agent/test/message-cache.test.ts index ac3db9997..bb2a1398c 100644 --- a/packages/agent/test/message-cache.test.ts +++ b/packages/agent/test/message-cache.test.ts @@ -110,6 +110,34 @@ describe("estimate cache option split", () => { expect(estimateTokens(msg as AgentMessage)).toBe(withBlob); expect(estimateTokens(msg as AgentMessage, { excludeEncryptedReasoning: true })).toBe(floored); }); + + test("counts native server-tool blocks by default and drops them from the compaction floor", () => { + const encrypted = "cipher".repeat(4000); + const msg: AssistantMessage = { + ...settledAssistant("with search"), + content: [ + { type: "text", text: "answer" }, + { + type: "anthropicServerTool", + block: { + type: "web_search_tool_result", + tool_use_id: "srvtoolu_1", + content: [{ type: "web_search_result", encrypted_content: encrypted }], + }, + }, + ], + }; + const textOnly = estimateTokens({ + ...settledAssistant("x"), + content: [{ type: "text", text: "answer" }], + } as AgentMessage); + const withServerTool = estimateTokens(msg as AgentMessage); + const floored = estimateTokens(msg as AgentMessage, { excludeEncryptedReasoning: true }); + // Default estimate charges for the serialized server-tool payload… + expect(withServerTool).toBeGreaterThan(floored + 500); + // …while the compaction floor ignores the opaque encrypted blob entirely. + expect(floored).toBe(textOnly); + }); }); describe("estimate cache invalidation seams", () => {