Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 925018d

Browse files
committed
feat(EXT-617): Refactor read_file tool with pagination and bounded reads
BREAKING CHANGE: Complete rewrite of read_file tool API Changes: - New input schema: single file with offset/limit pagination - Output now returns structured JSON with metadata - Line numbering uses cat -n style (right-aligned, stable) - Default 2000 line limit per call with pagination via next_offset - Removed multi-file reads (now single file per call) - Removed user approval workflow (direct execution) - Removed image processing (to be added back in follow-up) This implements the spec from Linear issue EXT-617 for line-based pagination, reliable continuation via offset/limit, and bounded output for context budget management. Note: This is a work-in-progress draft. Tests and additional features need to be updated/added: - All existing tests need rewrite (300+ references to old API) - Image handling needs to be re-implemented - UI approval workflow needs removal - Binary file handling (PDF/DOCX) needs re-implementation
1 parent 953c777 commit 925018d

6 files changed

Lines changed: 400 additions & 732 deletions

File tree

packages/types/src/tool-params.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,57 @@ export interface FileEntry {
1212
lineRanges?: LineRange[]
1313
}
1414

15+
/**
16+
* read_file tool input parameters (new spec)
17+
*/
18+
export interface ReadFileInput {
19+
file_path: string
20+
offset?: number
21+
limit?: number
22+
format?: "cat_n"
23+
max_chars_per_line?: number
24+
}
25+
26+
/**
27+
* read_file tool success output
28+
*/
29+
export interface ReadFileSuccess {
30+
ok: true
31+
file_path: string
32+
resolved_path: string
33+
mime_type: string
34+
encoding: string | null
35+
line_offset: number
36+
lines_returned: number
37+
reached_eof: boolean
38+
truncated: boolean
39+
truncation_reason?: "limit" | "max_chars_per_line" | "max_total_chars" | "binary_policy"
40+
next_offset: number | null
41+
content: string
42+
warnings: string[]
43+
}
44+
45+
/**
46+
* read_file tool error output
47+
*/
48+
export interface ReadFileError {
49+
ok: false
50+
error: {
51+
code:
52+
| "file_not_found"
53+
| "permission_denied"
54+
| "outside_workspace"
55+
| "is_directory"
56+
| "unsupported_mime_type"
57+
| "decode_failed"
58+
| "io_error"
59+
message: string
60+
details?: Record<string, unknown>
61+
}
62+
}
63+
64+
export type ReadFileOutput = ReadFileSuccess | ReadFileError
65+
1566
export interface Coordinate {
1667
x: number
1768
y: number

src/core/prompts/tools/native-tools/index.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import fetchInstructions from "./fetch_instructions"
1111
import generateImage from "./generate_image"
1212
import listFiles from "./list_files"
1313
import newTask from "./new_task"
14-
import { createReadFileTool, type ReadFileToolOptions } from "./read_file"
14+
import { createReadFileTool } from "./read_file"
1515
import runSlashCommand from "./run_slash_command"
1616
import searchAndReplace from "./search_and_replace"
1717
import searchReplace from "./search_replace"
@@ -23,35 +23,21 @@ import writeToFile from "./write_to_file"
2323

2424
export { getMcpServerTools } from "./mcp_server"
2525
export { convertOpenAIToolToAnthropic, convertOpenAIToolsToAnthropic } from "./converters"
26-
export type { ReadFileToolOptions } from "./read_file"
2726

2827
/**
2928
* Options for customizing the native tools array.
29+
* Currently empty but reserved for future tool configuration.
3030
*/
31-
export interface NativeToolsOptions {
32-
/** Whether to include line_ranges support in read_file tool (default: true) */
33-
partialReadsEnabled?: boolean
34-
/** Maximum number of files that can be read in a single read_file request (default: 5) */
35-
maxConcurrentFileReads?: number
36-
/** Whether the model supports image processing (default: false) */
37-
supportsImages?: boolean
38-
}
31+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
32+
export interface NativeToolsOptions {}
3933

4034
/**
41-
* Get native tools array, optionally customizing based on settings.
35+
* Get native tools array.
4236
*
43-
* @param options - Configuration options for the tools
37+
* @param options - Configuration options for the tools (currently unused)
4438
* @returns Array of native tool definitions
4539
*/
4640
export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.ChatCompletionTool[] {
47-
const { partialReadsEnabled = true, maxConcurrentFileReads = 5, supportsImages = false } = options
48-
49-
const readFileOptions: ReadFileToolOptions = {
50-
partialReadsEnabled,
51-
maxConcurrentFileReads,
52-
supportsImages,
53-
}
54-
5541
return [
5642
accessMcpResource,
5743
apply_diff,
@@ -65,7 +51,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
6551
generateImage,
6652
listFiles,
6753
newTask,
68-
createReadFileTool(readFileOptions),
54+
createReadFileTool(),
6955
runSlashCommand,
7056
searchAndReplace,
7157
searchReplace,
Lines changed: 47 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,41 @@
11
import type OpenAI from "openai"
22

33
/**
4-
* Generates the file support note, optionally including image format support.
4+
* Creates the read_file tool definition following the paginated read spec.
55
*
6-
* @param supportsImages - Whether the model supports image processing
7-
* @returns Support note string
8-
*/
9-
function getReadFileSupportsNote(supportsImages: boolean): string {
10-
if (supportsImages) {
11-
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.`
12-
}
13-
return `Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.`
14-
}
15-
16-
/**
17-
* Options for creating the read_file tool definition.
18-
*/
19-
export interface ReadFileToolOptions {
20-
/** Whether to include line_ranges parameter (default: true) */
21-
partialReadsEnabled?: boolean
22-
/** Maximum number of files that can be read in a single request (default: 5) */
23-
maxConcurrentFileReads?: number
24-
/** Whether the model supports image processing (default: false) */
25-
supportsImages?: boolean
26-
}
27-
28-
/**
29-
* Creates the read_file tool definition, optionally including line_ranges support
30-
* based on whether partial reads are enabled.
6+
* Single-file reads with line-based pagination, stable line numbering,
7+
* and bounded output to stay within context budgets.
318
*
32-
* @param options - Configuration options for the tool
339
* @returns Native tool definition for read_file
3410
*/
35-
export function createReadFileTool(options: ReadFileToolOptions = {}): OpenAI.Chat.ChatCompletionTool {
36-
const { partialReadsEnabled = true, maxConcurrentFileReads = 5, supportsImages = false } = options
37-
const isMultipleReadsEnabled = maxConcurrentFileReads > 1
11+
export function createReadFileTool(): OpenAI.Chat.ChatCompletionTool {
12+
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.
3813
39-
// Build description intro with concurrent reads limit message
40-
const descriptionIntro = isMultipleReadsEnabled
41-
? `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. `
42-
: "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. "
14+
Path Resolution and Sandbox:
15+
- file_path is required and must be relative to workspace root
16+
- Paths are resolved to absolute and canonicalized
17+
- Access is restricted to workspace root (sandbox enforcement)
18+
- Directories are rejected
4319
44-
const baseDescription =
45-
descriptionIntro +
46-
"Structure: { files: [{ path: 'relative/path.ts'" +
47-
(partialReadsEnabled ? ", line_ranges: [[1, 50], [100, 150]]" : "") +
48-
" }] }. " +
49-
"The 'path' is required and relative to workspace. "
20+
Pagination:
21+
- offset (default: 0): 0-based line offset. offset=0 starts at file line 1
22+
- limit (default: 2000): Maximum lines per call (hard cap)
23+
- When reached_eof=false in response, continue with offset=next_offset
24+
- Line numbers are file-global and stable across chunks
5025
51-
const optionalRangesDescription = partialReadsEnabled
52-
? "The 'line_ranges' is optional for reading specific sections. Each range is a [start, end] tuple (1-based inclusive). "
53-
: ""
26+
Output Format:
27+
- format (default: "cat_n"): Returns cat -n style with right-aligned line numbers
28+
- max_chars_per_line (default: 2000): Truncates lines exceeding this limit
29+
- Binary files return error with code "unsupported_mime_type"
5430
55-
const examples = partialReadsEnabled
56-
? "Example single file: { files: [{ path: 'src/app.ts' }] }. " +
57-
"Example with line ranges: { files: [{ path: 'src/app.ts', line_ranges: [[1, 50], [100, 150]] }] }. " +
58-
(isMultipleReadsEnabled
59-
? `Example multiple files (within ${maxConcurrentFileReads}-file limit): { files: [{ path: 'file1.ts', line_ranges: [[1, 50]] }, { path: 'file2.ts' }] }`
60-
: "")
61-
: "Example single file: { files: [{ path: 'src/app.ts' }] }. " +
62-
(isMultipleReadsEnabled
63-
? `Example multiple files (within ${maxConcurrentFileReads}-file limit): { files: [{ path: 'file1.ts' }, { path: 'file2.ts' }] }`
64-
: "")
31+
Example: Read first chunk of file:
32+
{ "file_path": "src/main.ts" }
6533
66-
const description =
67-
baseDescription + optionalRangesDescription + getReadFileSupportsNote(supportsImages) + " " + examples
34+
Example: Read next chunk using pagination:
35+
{ "file_path": "src/main.ts", "offset": 2000 }
6836
69-
// Build the properties object conditionally
70-
const fileProperties: Record<string, any> = {
71-
path: {
72-
type: "string",
73-
description: "Path to the file to read, relative to the workspace",
74-
},
75-
}
76-
77-
// Only include line_ranges if partial reads are enabled
78-
if (partialReadsEnabled) {
79-
fileProperties.line_ranges = {
80-
type: ["array", "null"],
81-
description:
82-
"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.",
83-
items: {
84-
type: "array",
85-
items: { type: "integer" },
86-
minItems: 2,
87-
maxItems: 2,
88-
},
89-
}
90-
}
91-
92-
// When using strict mode, ALL properties must be in the required array
93-
// Optional properties are handled by having type: ["...", "null"]
94-
const fileRequiredProperties = partialReadsEnabled ? ["path", "line_ranges"] : ["path"]
37+
Example: Read specific section with custom limit:
38+
{ "file_path": "src/main.ts", "offset": 100, "limit": 50 }`
9539

9640
return {
9741
type: "function",
@@ -102,23 +46,33 @@ export function createReadFileTool(options: ReadFileToolOptions = {}): OpenAI.Ch
10246
parameters: {
10347
type: "object",
10448
properties: {
105-
files: {
106-
type: "array",
107-
description: "List of files to read; request related files together when allowed",
108-
items: {
109-
type: "object",
110-
properties: fileProperties,
111-
required: fileRequiredProperties,
112-
additionalProperties: false,
113-
},
114-
minItems: 1,
49+
file_path: {
50+
type: "string",
51+
description: "Path to the file to read, relative to workspace root (required)",
52+
},
53+
offset: {
54+
type: ["integer", "null"],
55+
description: "0-based line offset. offset=0 starts at file line 1 (default: 0)",
56+
},
57+
limit: {
58+
type: ["integer", "null"],
59+
description: "Maximum number of lines to return (default: 2000, hard cap enforced)",
60+
},
61+
format: {
62+
type: ["string", "null"],
63+
description: 'Output format, currently only "cat_n" supported (default: "cat_n")',
64+
enum: ["cat_n", null],
65+
},
66+
max_chars_per_line: {
67+
type: ["integer", "null"],
68+
description: "Maximum characters per line before truncation (default: 2000)",
11569
},
11670
},
117-
required: ["files"],
71+
required: ["file_path", "offset", "limit", "format", "max_chars_per_line"],
11872
additionalProperties: false,
11973
},
12074
},
12175
} satisfies OpenAI.Chat.ChatCompletionTool
12276
}
12377

124-
export const read_file = createReadFileTool({ partialReadsEnabled: false })
78+
export const read_file = createReadFileTool()

src/core/task/build-tools.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,18 +109,8 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO
109109
modelInfo,
110110
}
111111

112-
// Determine if partial reads are enabled based on maxReadFileLine setting.
113-
const partialReadsEnabled = maxReadFileLine !== -1
114-
115-
// Check if the model supports images for read_file tool description.
116-
const supportsImages = modelInfo?.supportsImages ?? false
117-
118-
// Build native tools with dynamic read_file tool based on settings.
119-
const nativeTools = getNativeTools({
120-
partialReadsEnabled,
121-
maxConcurrentFileReads,
122-
supportsImages,
123-
})
112+
// Build native tools.
113+
const nativeTools = getNativeTools()
124114

125115
// Filter native tools based on mode restrictions.
126116
const filteredNativeTools = filterNativeToolsForMode(

0 commit comments

Comments
 (0)