Procházet zdrojové kódy

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

Co-authored-by: Roo Code <[email protected]>
Co-authored-by: Matt Rubens <[email protected]>
roomote[bot] před 3 týdny
rodič
revize
c307dc7756

+ 158 - 0
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")
+		})
+	})
+})

+ 23 - 5
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()

+ 29 - 9
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 })

+ 1 - 0
src/core/task/Task.ts

@@ -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,

+ 7 - 2
src/core/task/build-tools.ts

@@ -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(