diff --git a/src/core/prompts/tools/native-tools/__tests__/read_file.spec.ts b/src/core/prompts/tools/native-tools/__tests__/read_file.spec.ts new file mode 100644 index 00000000000..92c2c9f5db6 --- /dev/null +++ b/src/core/prompts/tools/native-tools/__tests__/read_file.spec.ts @@ -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") + }) + }) +}) diff --git a/src/core/prompts/tools/native-tools/index.ts b/src/core/prompts/tools/native-tools/index.ts index 79302a39f31..aaa89087336 100644 --- a/src/core/prompts/tools/native-tools/index.ts +++ b/src/core/prompts/tools/native-tools/index.ts @@ -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() diff --git a/src/core/prompts/tools/native-tools/read_file.ts b/src/core/prompts/tools/native-tools/read_file.ts index bf43f26c8af..cfb0b8bbe11 100644 --- a/src/core/prompts/tools/native-tools/read_file.ts +++ b/src/core/prompts/tools/native-tools/read_file.ts @@ -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 }) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 8b14327289a..c1550bdd5a0 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -3924,6 +3924,7 @@ export class Task extends EventEmitter implements TaskLike { experiments: state?.experiments, apiConfiguration, maxReadFileLine: state?.maxReadFileLine ?? -1, + maxConcurrentFileReads: state?.maxConcurrentFileReads ?? 5, browserToolEnabled: state?.browserToolEnabled ?? true, modelInfo, diffEnabled: this.diffEnabled, diff --git a/src/core/task/build-tools.ts b/src/core/task/build-tools.ts index 8eea4ace82c..d3e43472b6c 100644 --- a/src/core/task/build-tools.ts +++ b/src/core/task/build-tools.ts @@ -19,6 +19,7 @@ interface BuildToolsOptions { experiments: Record | 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(