Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions src/core/prompts/tools/native-tools/__tests__/read_file.spec.ts
Original file line number Diff line number Diff line change
@@ -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")
})
})
})
28 changes: 23 additions & 5 deletions src/core/prompts/tools/native-tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand All @@ -44,7 +62,7 @@ export function getNativeTools(partialReadsEnabled: boolean = true): OpenAI.Chat
generateImage,
listFiles,
newTask,
createReadFileTool(partialReadsEnabled),
createReadFileTool(readFileOptions),
runSlashCommand,
searchAndReplace,
searchReplace,
Expand All @@ -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()
38 changes: 29 additions & 9 deletions src/core/prompts/tools/native-tools/read_file.ts
Original file line number Diff line number Diff line change
@@ -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. "
Expand All @@ -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

Expand Down Expand Up @@ -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 change: 1 addition & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions src/core/task/build-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface BuildToolsOptions {
experiments: Record<string, boolean> | undefined
apiConfiguration: ProviderSettings | undefined
maxReadFileLine: number
maxConcurrentFileReads: number
browserToolEnabled: boolean
modelInfo?: ModelInfo
diffEnabled: boolean
Expand All @@ -40,6 +41,7 @@ export async function buildNativeToolsArray(options: BuildToolsOptions): Promise
experiments,
apiConfiguration,
maxReadFileLine,
maxConcurrentFileReads,
browserToolEnabled,
modelInfo,
diffEnabled,
Expand All @@ -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(
Expand Down
Loading