From 1197f919af063bed194eb3052b6418f3ce34118d Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 15 Feb 2026 14:56:17 +0900 Subject: [PATCH] feat: add SDK/HTTP paths for tool-result-storage truncation --- .../storage.ts | 7 + .../tool-result-storage-sdk.ts | 127 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 src/hooks/anthropic-context-window-limit-recovery/tool-result-storage-sdk.ts diff --git a/src/hooks/anthropic-context-window-limit-recovery/storage.ts b/src/hooks/anthropic-context-window-limit-recovery/storage.ts index 3cd302c8..2f2136fd 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/storage.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/storage.ts @@ -8,4 +8,11 @@ export { truncateToolResult, } from "./tool-result-storage" +export { + countTruncatedResultsFromSDK, + findToolResultsBySizeFromSDK, + getTotalToolOutputSizeFromSDK, + truncateToolResultAsync, +} from "./tool-result-storage-sdk" + export { truncateUntilTargetTokens } from "./target-token-truncation" diff --git a/src/hooks/anthropic-context-window-limit-recovery/tool-result-storage-sdk.ts b/src/hooks/anthropic-context-window-limit-recovery/tool-result-storage-sdk.ts new file mode 100644 index 00000000..2db298d3 --- /dev/null +++ b/src/hooks/anthropic-context-window-limit-recovery/tool-result-storage-sdk.ts @@ -0,0 +1,127 @@ +import type { PluginInput } from "@opencode-ai/plugin" +import { getMessageIdsFromSDK } from "./message-storage-directory" +import { TRUNCATION_MESSAGE } from "./storage-paths" +import type { ToolResultInfo } from "./tool-part-types" +import { patchPart } from "../../shared/opencode-http-api" +import { log } from "../../shared/logger" + +type OpencodeClient = PluginInput["client"] + +interface SDKToolPart { + id: string + type: string + callID?: string + tool?: string + state?: { + status?: string + input?: Record + output?: string + error?: string + time?: { start?: number; end?: number; compacted?: number } + } + truncated?: boolean + originalSize?: number +} + +interface SDKMessage { + info?: { id?: string } + parts?: SDKToolPart[] +} + +export async function findToolResultsBySizeFromSDK( + client: OpencodeClient, + sessionID: string +): Promise { + try { + const response = await client.session.messages({ path: { id: sessionID } }) + const messages = (response.data ?? []) as SDKMessage[] + const results: ToolResultInfo[] = [] + + for (const msg of messages) { + const messageID = msg.info?.id + if (!messageID || !msg.parts) continue + + for (const part of msg.parts) { + if (part.type === "tool" && part.state?.output && !part.truncated && part.tool) { + results.push({ + partPath: "", + partId: part.id, + messageID, + toolName: part.tool, + outputSize: part.state.output.length, + }) + } + } + } + + return results.sort((a, b) => b.outputSize - a.outputSize) + } catch { + return [] + } +} + +export async function truncateToolResultAsync( + client: OpencodeClient, + sessionID: string, + messageID: string, + partId: string, + part: SDKToolPart +): Promise<{ success: boolean; toolName?: string; originalSize?: number }> { + if (!part.state?.output) return { success: false } + + const originalSize = part.state.output.length + const toolName = part.tool + + const updatedPart: Record = { + ...part, + truncated: true, + originalSize, + state: { + ...part.state, + output: TRUNCATION_MESSAGE, + time: { + ...(part.state.time ?? { start: Date.now() }), + compacted: Date.now(), + }, + }, + } + + try { + const patched = await patchPart(client, sessionID, messageID, partId, updatedPart) + if (!patched) return { success: false } + return { success: true, toolName, originalSize } + } catch (error) { + log("[context-window-recovery] truncateToolResultAsync failed", { error: String(error) }) + return { success: false } + } +} + +export async function countTruncatedResultsFromSDK( + client: OpencodeClient, + sessionID: string +): Promise { + try { + const response = await client.session.messages({ path: { id: sessionID } }) + const messages = (response.data ?? []) as SDKMessage[] + let count = 0 + + for (const msg of messages) { + if (!msg.parts) continue + for (const part of msg.parts) { + if (part.truncated === true) count++ + } + } + + return count + } catch { + return 0 + } +} + +export async function getTotalToolOutputSizeFromSDK( + client: OpencodeClient, + sessionID: string +): Promise { + const results = await findToolResultsBySizeFromSDK(client, sessionID) + return results.reduce((sum, result) => sum + result.outputSize, 0) +}