From 70787f7a4a7b9e7933d997cbafc40e8e31950bad Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Tue, 27 Jan 2026 17:28:00 -0700 Subject: [PATCH] feat: align output limits with terminal integration spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update preview sizes: 2KB/4KB/8KB → 5KB/10KB/20KB (default 10KB) - Update read_command_output default limit: 32KB → 40KB - Match spec's MODEL_TRUNCATION_BYTES (10KB) for preview - Match spec's DEFAULT_MAX_OUTPUT_TOKENS (10000 tokens × 4 bytes = 40KB) for retrieval - Update all related tests and documentation --- packages/types/src/global-settings.ts | 14 +-- .../tools/native-tools/read_command_output.ts | 8 +- src/core/tools/ReadCommandOutputTool.ts | 4 +- .../__tests__/ReadCommandOutputTool.test.ts | 6 +- .../__tests__/OutputInterceptor.test.ts | 105 +++++++++--------- 5 files changed, 69 insertions(+), 68 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index f07134df3a..d57ec616ff 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -29,9 +29,9 @@ export const DEFAULT_WRITE_DELAY_MS = 1000 * the LLM decides to retrieve more via `read_command_output`. Larger previews * mean more immediate context but consume more of the context window. * - * - `small`: 2KB preview - Best for long-running commands with verbose output - * - `medium`: 4KB preview - Balanced default for most use cases - * - `large`: 8KB preview - Best when commands produce critical info early + * - `small`: 5KB preview - Best for long-running commands with verbose output + * - `medium`: 10KB preview - Balanced default for most use cases + * - `large`: 20KB preview - Best when commands produce critical info early * * @see OutputInterceptor - Uses this setting to determine when to spill to disk * @see PersistedCommandOutput - Contains the resulting preview and artifact reference @@ -46,14 +46,14 @@ export type TerminalOutputPreviewSize = "small" | "medium" | "large" * to disk and made available via the `read_command_output` tool. */ export const TERMINAL_PREVIEW_BYTES: Record = { - small: 2048, // 2KB - medium: 4096, // 4KB - large: 8192, // 8KB + small: 5 * 1024, // 5KB + medium: 10 * 1024, // 10KB + large: 20 * 1024, // 20KB } /** * Default terminal output preview size. - * The "medium" (4KB) setting provides a good balance between immediate + * The "medium" (10KB) setting provides a good balance between immediate * visibility and context window conservation for most use cases. */ export const DEFAULT_TERMINAL_OUTPUT_PREVIEW_SIZE: TerminalOutputPreviewSize = "medium" diff --git a/src/core/prompts/tools/native-tools/read_command_output.ts b/src/core/prompts/tools/native-tools/read_command_output.ts index af5148cd10..007915b005 100644 --- a/src/core/prompts/tools/native-tools/read_command_output.ts +++ b/src/core/prompts/tools/native-tools/read_command_output.ts @@ -22,13 +22,13 @@ Parameters: - artifact_id: (required) The artifact filename from the truncated output message (e.g., "cmd-1706119234567.txt") - search: (optional) Pattern to filter lines. Supports regex or literal strings. Case-insensitive. - offset: (optional) Byte offset to start reading from. Default: 0. Use for pagination. -- limit: (optional) Maximum bytes to return. Default: 32KB. +- limit: (optional) Maximum bytes to return. Default: 40KB. Example: Reading truncated command output { "artifact_id": "cmd-1706119234567.txt" } -Example: Reading with pagination (after first 32KB) -{ "artifact_id": "cmd-1706119234567.txt", "offset": 32768 } +Example: Reading with pagination (after first 40KB) +{ "artifact_id": "cmd-1706119234567.txt", "offset": 40960 } Example: Searching for errors in build output { "artifact_id": "cmd-1706119234567.txt", "search": "error|failed|Error" } @@ -42,7 +42,7 @@ const SEARCH_DESCRIPTION = `Optional regex or literal pattern to filter lines (c const OFFSET_DESCRIPTION = `Byte offset to start reading from (default: 0, for pagination)` -const LIMIT_DESCRIPTION = `Maximum bytes to return (default: 32KB)` +const LIMIT_DESCRIPTION = `Maximum bytes to return (default: 40KB)` export default { type: "function", diff --git a/src/core/tools/ReadCommandOutputTool.ts b/src/core/tools/ReadCommandOutputTool.ts index d1893c380b..9ae4a377ef 100644 --- a/src/core/tools/ReadCommandOutputTool.ts +++ b/src/core/tools/ReadCommandOutputTool.ts @@ -6,8 +6,8 @@ import { getTaskDirectoryPath } from "../../utils/storage" import { BaseTool, ToolCallbacks } from "./BaseTool" -/** Default byte limit for read operations (32KB) */ -const DEFAULT_LIMIT = 32 * 1024 // 32KB default limit +/** Default byte limit for read operations (40KB) */ +const DEFAULT_LIMIT = 40 * 1024 // 40KB default limit /** * Parameters accepted by the read_command_output tool. diff --git a/src/core/tools/__tests__/ReadCommandOutputTool.test.ts b/src/core/tools/__tests__/ReadCommandOutputTool.test.ts index beec4094a1..11f85e67c0 100644 --- a/src/core/tools/__tests__/ReadCommandOutputTool.test.ts +++ b/src/core/tools/__tests__/ReadCommandOutputTool.test.ts @@ -159,16 +159,16 @@ describe("ReadCommandOutputTool", () => { }) describe("Pagination (offset/limit)", () => { - it("should use default limit of 32KB", async () => { + it("should use default limit of 40KB", async () => { const artifactId = "cmd-1706119234567.txt" const largeContent = "x".repeat(50 * 1024) // 50KB const fileSize = Buffer.byteLength(largeContent, "utf8") vi.mocked(fs.stat).mockResolvedValue({ size: fileSize } as any) - // Mock read to return only up to default limit (32KB) + // Mock read to return only up to default limit (40KB) mockFileHandle.read.mockImplementation((buf: Buffer) => { - const defaultLimit = 32 * 1024 + const defaultLimit = 40 * 1024 const bytesToRead = Math.min(buf.length, defaultLimit) buf.write(largeContent.slice(0, bytesToRead)) return Promise.resolve({ bytesRead: bytesToRead }) diff --git a/src/integrations/terminal/__tests__/OutputInterceptor.test.ts b/src/integrations/terminal/__tests__/OutputInterceptor.test.ts index c91253d6ed..ed308cff13 100644 --- a/src/integrations/terminal/__tests__/OutputInterceptor.test.ts +++ b/src/integrations/terminal/__tests__/OutputInterceptor.test.ts @@ -59,7 +59,7 @@ describe("OutputInterceptor", () => { taskId: "task-1", command: "echo test", storageDir, - previewSize: "small", // 2KB + previewSize: "small", // 5KB }) const smallOutput = "Hello World\n" @@ -81,18 +81,18 @@ describe("OutputInterceptor", () => { taskId: "task-1", command: "echo test", storageDir, - previewSize: "small", // 2KB = 2048 bytes + previewSize: "small", // 5KB = 5120 bytes }) - // Write enough data to exceed 2KB threshold - const chunk = "x".repeat(1024) // 1KB chunk - interceptor.write(chunk) // 1KB - should stay in memory - expect(interceptor.hasSpilledToDisk()).toBe(false) - + // Write enough data to exceed 5KB threshold + const chunk = "x".repeat(2 * 1024) // 2KB chunk interceptor.write(chunk) // 2KB - should stay in memory expect(interceptor.hasSpilledToDisk()).toBe(false) - interceptor.write(chunk) // 3KB - should trigger spill + interceptor.write(chunk) // 4KB - should stay in memory + expect(interceptor.hasSpilledToDisk()).toBe(false) + + interceptor.write(chunk) // 6KB - should trigger spill expect(interceptor.hasSpilledToDisk()).toBe(true) expect(fs.createWriteStream).toHaveBeenCalledWith(path.join(storageDir, "cmd-12345.txt")) expect(mockWriteStream.write).toHaveBeenCalled() @@ -104,11 +104,11 @@ describe("OutputInterceptor", () => { taskId: "task-1", command: "echo test", storageDir, - previewSize: "small", // 2KB + previewSize: "small", // 5KB }) // Write data that exceeds threshold - const chunk = "x".repeat(3000) + const chunk = "x".repeat(6000) interceptor.write(chunk) expect(interceptor.hasSpilledToDisk()).toBe(true) @@ -131,8 +131,8 @@ describe("OutputInterceptor", () => { previewSize: "small", }) - // Trigger spill - const largeChunk = "x".repeat(3000) + // Trigger spill (must exceed 5KB = 5120 bytes) + const largeChunk = "x".repeat(6000) interceptor.write(largeChunk) expect(interceptor.hasSpilledToDisk()).toBe(true) @@ -148,7 +148,7 @@ describe("OutputInterceptor", () => { }) describe("Threshold settings", () => { - it("should handle small (2KB) threshold correctly", () => { + it("should handle small (5KB) threshold correctly", () => { const interceptor = new OutputInterceptor({ executionId: "12345", taskId: "task-1", @@ -157,16 +157,16 @@ describe("OutputInterceptor", () => { previewSize: "small", }) - // Write exactly 2KB - interceptor.write("x".repeat(2048)) + // Write exactly 5KB + interceptor.write("x".repeat(5 * 1024)) expect(interceptor.hasSpilledToDisk()).toBe(false) - // Write more to exceed 2KB + // Write more to exceed 5KB interceptor.write("x") expect(interceptor.hasSpilledToDisk()).toBe(true) }) - it("should handle medium (4KB) threshold correctly", () => { + it("should handle medium (10KB) threshold correctly", () => { const interceptor = new OutputInterceptor({ executionId: "12345", taskId: "task-1", @@ -175,16 +175,16 @@ describe("OutputInterceptor", () => { previewSize: "medium", }) - // Write exactly 4KB - interceptor.write("x".repeat(4096)) + // Write exactly 10KB + interceptor.write("x".repeat(10 * 1024)) expect(interceptor.hasSpilledToDisk()).toBe(false) - // Write more to exceed 4KB + // Write more to exceed 10KB interceptor.write("x") expect(interceptor.hasSpilledToDisk()).toBe(true) }) - it("should handle large (8KB) threshold correctly", () => { + it("should handle large (20KB) threshold correctly", () => { const interceptor = new OutputInterceptor({ executionId: "12345", taskId: "task-1", @@ -193,11 +193,11 @@ describe("OutputInterceptor", () => { previewSize: "large", }) - // Write exactly 8KB - interceptor.write("x".repeat(8192)) + // Write exactly 20KB + interceptor.write("x".repeat(20 * 1024)) expect(interceptor.hasSpilledToDisk()).toBe(false) - // Write more to exceed 8KB + // Write more to exceed 20KB interceptor.write("x") expect(interceptor.hasSpilledToDisk()).toBe(true) }) @@ -215,8 +215,8 @@ describe("OutputInterceptor", () => { previewSize: "small", }) - // Trigger spill - interceptor.write("x".repeat(3000)) + // Trigger spill (must exceed 5KB = 5120 bytes) + interceptor.write("x".repeat(6000)) expect(fs.mkdirSync).toHaveBeenCalledWith(storageDir, { recursive: true }) }) @@ -231,8 +231,8 @@ describe("OutputInterceptor", () => { previewSize: "small", }) - // Trigger spill - interceptor.write("x".repeat(3000)) + // Trigger spill (must exceed 5KB = 5120 bytes) + interceptor.write("x".repeat(6000)) expect(fs.createWriteStream).toHaveBeenCalledWith(path.join(storageDir, `cmd-${executionId}.txt`)) }) @@ -243,10 +243,10 @@ describe("OutputInterceptor", () => { taskId: "task-1", command: "test", storageDir, - previewSize: "small", // 2KB = 2048 bytes, so head=1024, tail=1024 + previewSize: "small", // 5KB = 5120 bytes, so head=2560, tail=2560 }) - const fullOutput = "x".repeat(5000) + const fullOutput = "x".repeat(10000) interceptor.write(fullOutput) // The write stream should receive the head buffer content first @@ -298,10 +298,10 @@ describe("OutputInterceptor", () => { taskId: "task-1", command: "test", storageDir, - previewSize: "small", // 2KB = 2048, head=1024, tail=1024 + previewSize: "small", // 5KB = 5120, head=2560, tail=2560 }) - const largeOutput = "x".repeat(5000) + const largeOutput = "x".repeat(10000) interceptor.write(largeOutput) const result = await interceptor.finalize() @@ -323,8 +323,8 @@ describe("OutputInterceptor", () => { previewSize: "small", }) - // Trigger spill - interceptor.write("x".repeat(3000)) + // Trigger spill (must exceed 5KB = 5120 bytes) + interceptor.write("x".repeat(6000)) await interceptor.finalize() expect(mockWriteStream.end).toHaveBeenCalled() @@ -339,13 +339,14 @@ describe("OutputInterceptor", () => { previewSize: "small", }) - const output = "x".repeat(5000) + // Must exceed 5KB = 5120 bytes to trigger truncation + const output = "x".repeat(6000) interceptor.write(output) const result = await interceptor.finalize() expect(result).toHaveProperty("preview") - expect(result).toHaveProperty("totalBytes", 5000) + expect(result).toHaveProperty("totalBytes", 6000) expect(result).toHaveProperty("artifactPath") expect(result).toHaveProperty("truncated", true) expect(result.artifactPath).toMatch(/cmd-12345\.txt$/) @@ -422,16 +423,16 @@ describe("OutputInterceptor", () => { taskId: "task-1", command: "test", storageDir, - previewSize: "small", // 2KB = 2048, head=1024, tail=1024 + previewSize: "small", // 5KB = 5120, head=2560, tail=2560 }) // Trigger spill - const largeOutput = "x".repeat(5000) + const largeOutput = "x".repeat(10000) interceptor.write(largeOutput) const buffer = interceptor.getBufferForUI() // Buffer for UI is head + tail (no omission indicator for smooth streaming) - expect(Buffer.byteLength(buffer, "utf8")).toBeLessThanOrEqual(2048) + expect(Buffer.byteLength(buffer, "utf8")).toBeLessThanOrEqual(5120) }) }) @@ -442,13 +443,13 @@ describe("OutputInterceptor", () => { taskId: "task-1", command: "test", storageDir, - previewSize: "small", // 2KB = 2048, head=1024, tail=1024 + previewSize: "small", // 5KB = 5120, head=2560, tail=2560 }) // Create identifiable head and tail content - const headContent = "HEAD".repeat(300) // 1200 bytes - const middleContent = "M".repeat(3000) // 3000 bytes (will be omitted) - const tailContent = "TAIL".repeat(300) // 1200 bytes + const headContent = "HEAD".repeat(750) // 3000 bytes + const middleContent = "M".repeat(6000) // 6000 bytes (will be omitted) + const tailContent = "TAIL".repeat(750) // 3000 bytes interceptor.write(headContent) interceptor.write(middleContent) @@ -456,9 +457,9 @@ describe("OutputInterceptor", () => { const result = await interceptor.finalize() - // Should start with HEAD content (first 1024 bytes of head budget) + // Should start with HEAD content (first 2560 bytes of head budget) expect(result.preview.startsWith("HEAD")).toBe(true) - // Should end with TAIL content (last 1024 bytes) + // Should end with TAIL content (last 2560 bytes) expect(result.preview.endsWith("TAIL")).toBe(true) // Should have omission indicator expect(result.preview).toContain("[...") @@ -471,7 +472,7 @@ describe("OutputInterceptor", () => { taskId: "task-1", command: "test", storageDir, - previewSize: "small", // 2KB + previewSize: "small", // 5KB }) const smallOutput = "Hello World\n" @@ -490,11 +491,11 @@ describe("OutputInterceptor", () => { taskId: "task-1", command: "test", storageDir, - previewSize: "small", // 2KB = 2048, head=1024 + previewSize: "small", // 5KB = 5120, head=2560 }) - // Write exactly 1024 bytes (head budget) - const exactHeadContent = "x".repeat(1024) + // Write exactly 2560 bytes (head budget) + const exactHeadContent = "x".repeat(2560) interceptor.write(exactHeadContent) const result = await interceptor.finalize() @@ -510,12 +511,12 @@ describe("OutputInterceptor", () => { taskId: "task-1", command: "test", storageDir, - previewSize: "small", // 2KB = 2048, head=1024, tail=1024 + previewSize: "small", // 5KB = 5120, head=2560, tail=2560 }) // Write a single chunk larger than preview budget - // First 1024 chars go to head, last 1024 chars go to tail - const content = "A".repeat(1024) + "B".repeat(2000) + "C".repeat(1024) + // First 2560 chars go to head, last 2560 chars go to tail + const content = "A".repeat(2560) + "B".repeat(4000) + "C".repeat(2560) interceptor.write(content) const result = await interceptor.finalize()