mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
working
This commit is contained in:
parent
d62a260576
commit
5c48253148
4 changed files with 654 additions and 8 deletions
380
src/core/tools/__tests__/contextValidator.test.ts
Normal file
380
src/core/tools/__tests__/contextValidator.test.ts
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { validateFileSizeForContext } from "../contextValidator"
|
||||
import { Task } from "../../task/Task"
|
||||
import { promises as fs } from "fs"
|
||||
import { readLines } from "../../../integrations/misc/read-lines"
|
||||
import * as sharedApi from "../../../shared/api"
|
||||
|
||||
vi.mock("fs", () => ({
|
||||
promises: {
|
||||
stat: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("../../../integrations/misc/read-lines", () => ({
|
||||
readLines: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("../../../shared/api", () => ({
|
||||
getModelMaxOutputTokens: vi.fn(),
|
||||
}))
|
||||
|
||||
describe("contextValidator", () => {
|
||||
let mockTask: Task
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Mock Task instance
|
||||
mockTask = {
|
||||
api: {
|
||||
getModel: vi.fn().mockReturnValue({
|
||||
id: "test-model",
|
||||
info: {
|
||||
contextWindow: 100000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
}),
|
||||
countTokens: vi.fn().mockResolvedValue(1000),
|
||||
},
|
||||
getTokenUsage: vi.fn().mockReturnValue({
|
||||
contextTokens: 10000,
|
||||
}),
|
||||
apiConfiguration: {
|
||||
apiProvider: "anthropic",
|
||||
},
|
||||
providerRef: {
|
||||
deref: vi.fn().mockReturnValue({
|
||||
getState: vi.fn().mockResolvedValue({}),
|
||||
}),
|
||||
},
|
||||
} as any
|
||||
|
||||
// Mock getModelMaxOutputTokens to return a consistent value
|
||||
vi.mocked(sharedApi.getModelMaxOutputTokens).mockReturnValue(4096)
|
||||
})
|
||||
|
||||
describe("validateFileSizeForContext", () => {
|
||||
it("should apply 25% buffer to remaining context and read incrementally", async () => {
|
||||
const mockStats = { size: 50000 }
|
||||
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
|
||||
|
||||
// Mock readLines to return content in batches
|
||||
// Each batch is 100 lines, returning content that results in 1200 tokens per batch
|
||||
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
|
||||
const start = startLine ?? 0
|
||||
const end = endLine ?? 99
|
||||
const lines = end - start + 1
|
||||
return `test content line\n`.repeat(lines)
|
||||
})
|
||||
|
||||
// Mock token count - 12 tokens per line (1200 per 100-line batch)
|
||||
let callCount = 0
|
||||
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
|
||||
callCount++
|
||||
const text = content[0].text
|
||||
const lines = text.split("\n").length - 1
|
||||
return lines * 12 // 12 tokens per line
|
||||
})
|
||||
|
||||
const result = await validateFileSizeForContext(
|
||||
"/test/file.ts",
|
||||
1000, // totalLines
|
||||
-1, // currentMaxReadFileLine
|
||||
mockTask,
|
||||
)
|
||||
|
||||
// New calculation:
|
||||
// Context window = 100k, current usage = 10k
|
||||
// Remaining = 90k
|
||||
// With 25% buffer on remaining: usable = 90k * 0.75 = 67.5k
|
||||
// Reserved for response ~2k
|
||||
// Available should be around 65.5k tokens
|
||||
// File needs 12k tokens total (1000 lines * 12 tokens)
|
||||
expect(result.shouldLimit).toBe(false)
|
||||
|
||||
// Verify readLines was called multiple times (incremental reading)
|
||||
expect(readLines).toHaveBeenCalled()
|
||||
|
||||
// Verify the new calculation approach
|
||||
const remaining = 100000 - 10000 // 90k remaining
|
||||
const usableRemaining = remaining * 0.75 // 67.5k with 25% buffer
|
||||
expect(usableRemaining).toBe(67500)
|
||||
})
|
||||
|
||||
it("should handle different context usage levels correctly", async () => {
|
||||
const mockStats = { size: 50000 }
|
||||
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
|
||||
|
||||
// Mock readLines
|
||||
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
|
||||
const start = startLine ?? 0
|
||||
const end = endLine ?? 99
|
||||
const lines = end - start + 1
|
||||
return `test content line\n`.repeat(lines)
|
||||
})
|
||||
|
||||
// Mock token count - 50 tokens per line
|
||||
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
|
||||
const text = content[0].text
|
||||
const lines = text.split("\n").length - 1
|
||||
return lines * 50
|
||||
})
|
||||
|
||||
// Test with 50% context already used
|
||||
mockTask.getTokenUsage = vi.fn().mockReturnValue({
|
||||
contextTokens: 50000, // 50% of 100k context used
|
||||
})
|
||||
|
||||
const result = await validateFileSizeForContext(
|
||||
"/test/file.ts",
|
||||
2000, // totalLines
|
||||
-1,
|
||||
mockTask,
|
||||
)
|
||||
|
||||
// With 50k remaining and 25% buffer: 50k * 0.75 = 37.5k usable
|
||||
// Minus ~2k for response = ~35.5k available
|
||||
// File needs 100k tokens (2000 lines * 50 tokens)
|
||||
// Should limit the file
|
||||
expect(result.shouldLimit).toBe(true)
|
||||
expect(result.safeMaxLines).toBeLessThan(2000)
|
||||
expect(result.reason).toContain("exceeds available context space")
|
||||
})
|
||||
|
||||
it("should limit file when it exceeds available space with buffer", async () => {
|
||||
// Set up a scenario where file is too large
|
||||
const mockStats = { size: 500000 } // Large file
|
||||
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
|
||||
|
||||
// Mock readLines to return content in batches
|
||||
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
|
||||
const start = startLine ?? 0
|
||||
const end = endLine ?? 99
|
||||
const lines = end - start + 1
|
||||
return `large content line\n`.repeat(lines)
|
||||
})
|
||||
|
||||
// Mock large token count - 100 tokens per line
|
||||
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
|
||||
const text = content[0].text
|
||||
const lines = text.split("\n").length - 1
|
||||
return lines * 100 // 100 tokens per line
|
||||
})
|
||||
|
||||
const result = await validateFileSizeForContext(
|
||||
"/test/largefile.ts",
|
||||
10000, // totalLines
|
||||
-1,
|
||||
mockTask,
|
||||
)
|
||||
|
||||
expect(result.shouldLimit).toBe(true)
|
||||
expect(result.safeMaxLines).toBeGreaterThan(0)
|
||||
expect(result.safeMaxLines).toBeLessThan(10000) // Should stop before reading all lines
|
||||
expect(result.reason).toContain("exceeds available context space")
|
||||
})
|
||||
|
||||
it("should handle very large files through incremental reading", async () => {
|
||||
// Set up a file larger than 50MB
|
||||
const mockStats = { size: 60_000_000 } // 60MB file
|
||||
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
|
||||
|
||||
// Mock readLines to return content in batches
|
||||
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
|
||||
const start = startLine ?? 0
|
||||
const end = endLine ?? 99
|
||||
const lines = end - start + 1
|
||||
return `large file content line\n`.repeat(lines)
|
||||
})
|
||||
|
||||
// Mock very high token count per line (simulating dense content)
|
||||
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
|
||||
const text = content[0].text
|
||||
const lines = text.split("\n").length - 1
|
||||
return lines * 200 // 200 tokens per line for very large file
|
||||
})
|
||||
|
||||
const result = await validateFileSizeForContext(
|
||||
"/test/hugefile.ts",
|
||||
100000, // totalLines
|
||||
-1,
|
||||
mockTask,
|
||||
)
|
||||
|
||||
expect(result.shouldLimit).toBe(true)
|
||||
// Should have attempted to read the file incrementally
|
||||
expect(readLines).toHaveBeenCalled()
|
||||
// Should stop early due to token limits
|
||||
expect(result.safeMaxLines).toBeLessThan(1000)
|
||||
expect(result.reason).toContain("exceeds available context space")
|
||||
})
|
||||
|
||||
it("should handle read failures gracefully", async () => {
|
||||
const mockStats = { size: 100000 } // 100KB file
|
||||
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
|
||||
|
||||
// Mock readLines to fail
|
||||
vi.mocked(readLines).mockRejectedValue(new Error("Read error"))
|
||||
|
||||
const result = await validateFileSizeForContext(
|
||||
"/test/problematic.ts",
|
||||
2000, // totalLines
|
||||
-1,
|
||||
mockTask,
|
||||
)
|
||||
|
||||
// Should return a safe default when reading fails
|
||||
expect(result.shouldLimit).toBe(true)
|
||||
expect(result.safeMaxLines).toBe(50) // Minimum useful lines
|
||||
})
|
||||
|
||||
it("should handle very limited context space", async () => {
|
||||
const mockStats = { size: 10000 } // 10KB file
|
||||
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
|
||||
|
||||
// Set very high context usage
|
||||
// With new calculation: 100k - 95k = 5k remaining
|
||||
// 5k * 0.75 = 3.75k usable
|
||||
// Minus ~2k for response = ~1.75k available
|
||||
mockTask.getTokenUsage = vi.fn().mockReturnValue({
|
||||
contextTokens: 95000, // 95% of context used
|
||||
})
|
||||
|
||||
// Mock small token count
|
||||
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
|
||||
const text = content[0].text
|
||||
const lines = text.split("\n").length - 1
|
||||
return lines * 10 // 10 tokens per line
|
||||
})
|
||||
|
||||
// Mock readLines
|
||||
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
|
||||
const start = startLine ?? 0
|
||||
const end = endLine ?? 99
|
||||
const lines = end - start + 1
|
||||
return `test line\n`.repeat(lines)
|
||||
})
|
||||
|
||||
const result = await validateFileSizeForContext(
|
||||
"/test/smallfile.ts",
|
||||
500, // totalLines
|
||||
-1,
|
||||
mockTask,
|
||||
)
|
||||
|
||||
expect(result.shouldLimit).toBe(true)
|
||||
// With the new calculation using full model max tokens (4096),
|
||||
// we have less space available, so we get the minimum 50 lines
|
||||
expect(result.safeMaxLines).toBe(50)
|
||||
expect(result.reason).toContain("Very limited context space")
|
||||
})
|
||||
|
||||
it("should handle negative available space gracefully", async () => {
|
||||
const mockStats = { size: 10000 } // 10KB file
|
||||
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
|
||||
|
||||
// Set extremely high context usage
|
||||
// With 100k - 99k = 1k remaining
|
||||
// 1k * 0.75 = 750 tokens usable
|
||||
// Minus 2k for response = negative available space
|
||||
mockTask.getTokenUsage = vi.fn().mockReturnValue({
|
||||
contextTokens: 99000, // 99% of context used
|
||||
})
|
||||
|
||||
const result = await validateFileSizeForContext(
|
||||
"/test/smallfile.ts",
|
||||
500, // totalLines
|
||||
-1,
|
||||
mockTask,
|
||||
)
|
||||
|
||||
expect(result.shouldLimit).toBe(true)
|
||||
expect(result.safeMaxLines).toBe(50) // Should be limited to minimum useful lines
|
||||
expect(result.reason).toContain("Very limited context space")
|
||||
// With negative available space, readLines won't be called
|
||||
expect(readLines).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should limit file when it is too large and would be truncated", async () => {
|
||||
const filePath = "/test/large-file.ts"
|
||||
const totalLines = 10000
|
||||
const currentMaxReadFileLine = -1 // Unlimited
|
||||
|
||||
// Set up context to have limited space
|
||||
mockTask.getTokenUsage = vi.fn().mockReturnValue({
|
||||
contextTokens: 90000, // 90% of context used
|
||||
})
|
||||
|
||||
// Mock token counting to simulate a large file
|
||||
mockTask.api.countTokens = vi.fn().mockResolvedValue(1000) // Each batch is 1000 tokens
|
||||
|
||||
// Mock readLines to return some content
|
||||
vi.mocked(readLines).mockResolvedValue("line content")
|
||||
|
||||
const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
|
||||
|
||||
expect(result.shouldLimit).toBe(true)
|
||||
expect(result.safeMaxLines).toBeGreaterThan(0)
|
||||
expect(result.safeMaxLines).toBeLessThan(totalLines)
|
||||
expect(result.reason).toContain("File exceeds available context space")
|
||||
expect(result.reason).toContain("Use line_range to read specific sections")
|
||||
})
|
||||
|
||||
it("should limit file when very limited context space", async () => {
|
||||
const filePath = "/test/file.ts"
|
||||
const totalLines = 1000
|
||||
const currentMaxReadFileLine = -1
|
||||
|
||||
// Mock very high token usage leaving little room
|
||||
mockTask.getTokenUsage = vi.fn().mockReturnValue({
|
||||
contextTokens: 98000, // Almost all context used (98% of 100k)
|
||||
})
|
||||
|
||||
// Mock token counting to quickly exceed limit
|
||||
mockTask.api.countTokens = vi.fn().mockResolvedValue(500) // Each batch uses a lot of tokens
|
||||
|
||||
vi.mocked(readLines).mockResolvedValue("line content")
|
||||
|
||||
const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
|
||||
|
||||
expect(result.shouldLimit).toBe(true)
|
||||
expect(result.reason).toContain("Very limited context space")
|
||||
expect(result.reason).toContain("Consider using search_files or line_range")
|
||||
})
|
||||
|
||||
it("should not limit when file fits within context", async () => {
|
||||
const filePath = "/test/small-file.ts"
|
||||
const totalLines = 100
|
||||
const currentMaxReadFileLine = -1
|
||||
|
||||
// Mock low token usage
|
||||
mockTask.api.countTokens = vi.fn().mockResolvedValue(10) // Small token count per batch
|
||||
|
||||
vi.mocked(readLines).mockResolvedValue("line content")
|
||||
|
||||
const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
|
||||
|
||||
expect(result.shouldLimit).toBe(false)
|
||||
expect(result.safeMaxLines).toBe(currentMaxReadFileLine)
|
||||
})
|
||||
|
||||
it("should handle errors gracefully", async () => {
|
||||
const filePath = "/test/error-file.ts"
|
||||
const totalLines = 20000 // Large file
|
||||
const currentMaxReadFileLine = -1
|
||||
|
||||
// Mock an error in the API
|
||||
mockTask.api.getModel = vi.fn().mockImplementation(() => {
|
||||
throw new Error("API Error")
|
||||
})
|
||||
|
||||
const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
|
||||
|
||||
// Should fall back to conservative limits
|
||||
expect(result.shouldLimit).toBe(true)
|
||||
expect(result.safeMaxLines).toBe(1000)
|
||||
expect(result.reason).toContain("Large file detected")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -4,12 +4,13 @@ import * as path from "path"
|
|||
|
||||
import { countFileLines } from "../../../integrations/misc/line-counter"
|
||||
import { readLines } from "../../../integrations/misc/read-lines"
|
||||
import { extractTextFromFile } from "../../../integrations/misc/extract-text"
|
||||
import { extractTextFromFile, addLineNumbers } from "../../../integrations/misc/extract-text"
|
||||
import { parseSourceCodeDefinitionsForFile } from "../../../services/tree-sitter"
|
||||
import { isBinaryFile } from "isbinaryfile"
|
||||
import { ReadFileToolUse, ToolParamName, ToolResponse } from "../../../shared/tools"
|
||||
import { readFileTool } from "../readFileTool"
|
||||
import { formatResponse } from "../../prompts/responses"
|
||||
import * as contextValidatorModule from "../contextValidator"
|
||||
|
||||
vi.mock("path", async () => {
|
||||
const originalPath = await vi.importActual("path")
|
||||
|
|
@ -30,6 +31,7 @@ vi.mock("isbinaryfile")
|
|||
|
||||
vi.mock("../../../integrations/misc/line-counter")
|
||||
vi.mock("../../../integrations/misc/read-lines")
|
||||
vi.mock("../contextValidator")
|
||||
|
||||
// Mock input content for tests
|
||||
let mockInputContent = ""
|
||||
|
|
@ -90,6 +92,12 @@ describe("read_file tool with maxReadFileLine setting", () => {
|
|||
mockedPathResolve.mockReturnValue(absoluteFilePath)
|
||||
mockedIsBinaryFile.mockResolvedValue(false)
|
||||
|
||||
// Default mock for validateFileSizeForContext - no limit
|
||||
vi.mocked(contextValidatorModule.validateFileSizeForContext).mockResolvedValue({
|
||||
shouldLimit: false,
|
||||
safeMaxLines: -1,
|
||||
})
|
||||
|
||||
mockInputContent = fileContent
|
||||
|
||||
// Setup the extractTextFromFile mock implementation with the current mockInputContent
|
||||
|
|
@ -519,4 +527,67 @@ describe("read_file tool XML output structure", () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("line range instructions", () => {
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Mock file system functions
|
||||
vi.mocked(isBinaryFile).mockResolvedValue(false)
|
||||
vi.mocked(countFileLines).mockResolvedValue(10000) // Large file
|
||||
vi.mocked(readLines).mockResolvedValue("line content")
|
||||
vi.mocked(extractTextFromFile).mockResolvedValue("file content")
|
||||
|
||||
// Mock addLineNumbers
|
||||
vi.mocked(addLineNumbers).mockImplementation((content, start) => `${start || 1} | ${content}`)
|
||||
})
|
||||
|
||||
it("should always include inline line_range instructions when shouldLimit is true", async () => {
|
||||
// Mock a large file
|
||||
vi.mocked(countFileLines).mockResolvedValue(10000)
|
||||
|
||||
// Mock contextValidator to return shouldLimit true
|
||||
vi.mocked(contextValidatorModule.validateFileSizeForContext).mockResolvedValue({
|
||||
shouldLimit: true,
|
||||
safeMaxLines: 2000,
|
||||
reason: "File exceeds available context space",
|
||||
})
|
||||
|
||||
// Mock readLines to return truncated content
|
||||
vi.mocked(readLines).mockResolvedValue("Line 1\nLine 2\n...truncated...")
|
||||
|
||||
const result = await executeReadFileTool(
|
||||
{ args: `<file><path>large-file.ts</path></file>` },
|
||||
{ totalLines: 10000, maxReadFileLine: -1 },
|
||||
)
|
||||
|
||||
// Verify the result contains the inline instructions
|
||||
expect(result).toContain("<notice>")
|
||||
expect(result).toContain("File exceeds available context space")
|
||||
expect(result).toContain("To read specific sections of this file, use the following format:")
|
||||
expect(result).toContain("<line_range>start-end</line_range>")
|
||||
expect(result).toContain("For example, to read lines 2001-3000:")
|
||||
expect(result).toContain("<line_range>2001-3000</line_range>")
|
||||
expect(result).toContain("large-file.ts")
|
||||
})
|
||||
|
||||
it("should not show any special notice when file fits in context", async () => {
|
||||
// Mock small file that fits in context
|
||||
vi.mocked(countFileLines).mockResolvedValue(100)
|
||||
vi.mocked(contextValidatorModule.validateFileSizeForContext).mockResolvedValue({
|
||||
shouldLimit: false,
|
||||
safeMaxLines: -1,
|
||||
})
|
||||
|
||||
const result = await executeReadFileTool({ args: `<file><path>small-file.ts</path></file>` })
|
||||
|
||||
// Should have file content but no notice about limits
|
||||
expect(result).toContain("<file>")
|
||||
expect(result).toContain("<path>small-file.ts</path>")
|
||||
expect(result).toContain("<content")
|
||||
expect(result).not.toContain("Use line_range")
|
||||
expect(result).not.toContain("File exceeds available context space")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
171
src/core/tools/contextValidator.ts
Normal file
171
src/core/tools/contextValidator.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { promises as fs } from "fs"
|
||||
import { Task } from "../task/Task"
|
||||
import { readLines } from "../../integrations/misc/read-lines"
|
||||
import { getModelMaxOutputTokens } from "../../shared/api"
|
||||
|
||||
/**
|
||||
* More aggressive buffer percentage specifically for file reading validation.
|
||||
* This is separate from the global TOKEN_BUFFER_PERCENTAGE to provide extra safety
|
||||
* when reading files without affecting other context window calculations.
|
||||
*/
|
||||
const FILE_READ_BUFFER_PERCENTAGE = 0.25 // 25% buffer for file reads
|
||||
|
||||
export interface ContextValidationResult {
|
||||
shouldLimit: boolean
|
||||
safeMaxLines: number
|
||||
reason?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a file can be safely read based on its size and current runtime context state.
|
||||
* Reads lines incrementally and counts tokens as it goes, stopping when reaching the token limit.
|
||||
* Returns a safe maxReadFileLine value to prevent context overflow.
|
||||
*/
|
||||
export async function validateFileSizeForContext(
|
||||
filePath: string,
|
||||
totalLines: number,
|
||||
currentMaxReadFileLine: number,
|
||||
cline: Task,
|
||||
): Promise<ContextValidationResult> {
|
||||
try {
|
||||
// Get actual runtime state from the task
|
||||
const modelInfo = cline.api.getModel().info
|
||||
const { contextTokens: currentContextTokens } = cline.getTokenUsage()
|
||||
const contextWindow = modelInfo.contextWindow
|
||||
|
||||
// Get the model-specific max output tokens using the same logic as sliding window
|
||||
const modelId = cline.api.getModel().id
|
||||
const apiProvider = cline.apiConfiguration.apiProvider
|
||||
const settings = await cline.providerRef.deref()?.getState()
|
||||
|
||||
// Map apiProvider to the format expected by getModelMaxOutputTokens
|
||||
let format: "anthropic" | "openai" | "gemini" | "openrouter" | undefined
|
||||
if (
|
||||
apiProvider === "anthropic" ||
|
||||
apiProvider === "bedrock" ||
|
||||
apiProvider === "vertex" ||
|
||||
apiProvider === "claude-code"
|
||||
) {
|
||||
format = "anthropic"
|
||||
} else if (apiProvider === "openrouter") {
|
||||
format = "openrouter"
|
||||
} else if (apiProvider === "openai" || apiProvider === "openai-native") {
|
||||
format = "openai"
|
||||
} else if (apiProvider === "gemini" || apiProvider === "gemini-cli") {
|
||||
format = "gemini"
|
||||
}
|
||||
|
||||
const maxResponseTokens = getModelMaxOutputTokens({ modelId, model: modelInfo, settings, format })
|
||||
|
||||
// Calculate how much context is already used
|
||||
const currentlyUsed = currentContextTokens || 0
|
||||
|
||||
// Calculate remaining context space
|
||||
const remainingContext = contextWindow - currentlyUsed
|
||||
|
||||
// Apply buffer to the remaining context, not the total context window
|
||||
// This gives us a more accurate assessment of what's actually available
|
||||
const usableRemainingContext = Math.floor(remainingContext * (1 - FILE_READ_BUFFER_PERCENTAGE))
|
||||
|
||||
// Use the same approach as sliding window: reserve the model's max tokens
|
||||
// This ensures consistency across the codebase
|
||||
const reservedForResponse = maxResponseTokens || 0
|
||||
|
||||
// Calculate available tokens for file content
|
||||
const availableTokensForFile = usableRemainingContext - reservedForResponse
|
||||
|
||||
// Now read lines incrementally and count tokens until we reach the limit
|
||||
const BATCH_SIZE = 100 // Read 100 lines at a time
|
||||
let currentLine = 0
|
||||
let totalTokensSoFar = 0
|
||||
let safeMaxLines = 0
|
||||
|
||||
// Use 90% of available space to leave some margin
|
||||
const targetTokenLimit = Math.floor(availableTokensForFile * 0.9)
|
||||
|
||||
while (currentLine < totalLines && totalTokensSoFar < targetTokenLimit) {
|
||||
// Calculate the end line for this batch
|
||||
const batchEndLine = Math.min(currentLine + BATCH_SIZE - 1, totalLines - 1)
|
||||
|
||||
try {
|
||||
// Read the next batch of lines
|
||||
const batchContent = await readLines(filePath, batchEndLine, currentLine)
|
||||
|
||||
// Count tokens for this batch
|
||||
const batchTokens = await cline.api.countTokens([{ type: "text", text: batchContent }])
|
||||
|
||||
// Check if adding this batch would exceed our limit
|
||||
if (totalTokensSoFar + batchTokens > targetTokenLimit) {
|
||||
// This batch would exceed the limit
|
||||
// Try to find a more precise cutoff within this batch
|
||||
if (batchEndLine - currentLine > 10) {
|
||||
// Read smaller chunks to find a more precise cutoff
|
||||
const FINE_BATCH_SIZE = 10
|
||||
let fineLine = currentLine
|
||||
|
||||
while (fineLine <= batchEndLine && totalTokensSoFar < targetTokenLimit) {
|
||||
const fineEndLine = Math.min(fineLine + FINE_BATCH_SIZE - 1, batchEndLine)
|
||||
const fineContent = await readLines(filePath, fineEndLine, fineLine)
|
||||
const fineTokens = await cline.api.countTokens([{ type: "text", text: fineContent }])
|
||||
|
||||
if (totalTokensSoFar + fineTokens > targetTokenLimit) {
|
||||
// Even this fine batch exceeds the limit
|
||||
break
|
||||
}
|
||||
|
||||
totalTokensSoFar += fineTokens
|
||||
safeMaxLines = fineEndLine + 1 // Convert to 1-based line count
|
||||
fineLine = fineEndLine + 1
|
||||
}
|
||||
}
|
||||
// Stop processing more batches
|
||||
break
|
||||
}
|
||||
|
||||
// Add this batch's tokens to our total
|
||||
totalTokensSoFar += batchTokens
|
||||
safeMaxLines = batchEndLine + 1 // Convert to 1-based line count
|
||||
currentLine = batchEndLine + 1
|
||||
} catch (error) {
|
||||
// If we encounter an error reading a batch, stop here
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we provide at least a minimum useful amount
|
||||
const minUsefulLines = 50
|
||||
const finalSafeMaxLines = Math.max(minUsefulLines, safeMaxLines)
|
||||
|
||||
// If we read the entire file without exceeding the limit, no limitation needed
|
||||
if (safeMaxLines >= totalLines) {
|
||||
return { shouldLimit: false, safeMaxLines: currentMaxReadFileLine }
|
||||
}
|
||||
|
||||
// If we couldn't read even the minimum useful lines
|
||||
if (safeMaxLines < minUsefulLines) {
|
||||
return {
|
||||
shouldLimit: true,
|
||||
safeMaxLines: finalSafeMaxLines,
|
||||
reason: `Very limited context space. Could only safely read ${safeMaxLines} lines before exceeding token limit. Context: ${currentlyUsed}/${contextWindow} tokens used (${Math.round((currentlyUsed / contextWindow) * 100)}%). Limited to ${finalSafeMaxLines} lines. Consider using search_files or line_range for specific sections.`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
shouldLimit: true,
|
||||
safeMaxLines: finalSafeMaxLines,
|
||||
reason: `File exceeds available context space. Safely read ${finalSafeMaxLines} lines (${totalTokensSoFar} tokens) out of ${totalLines} total lines. Context usage: ${currentlyUsed}/${contextWindow} tokens (${Math.round((currentlyUsed / contextWindow) * 100)}%). Use line_range to read specific sections.`,
|
||||
}
|
||||
} catch (error) {
|
||||
// If we can't get runtime state, fall back to conservative estimation
|
||||
console.warn(`[validateFileSizeForContext] Error accessing runtime state: ${error}`)
|
||||
|
||||
if (totalLines > 10000) {
|
||||
return {
|
||||
shouldLimit: true,
|
||||
safeMaxLines: 1000,
|
||||
reason: "Large file detected (>10,000 lines). Limited to 1000 lines to prevent context overflow (runtime state unavailable).",
|
||||
}
|
||||
}
|
||||
return { shouldLimit: false, safeMaxLines: currentMaxReadFileLine }
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import path from "path"
|
|||
import { isBinaryFile } from "isbinaryfile"
|
||||
|
||||
import { Task } from "../task/Task"
|
||||
import { validateFileSizeForContext } from "./contextValidator"
|
||||
import { ClineSayTool } from "../../shared/ExtensionMessage"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { t } from "../../i18n"
|
||||
|
|
@ -435,6 +436,21 @@ export async function readFileTool(
|
|||
try {
|
||||
const [totalLines, isBinary] = await Promise.all([countFileLines(fullPath), isBinaryFile(fullPath)])
|
||||
|
||||
// Preemptive file size validation to prevent context overflow
|
||||
const validation = await validateFileSizeForContext(fullPath, totalLines, maxReadFileLine, cline)
|
||||
let effectiveMaxReadFileLine = maxReadFileLine
|
||||
let validationNotice = ""
|
||||
|
||||
if (validation.shouldLimit && maxReadFileLine === -1) {
|
||||
// Only apply limitation if maxReadFileLine is -1 (unlimited)
|
||||
// If user has already set a limit, respect their choice
|
||||
effectiveMaxReadFileLine = validation.safeMaxLines
|
||||
validationNotice = validation.reason || ""
|
||||
console.log(
|
||||
`[read_file] Applied preemptive size limit to ${relPath}: ${validation.safeMaxLines} lines`,
|
||||
)
|
||||
}
|
||||
|
||||
// Handle binary files (but allow specific file types that extractTextFromFile can handle)
|
||||
if (isBinary) {
|
||||
const fileExtension = path.extname(relPath).toLowerCase()
|
||||
|
|
@ -468,11 +484,11 @@ export async function readFileTool(
|
|||
}
|
||||
|
||||
// Handle definitions-only mode
|
||||
if (maxReadFileLine === 0) {
|
||||
if (effectiveMaxReadFileLine === 0) {
|
||||
try {
|
||||
const defResult = await parseSourceCodeDefinitionsForFile(fullPath, cline.rooIgnoreController)
|
||||
if (defResult) {
|
||||
let xmlInfo = `<notice>Showing only ${maxReadFileLine} of ${totalLines} total lines. Use line_range if you need to read more lines</notice>\n`
|
||||
let xmlInfo = `<notice>Showing only ${effectiveMaxReadFileLine} of ${totalLines} total lines. Use line_range if you need to read more lines</notice>\n`
|
||||
updateFileResult(relPath, {
|
||||
xmlContent: `<file><path>${relPath}</path>\n<list_code_definition_names>${defResult}</list_code_definition_names>\n${xmlInfo}</file>`,
|
||||
})
|
||||
|
|
@ -489,10 +505,10 @@ export async function readFileTool(
|
|||
continue
|
||||
}
|
||||
|
||||
// Handle files exceeding line threshold
|
||||
if (maxReadFileLine > 0 && totalLines > maxReadFileLine) {
|
||||
const content = addLineNumbers(await readLines(fullPath, maxReadFileLine - 1, 0))
|
||||
const lineRangeAttr = ` lines="1-${maxReadFileLine}"`
|
||||
// Handle files exceeding line threshold (including preemptive limits)
|
||||
if (effectiveMaxReadFileLine > 0 && totalLines > effectiveMaxReadFileLine) {
|
||||
const content = addLineNumbers(await readLines(fullPath, effectiveMaxReadFileLine - 1, 0))
|
||||
const lineRangeAttr = ` lines="1-${effectiveMaxReadFileLine}"`
|
||||
let xmlInfo = `<content${lineRangeAttr}>\n${content}</content>\n`
|
||||
|
||||
try {
|
||||
|
|
@ -500,7 +516,15 @@ export async function readFileTool(
|
|||
if (defResult) {
|
||||
xmlInfo += `<list_code_definition_names>${defResult}</list_code_definition_names>\n`
|
||||
}
|
||||
xmlInfo += `<notice>Showing only ${maxReadFileLine} of ${totalLines} total lines. Use line_range if you need to read more lines</notice>\n`
|
||||
|
||||
// Add appropriate notice based on whether this was a preemptive limit or user setting
|
||||
if (validationNotice) {
|
||||
// When shouldLimit is true, always provide inline instructions
|
||||
xmlInfo += `<notice>${validationNotice}\n\nTo read specific sections of this file, use the following format:\n<read_file>\n<args>\n <file>\n <path>${relPath}</path>\n <line_range>start-end</line_range>\n </file>\n</args>\n</read_file>\n\nFor example, to read lines 2001-3000:\n<read_file>\n<args>\n <file>\n <path>${relPath}</path>\n <line_range>2001-3000</line_range>\n </file>\n</args>\n</read_file></notice>\n`
|
||||
} else {
|
||||
xmlInfo += `<notice>Showing only ${effectiveMaxReadFileLine} of ${totalLines} total lines. Use line_range if you need to read more lines</notice>\n`
|
||||
}
|
||||
|
||||
updateFileResult(relPath, {
|
||||
xmlContent: `<file><path>${relPath}</path>\n${xmlInfo}</file>`,
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue