mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: align output limits with terminal integration spec
- 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
This commit is contained in:
parent
5711f1f32b
commit
70787f7a4a
5 changed files with 69 additions and 68 deletions
|
|
@ -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<TerminalOutputPreviewSize, number> = {
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue