Skip to content
Closed
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
51 changes: 51 additions & 0 deletions packages/types/src/tool-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,57 @@ export interface FileEntry {
lineRanges?: LineRange[]
}

/**
* read_file tool input parameters (new spec)
*/
export interface ReadFileInput {
file_path: string
offset?: number
limit?: number
format?: "cat_n"
max_chars_per_line?: number
}

/**
* read_file tool success output
*/
export interface ReadFileSuccess {
ok: true
file_path: string
resolved_path: string
mime_type: string
encoding: string | null
line_offset: number
lines_returned: number
reached_eof: boolean
truncated: boolean
truncation_reason?: "limit" | "max_chars_per_line" | "max_total_chars" | "binary_policy"
next_offset: number | null
content: string
warnings: string[]
}

/**
* read_file tool error output
*/
export interface ReadFileError {
ok: false
error: {
code:
| "file_not_found"
| "permission_denied"
| "outside_workspace"
| "is_directory"
| "unsupported_mime_type"
| "decode_failed"
| "io_error"
message: string
details?: Record<string, unknown>
}
}

export type ReadFileOutput = ReadFileSuccess | ReadFileError

export interface Coordinate {
x: number
y: number
Expand Down
28 changes: 7 additions & 21 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, type ReadFileToolOptions } from "./read_file"
import { createReadFileTool } from "./read_file"
import runSlashCommand from "./run_slash_command"
import searchAndReplace from "./search_and_replace"
import searchReplace from "./search_replace"
Expand All @@ -23,35 +23,21 @@ 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.
* Currently empty but reserved for future tool configuration.
*/
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
/** Whether the model supports image processing (default: false) */
supportsImages?: boolean
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface NativeToolsOptions {}

