From 928c12435627b47e9678398fb626842822df5c22 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 12 Jul 2026 16:01:11 +0200 Subject: [PATCH] refactor(server): slice oversized tool output directly --- docs/timeline-sync.md | 8 +-- .../agent-manager-stream-coalescing.test.ts | 20 ++----- .../server/agent/agent-timeline-content.ts | 58 +------------------ 3 files changed, 12 insertions(+), 74 deletions(-) diff --git a/docs/timeline-sync.md b/docs/timeline-sync.md index 168cf26b7..86414257a 100644 --- a/docs/timeline-sync.md +++ b/docs/timeline-sync.md @@ -9,10 +9,10 @@ The invariant is: > If the daemon has committed timeline rows for an agent, any connected client that opens or resumes that agent eventually displays every row through the daemon's current tail. -Tool output is bounded before it enters either delivery path. Canonical shell tool output keeps -the head and tail within a 64 KiB UTF-8 budget, and the same bounded item is used for durable -timeline rows and live stream events. Provider history hydration applies the same rule so reopening -an agent cannot restore an oversized tool payload. +Tool output is bounded before it enters either delivery path. Canonical shell tool output is sliced +to 64 KiB, and the same bounded item is used for durable timeline rows and live stream events. +Provider history hydration applies the same rule so reopening an agent cannot restore an oversized +tool payload. ## Presence is not delivery diff --git a/packages/server/src/server/agent/agent-manager-stream-coalescing.test.ts b/packages/server/src/server/agent/agent-manager-stream-coalescing.test.ts index 6b6cae9ba..980b3c5bf 100644 --- a/packages/server/src/server/agent/agent-manager-stream-coalescing.test.ts +++ b/packages/server/src/server/agent/agent-manager-stream-coalescing.test.ts @@ -30,8 +30,7 @@ import type { const COALESCE_WINDOW_MS = AGENT_STREAM_COALESCE_DEFAULT_WINDOW_MS; const BEFORE_COALESCE_WINDOW_MS = Math.max(COALESCE_WINDOW_MS - 1, 0); -const TOOL_CALL_CONTENT_MAX_BYTES = 64 * 1024; -const TOOL_CALL_CONTENT_TRUNCATION_MARKER = "\n......\n"; +const TOOL_CALL_CONTENT_MAX_LENGTH = 64 * 1024; const TEST_CAPABILITIES: AgentCapabilityFlags = { supportsStreaming: false, @@ -85,15 +84,6 @@ function toolCall(options?: { }; } -function boundedToolCallOutput(head: string, tail: string): string { - const availableBytes = - TOOL_CALL_CONTENT_MAX_BYTES - Buffer.byteLength(TOOL_CALL_CONTENT_TRUNCATION_MARKER); - const headBytes = Math.floor(availableBytes / 2); - return `${head.repeat(headBytes)}${TOOL_CALL_CONTENT_TRUNCATION_MARKER}${tail.repeat( - availableBytes - headBytes, - )}`; -} - class TestAgentSession implements AgentSession { readonly capabilities = TEST_CAPABILITIES; readonly id: string; @@ -395,7 +385,7 @@ describe("target coalesced behavior", () => { const output = `${"a".repeat(512 * 1024)}${"z".repeat(512 * 1024)}`; const expectedItem = toolCall({ status: "completed", - output: boundedToolCallOutput("a", "z"), + output: "a".repeat(TOOL_CALL_CONTENT_MAX_LENGTH), }); session.pushEvent(timelineEvent(toolCall({ status: "completed", output }))); @@ -420,7 +410,7 @@ describe("target coalesced behavior", () => { const output = `${"a".repeat(512 * 1024)}${"z".repeat(512 * 1024)}`; const expectedItem = toolCall({ status: "completed", - output: boundedToolCallOutput("a", "z"), + output: "a".repeat(TOOL_CALL_CONTENT_MAX_LENGTH), }); await harness.manager.appendTimelineItem(agentId, toolCall({ status: "completed", output })); @@ -444,7 +434,7 @@ describe("target coalesced behavior", () => { const output = `${"a".repeat(512 * 1024)}${"z".repeat(512 * 1024)}`; const expectedItem = toolCall({ status: "completed", - output: boundedToolCallOutput("a", "z"), + output: "a".repeat(TOOL_CALL_CONTENT_MAX_LENGTH), }); session.setHistory([timelineEvent(toolCall({ status: "completed", output }))]); @@ -465,7 +455,7 @@ describe("target coalesced behavior", () => { const output = `${"a".repeat(512 * 1024)}${"z".repeat(512 * 1024)}`; const expectedItem = toolCall({ status: "completed", - output: boundedToolCallOutput("a", "z"), + output: "a".repeat(TOOL_CALL_CONTENT_MAX_LENGTH), }); await harness.manager.emitLiveTimelineItem( diff --git a/packages/server/src/server/agent/agent-timeline-content.ts b/packages/server/src/server/agent/agent-timeline-content.ts index 328c33bff..82ead36c2 100644 --- a/packages/server/src/server/agent/agent-timeline-content.ts +++ b/packages/server/src/server/agent/agent-timeline-content.ts @@ -1,57 +1,6 @@ import type { AgentTimelineItem } from "./agent-sdk-types.js"; -const TOOL_CALL_CONTENT_MAX_BYTES = 64 * 1024; -const TOOL_CALL_CONTENT_TRUNCATION_MARKER = "\n......\n"; - -function utf8ByteLength(text: string): number { - return Buffer.byteLength(text, "utf8"); -} - -function takeFirstUtf8Bytes(text: string, maxBytes: number): string { - let low = 0; - let high = text.length; - while (low < high) { - const midpoint = Math.ceil((low + high) / 2); - if (utf8ByteLength(text.slice(0, midpoint)) <= maxBytes) { - low = midpoint; - } else { - high = midpoint - 1; - } - } - if (low > 0 && low < text.length && /[\uD800-\uDBFF]/.test(text[low - 1] ?? "")) { - low -= 1; - } - return text.slice(0, low); -} - -function takeLastUtf8Bytes(text: string, maxBytes: number): string { - let low = 0; - let high = text.length; - while (low < high) { - const length = Math.ceil((low + high) / 2); - if (utf8ByteLength(text.slice(text.length - length)) <= maxBytes) { - low = length; - } else { - high = length - 1; - } - } - let start = text.length - low; - if (start > 0 && start < text.length && /[\uDC00-\uDFFF]/.test(text[start] ?? "")) { - start += 1; - } - return text.slice(start); -} - -function limitToolCallText(text: string): string { - if (utf8ByteLength(text) <= TOOL_CALL_CONTENT_MAX_BYTES) { - return text; - } - const availableBytes = - TOOL_CALL_CONTENT_MAX_BYTES - utf8ByteLength(TOOL_CALL_CONTENT_TRUNCATION_MARKER); - const headBytes = Math.floor(availableBytes / 2); - const tailBytes = availableBytes - headBytes; - return `${takeFirstUtf8Bytes(text, headBytes)}${TOOL_CALL_CONTENT_TRUNCATION_MARKER}${takeLastUtf8Bytes(text, tailBytes)}`; -} +const TOOL_CALL_CONTENT_MAX_LENGTH = 64 * 1024; export function limitAgentTimelineItemContent(item: AgentTimelineItem): AgentTimelineItem { if ( @@ -61,15 +10,14 @@ export function limitAgentTimelineItemContent(item: AgentTimelineItem): AgentTim ) { return item; } - const output = limitToolCallText(item.detail.output); - if (output === item.detail.output) { + if (item.detail.output.length <= TOOL_CALL_CONTENT_MAX_LENGTH) { return item; } return { ...item, detail: { ...item.detail, - output, + output: item.detail.output.slice(0, TOOL_CALL_CONTENT_MAX_LENGTH), }, }; }