fix: add maxConcurrentFileReads limit to native read_file tool schema (#10449)

Co-authored-by: Roo Code <roomote@roocode.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
This commit is contained in:
roomote[bot] 2026-01-03 19:08:37 -05:00 committed by GitHub
parent 1459f0113b
commit c307dc7756
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 218 additions and 16 deletions

View file

@ -0,0 +1,158 @@
import type OpenAI from "openai"
import { createReadFileTool, type ReadFileToolOptions } from "../read_file"
// Helper type to access function tools
type FunctionTool = OpenAI.Chat.ChatCompletionTool & { type: "function" }
// Helper to get function definition from tool
const getFunctionDef = (tool: OpenAI.Chat.ChatCompletionTool) => (tool as FunctionTool).function
describe("createReadFileTool", () => {
describe("maxConcurrentFileReads documentation", () => {
it("should include default maxConcurrentFileReads limit (5) in description", () => {
const tool = createReadFileTool()
const description = getFunctionDef(tool).description
expect(description).toContain("maximum of 5 files")
expect(description).toContain("If you need to read more files, use multiple sequential read_file requests")
})
it("should include custom maxConcurrentFileReads limit in description", () => {
const tool = createReadFileTool({ maxConcurrentFileReads: 3 })
const description = getFunctionDef(tool).description
expect(description).toContain("maximum of 3 files")
expect(description).toContain("within 3-file limit")
})
it("should indicate single file reads only when maxConcurrentFileReads is 1", () => {
const tool = createReadFileTool({ maxConcurrentFileReads: 1 })
const description = getFunctionDef(tool).description
expect(description).toContain("Multiple file reads are currently disabled")
expect(description).toContain("only read one file at a time")
expect(description).not.toContain("Example multiple files")
})
it("should use singular 'Read a file' in base description when maxConcurrentFileReads is 1", () => {
const tool = createReadFileTool({ maxConcurrentFileReads: 1 })
const description = getFunctionDef(tool).description
expect(description).toMatch(/^Read a file/)
expect(description).not.toContain("Read one or more files")
})
it("should use plural 'Read one or more files' in base description when maxConcurrentFileReads is > 1", () => {
const tool = createReadFileTool({ maxConcurrentFileReads: 5 })
const description = getFunctionDef(tool).description
expect(description).toMatch(/^Read one or more files/)
})
it("should not show multiple files example when maxConcurrentFileReads is 1", () => {
const tool = createReadFileTool({ maxConcurrentFileReads: 1, partialReadsEnabled: true })
const description = getFunctionDef(tool).description
expect(description).not.toContain("Example multiple files")
})
it("should show multiple files example when maxConcurrentFileReads is > 1", () => {
const tool = createReadFileTool({ maxConcurrentFileReads: 5, partialReadsEnabled: true })
const description = getFunctionDef(tool).description
expect(description).toContain("Example multiple files")
})
})
describe("partialReadsEnabled option", () => {
it("should include line_ranges in description when partialReadsEnabled is true", () => {
const tool = createReadFileTool({ partialReadsEnabled: true })
const description = getFunctionDef(tool).description
expect(description).toContain("line_ranges")
expect(description).toContain("Example with line ranges")
})
it("should not include line_ranges in description when partialReadsEnabled is false", () => {
const tool = createReadFileTool({ partialReadsEnabled: false })
const description = getFunctionDef(tool).description
expect(description).not.toContain("line_ranges")
expect(description).not.toContain("Example with line ranges")
})
it("should include line_ranges parameter in schema when partialReadsEnabled is true", () => {
const tool = createReadFileTool({ partialReadsEnabled: true })
const schema = getFunctionDef(tool).parameters as any
expect(schema.properties.files.items.properties).toHaveProperty("line_ranges")
})
it("should not include line_ranges parameter in schema when partialReadsEnabled is false", () => {
const tool = createReadFileTool({ partialReadsEnabled: false })
const schema = getFunctionDef(tool).parameters as any
expect(schema.properties.files.items.properties).not.toHaveProperty("line_ranges")
})
})
describe("combined options", () => {
it("should correctly combine low maxConcurrentFileReads with partialReadsEnabled", () => {
const tool = createReadFileTool({
maxConcurrentFileReads: 2,
partialReadsEnabled: true,
})
const description = getFunctionDef(tool).description
expect(description).toContain("maximum of 2 files")
expect(description).toContain("line_ranges")
expect(description).toContain("within 2-file limit")
})
it("should correctly handle maxConcurrentFileReads of 1 with partialReadsEnabled false", () => {
const tool = createReadFileTool({
maxConcurrentFileReads: 1,
partialReadsEnabled: false,
})
const description = getFunctionDef(tool).description
expect(description).toContain("only read one file at a time")
expect(description).not.toContain("line_ranges")
expect(description).not.toContain("Example multiple files")
})
})
describe("tool structure", () => {
it("should have correct tool name", () => {
const tool = createReadFileTool()
expect(getFunctionDef(tool).name).toBe("read_file")
})
it("should be a function type tool", () => {
const tool = createReadFileTool()
expect(tool.type).toBe("function")
})
it("should have strict mode enabled", () => {
const tool = createReadFileTool()
expect(getFunctionDef(tool).strict).toBe(true)
})
it("should require files parameter", () => {
const tool = createReadFileTool()
const schema = getFunctionDef(tool).parameters as any
expect(schema.required).toContain("files")
})
it("should require path in file objects", () => {
const tool = createReadFileTool({ partialReadsEnabled: false })
const schema = getFunctionDef(tool).parameters as any
expect(schema.properties.files.items.required).toContain("path")
})
})
})

View file

@ -11,7 +11,7 @@ import fetchInstructions from "./fetch_instructions"
import generateImage from "./generate_image"
import listFiles from "./list_files"
import newTask from "./new_task"
import { createReadFileTool } from "./read_file"
import { createReadFileTool, type ReadFileToolOptions } from "./read_file"
import runSlashCommand from "./run_slash_command"
import searchAndReplace from "./search_and_replace"
import searchReplace from "./search_replace"
@ -23,14 +23,32 @@ import writeToFile from "./write_to_file"
export { getMcpServerTools } from "./mcp_server"
export { convertOpenAIToolToAnthropic, convertOpenAIToolsToAnthropic } from "./converters"
export type { ReadFileToolOptions } from "./read_file"
/**
* Options for customizing the native tools array.
*/
export interface NativeToolsOptions {
/** Whether to include line_ranges support in read_file tool (default: true) */
partialReadsEnabled?: boolean
/** Maximum number of files that can be read in a single read_file request (default: 5) */
maxConcurrentFileReads?: number
}
/**
* Get native tools array, optionally customizing based on settings.
*
* @param partialReadsEnabled - Whether to include line_ranges support in read_file tool (default: true)
* @param options - Configuration options for the tools
* @returns Array of native tool definitions
*/
export function getNativeTools(partialReadsEnabled: boolean = true): OpenAI.Chat.ChatCompletionTool[] {
export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.ChatCompletionTool[] {
const { partialReadsEnabled = true, maxConcurrentFileReads = 5 } = options
const readFileOptions: ReadFileToolOptions = {
partialReadsEnabled,
maxConcurrentFileReads,
}
return [
accessMcpResource,
apply_diff,
@ -44,7 +62,7 @@ export function getNativeTools(partialReadsEnabled: boolean = true): OpenAI.Chat
generateImage,
listFiles,
newTask,
createReadFileTool(partialReadsEnabled),
createReadFileTool(readFileOptions),
runSlashCommand,
searchAndReplace,
searchReplace,
@ -57,4 +75,4 @@ export function getNativeTools(partialReadsEnabled: boolean = true): OpenAI.Chat
}
// Backward compatibility: export default tools with line ranges enabled
export const nativeTools = getNativeTools(true)
export const nativeTools = getNativeTools()

View file

@ -1,20 +1,36 @@
import type OpenAI from "openai"
const READ_FILE_BASE_DESCRIPTION = `Read one or more files and return their contents with line numbers for diffing or discussion.`
const READ_FILE_SUPPORTS_NOTE = `Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.`
/**
* Options for creating the read_file tool definition.
*/
export interface ReadFileToolOptions {
/** Whether to include line_ranges parameter (default: true) */
partialReadsEnabled?: boolean
/** Maximum number of files that can be read in a single request (default: 5) */
maxConcurrentFileReads?: number
}
/**
* Creates the read_file tool definition, optionally including line_ranges support
* based on whether partial reads are enabled.
*
* @param partialReadsEnabled - Whether to include line_ranges parameter
* @param options - Configuration options for the tool
* @returns Native tool definition for read_file
*/
export function createReadFileTool(partialReadsEnabled: boolean = true): OpenAI.Chat.ChatCompletionTool {
export function createReadFileTool(options: ReadFileToolOptions = {}): OpenAI.Chat.ChatCompletionTool {
const { partialReadsEnabled = true, maxConcurrentFileReads = 5 } = options
const isMultipleReadsEnabled = maxConcurrentFileReads > 1
// Build description intro with concurrent reads limit message
const descriptionIntro = isMultipleReadsEnabled
? `Read one or more files and return their contents with line numbers for diffing or discussion. IMPORTANT: You can read a maximum of ${maxConcurrentFileReads} files in a single request. If you need to read more files, use multiple sequential read_file requests. `
: "Read a file and return its contents with line numbers for diffing or discussion. IMPORTANT: Multiple file reads are currently disabled. You can only read one file at a time. "
const baseDescription =
READ_FILE_BASE_DESCRIPTION +
" Structure: { files: [{ path: 'relative/path.ts'" +
descriptionIntro +
"Structure: { files: [{ path: 'relative/path.ts'" +
(partialReadsEnabled ? ", line_ranges: [[1, 50], [100, 150]]" : "") +
" }] }. " +
"The 'path' is required and relative to workspace. "
@ -26,9 +42,13 @@ export function createReadFileTool(partialReadsEnabled: boolean = true): OpenAI.
const examples = partialReadsEnabled
? "Example single file: { files: [{ path: 'src/app.ts' }] }. " +
"Example with line ranges: { files: [{ path: 'src/app.ts', line_ranges: [[1, 50], [100, 150]] }] }. " +
"Example multiple files: { files: [{ path: 'file1.ts', line_ranges: [[1, 50]] }, { path: 'file2.ts' }] }"
(isMultipleReadsEnabled
? `Example multiple files (within ${maxConcurrentFileReads}-file limit): { files: [{ path: 'file1.ts', line_ranges: [[1, 50]] }, { path: 'file2.ts' }] }`
: "")
: "Example single file: { files: [{ path: 'src/app.ts' }] }. " +
"Example multiple files: { files: [{ path: 'file1.ts' }, { path: 'file2.ts' }] }"
(isMultipleReadsEnabled
? `Example multiple files (within ${maxConcurrentFileReads}-file limit): { files: [{ path: 'file1.ts' }, { path: 'file2.ts' }] }`
: "")
const description = baseDescription + optionalRangesDescription + READ_FILE_SUPPORTS_NOTE + " " + examples
@ -87,4 +107,4 @@ export function createReadFileTool(partialReadsEnabled: boolean = true): OpenAI.
} satisfies OpenAI.Chat.ChatCompletionTool
}
export const read_file = createReadFileTool(false)
export const read_file = createReadFileTool({ partialReadsEnabled: false })

View file

@ -3924,6 +3924,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
experiments: state?.experiments,
apiConfiguration,
maxReadFileLine: state?.maxReadFileLine ?? -1,
maxConcurrentFileReads: state?.maxConcurrentFileReads ?? 5,
browserToolEnabled: state?.browserToolEnabled ?? true,
modelInfo,
diffEnabled: this.diffEnabled,

View file

@ -19,6 +19,7 @@ interface BuildToolsOptions {
experiments: Record<string, boolean> | undefined
apiConfiguration: ProviderSettings | undefined
maxReadFileLine: number
maxConcurrentFileReads: number
browserToolEnabled: boolean
modelInfo?: ModelInfo
diffEnabled: boolean
@ -40,6 +41,7 @@ export async function buildNativeToolsArray(options: BuildToolsOptions): Promise
experiments,
apiConfiguration,
maxReadFileLine,
maxConcurrentFileReads,
browserToolEnabled,
modelInfo,
diffEnabled,
@ -62,8 +64,11 @@ export async function buildNativeToolsArray(options: BuildToolsOptions): Promise
// Determine if partial reads are enabled based on maxReadFileLine setting.
const partialReadsEnabled = maxReadFileLine !== -1
// Build native tools with dynamic read_file tool based on partialReadsEnabled.
const nativeTools = getNativeTools(partialReadsEnabled)
// Build native tools with dynamic read_file tool based on settings.
const nativeTools = getNativeTools({
partialReadsEnabled,
maxConcurrentFileReads,
})
// Filter native tools based on mode restrictions.
const filteredNativeTools = filterNativeToolsForMode(