/**
* Get native tools array, optionally customizing based on settings.
* Get native tools array.
*
* @param options - Configuration options for the tools
* @param options - Configuration options for the tools (currently unused)
* @returns Array of native tool definitions
*/
export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.ChatCompletionTool[] {
const { partialReadsEnabled = true, maxConcurrentFileReads = 5, supportsImages = false } = options

const readFileOptions: ReadFileToolOptions = {
partialReadsEnabled,
maxConcurrentFileReads,
supportsImages,
}

return [
accessMcpResource,
apply_diff,
Expand All @@ -65,7 +51,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
generateImage,
listFiles,
newTask,
createReadFileTool(readFileOptions),
createReadFileTool(),
runSlashCommand,
searchAndReplace,
searchReplace,
Expand Down
140 changes: 47 additions & 93 deletions src/core/prompts/tools/native-tools/read_file.ts
Original file line number Diff line number Diff line change
@@ -1,97 +1,41 @@
import type OpenAI from "openai"

/**
* Generates the file support note, optionally including image format support.
* Creates the read_file tool definition following the paginated read spec.
*
* @param supportsImages - Whether the model supports image processing
* @returns Support note string
*/
function getReadFileSupportsNote(supportsImages: boolean): string {
if (supportsImages) {
return `Supports text extraction from PDF and DOCX files. Automatically processes and returns image files (PNG, JPG, JPEG, GIF, BMP, SVG, WEBP, ICO, AVIF) for visual analysis. May not handle other binary files properly.`
}
return `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
/** Whether the model supports image processing (default: false) */
supportsImages?: boolean
}

/**
* Creates the read_file tool definition, optionally including line_ranges support
* based on whether partial reads are enabled.
* Single-file reads with line-based pagination, stable line numbering,
* and bounded output to stay within context budgets.
*
* @param options - Configuration options for the tool
* @returns Native tool definition for read_file
*/
export function createReadFileTool(options: ReadFileToolOptions = {}): OpenAI.Chat.ChatCompletionTool {
const { partialReadsEnabled = true, maxConcurrentFileReads = 5, supportsImages = false } = options
const isMultipleReadsEnabled = maxConcurrentFileReads > 1
export function createReadFileTool(): OpenAI.Chat.ChatCompletionTool {
const description = `Request to read a file with line-based pagination. Returns at most 2000 lines per call (configurable via limit parameter). Use offset parameter to read subsequent chunks.

// 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. "
Path Resolution and Sandbox:
- file_path is required and must be relative to workspace root
- Paths are resolved to absolute and canonicalized
- Access is restricted to workspace root (sandbox enforcement)
- Directories are rejected

const baseDescription =
descriptionIntro +
"Structure: { files: [{ path: 'relative/path.ts'" +
(partialReadsEnabled ? ", line_ranges: [[1, 50], [100, 150]]" : "") +
" }] }. " +
"The 'path' is required and relative to workspace. "
Pagination:
- offset (default: 0): 0-based line offset. offset=0 starts at file line 1
- limit (default: 2000): Maximum lines per call (hard cap)
- When reached_eof=false in response, continue with offset=next_offset
- Line numbers are file-global and stable across chunks

const optionalRangesDescription = partialReadsEnabled
? "The 'line_ranges' is optional for reading specific sections. Each range is a [start, end] tuple (1-based inclusive). "
: ""
Output Format:
- format (default: "cat_n"): Returns cat -n style with right-aligned line numbers
- max_chars_per_line (default: 2000): Truncates lines exceeding this limit
- Binary files return error with code "unsupported_mime_type"

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]] }] }. " +
(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' }] }. " +
(isMultipleReadsEnabled
? `Example multiple files (within ${maxConcurrentFileReads}-file limit): { files: [{ path: 'file1.ts' }, { path: 'file2.ts' }] }`
: "")
Example: Read first chunk of file:
{ "file_path": "src/main.ts" }

const description =
baseDescription + optionalRangesDescription + getReadFileSupportsNote(supportsImages) + " " + examples
Example: Read next chunk using pagination:
{ "file_path": "src/main.ts", "offset": 2000 }

// Build the properties object conditionally
const fileProperties: Record<string, any> = {
path: {
type: "string",
description: "Path to the file to read, relative to the workspace",
},
}

// Only include line_ranges if partial reads are enabled
if (partialReadsEnabled) {
fileProperties.line_ranges = {
type: ["array", "null"],
description:
"Optional line ranges to read. Each range is a [start, end] tuple with 1-based inclusive line numbers. Use multiple ranges for non-contiguous sections.",
items: {
type: "array",
items: { type: "integer" },
minItems: 2,
maxItems: 2,
},
}
}

// When using strict mode, ALL properties must be in the required array
// Optional properties are handled by having type: ["...", "null"]
const fileRequiredProperties = partialReadsEnabled ? ["path", "line_ranges"] : ["path"]
Example: Read specific section with custom limit:
{ "file_path": "src/main.ts", "offset": 100, "limit": 50 }`

return {
type: "function",
Expand All @@ -102,23 +46,33 @@ export function createReadFileTool(options: ReadFileToolOptions = {}): OpenAI.Ch
parameters: {
type: "object",
properties: {
files: {
type: "array",
description: "List of files to read; request related files together when allowed",
items: {
type: "object",
properties: fileProperties,
required: fileRequiredProperties,
additionalProperties: false,
},
minItems: 1,
file_path: {
type: "string",
description: "Path to the file to read, relative to workspace root (required)",
},
offset: {
type: ["integer", "null"],
description: "0-based line offset. offset=0 starts at file line 1 (default: 0)",
},
limit: {
type: ["integer", "null"],
description: "Maximum number of lines to return (default: 2000, hard cap enforced)",
},
format: {
type: ["string", "null"],
description: 'Output format, currently only "cat_n" supported (default: "cat_n")',
enum: ["cat_n", null],
},
max_chars_per_line: {
type: ["integer", "null"],
description: "Maximum characters per line before truncation (default: 2000)",
},
},
required: ["files"],
required: ["file_path", "offset", "limit", "format", "max_chars_per_line"],
additionalProperties: false,
},
},
} satisfies OpenAI.Chat.ChatCompletionTool
}

export const read_file = createReadFileTool({ partialReadsEnabled: false })
export const read_file = createReadFileTool()
14 changes: 2 additions & 12 deletions src/core/task/build-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,8 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO
modelInfo,
}

// Determine if partial reads are enabled based on maxReadFileLine setting.
const partialReadsEnabled = maxReadFileLine !== -1

// Check if the model supports images for read_file tool description.
const supportsImages = modelInfo?.supportsImages ?? false

// Build native tools with dynamic read_file tool based on settings.
const nativeTools = getNativeTools({
partialReadsEnabled,
maxConcurrentFileReads,
supportsImages,
})
// Build native tools.
const nativeTools = getNativeTools()

// Filter native tools based on mode restrictions.
const filteredNativeTools = filterNativeToolsForMode(
Expand Down
Loading
Loading