Skip to content

Commit f39705d

Browse files
authored
Merge pull request #1828 from trungutt/fix/filesystem-tool-output-limit
builtin: add offset and line_count pagination to read_file and read_multiple_files
2 parents deec6a2 + 2829c94 commit f39705d

9 files changed

Lines changed: 123 additions & 79 deletions

File tree

e2e/testdata/cassettes/TestExec_Anthropic_ToolCall.yaml

Lines changed: 17 additions & 17 deletions
Large diffs are not rendered by default.

e2e/testdata/cassettes/TestExec_Gemini_ToolCall.yaml

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

e2e/testdata/cassettes/TestExec_Mistral_ToolCall.yaml

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

e2e/testdata/cassettes/TestExec_OpenAI_HideToolCalls.yaml

Lines changed: 20 additions & 20 deletions
Large diffs are not rendered by default.

e2e/testdata/cassettes/TestExec_OpenAI_ToolCall.yaml

Lines changed: 20 additions & 20 deletions
Large diffs are not rendered by default.

e2e/testdata/cassettes/TestExec_ToolCallsNeedAcceptance.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interactions:
88
proto_minor: 1
99
content_length: 0
1010
host: api.openai.com
11-
body: '{"input":[{"content":[{"text":"You are a knowledgeable assistant that can write test files.","type":"input_text"}],"role":"system"},{"content":[{"text":"## Filesystem Tool Instructions\n\nThis toolset provides comprehensive filesystem operations.\n\n### Working Directory\n- Relative paths (like \".\" or \"src/main.go\") are resolved relative to the working directory\n- Absolute paths (like \"/etc/hosts\") access files directly\n- Paths starting with \"..\" can access parent directories\n\n### Common Patterns\n- Always check if directories exist before creating files\n- Prefer read_multiple_files for batch operations\n- Use search_files_content for finding specific code or text\n\n### Performance Tips\n- Use read_multiple_files instead of multiple read_file calls\n- Use directory_tree with max_depth to limit large traversals\n- Use appropriate exclude patterns in search operations","type":"input_text"}],"role":"system"},{"content":"Create a hello.txt file with \"Hello, World!\" content. Try only once. On error, exit without further message.","role":"user"}],"model":"gpt-5-mini","tools":[{"strict":true,"parameters":{"additionalProperties":false,"properties":{"content":{"description":"The content to write to the file","type":"string"},"path":{"description":"The file path to write","type":"string"}},"required":["content","path"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","type":"function"}],"stream":true}'
11+
body: '{"input":[{"content":[{"text":"You are a knowledgeable assistant that can write test files.","type":"input_text"}],"role":"system"},{"content":[{"text":"## Filesystem Tool Instructions\n\nThis toolset provides comprehensive filesystem operations.\n\n### Working Directory\n- Relative paths (like \".\" or \"src/main.go\") are resolved relative to the working directory\n- Absolute paths (like \"/etc/hosts\") access files directly\n- Paths starting with \"..\" can access parent directories\n\n### Common Patterns\n- Always check if directories exist before creating files\n- Prefer read_multiple_files for batch operations\n- Use search_files_content for finding specific code or text\n\n### Performance Tips\n- Use read_multiple_files instead of multiple read_file calls\n- Use directory_tree with max_depth to limit large traversals\n- Use appropriate exclude patterns in search operations\n\n### Reading Large Files\n- read_file and read_multiple_files support offset and line_count parameters for pagination\n- When a file is large, read it in chunks: start with offset=1 and a reasonable line_count\n- The response includes total_lines so you know how many more lines remain\n- Continue reading with an incremented offset until you have read all required content","type":"input_text"}],"role":"system"},{"content":"Create a hello.txt file with \"Hello, World!\" content. Try only once. On error, exit without further message.","role":"user"}],"model":"gpt-5-mini","tools":[{"strict":true,"parameters":{"additionalProperties":false,"properties":{"content":{"description":"The content to write to the file","type":"string"},"path":{"description":"The file path to write","type":"string"}},"required":["content","path"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","type":"function"}],"stream":true}'
1212
url: https://api.openai.com/v1/responses
1313
method: POST
1414
response:
@@ -91,7 +91,7 @@ interactions:
9191
proto_minor: 1
9292
content_length: 0
9393
host: api.openai.com
94-
body: '{"input":[{"content":[{"text":"You are a knowledgeable assistant that can write test files.","type":"input_text"}],"role":"system"},{"content":[{"text":"## Filesystem Tool Instructions\n\nThis toolset provides comprehensive filesystem operations.\n\n### Working Directory\n- Relative paths (like \".\" or \"src/main.go\") are resolved relative to the working directory\n- Absolute paths (like \"/etc/hosts\") access files directly\n- Paths starting with \"..\" can access parent directories\n\n### Common Patterns\n- Always check if directories exist before creating files\n- Prefer read_multiple_files for batch operations\n- Use search_files_content for finding specific code or text\n\n### Performance Tips\n- Use read_multiple_files instead of multiple read_file calls\n- Use directory_tree with max_depth to limit large traversals\n- Use appropriate exclude patterns in search operations","type":"input_text"}],"role":"system"},{"content":"Create a hello.txt file with \"Hello, World!\" content. Try only once. On error, exit without further message.","role":"user"},{"arguments":"{\"content\":\"Hello, World!\",\"path\":\"hello.txt\"}","call_id":"call_5W18F6XkDh9NllAH9r0P9GuF","name":"write_file","type":"function_call"},{"call_id":"call_5W18F6XkDh9NllAH9r0P9GuF","output":"The user rejected the tool call.","type":"function_call_output"}],"model":"gpt-5-mini","tools":[{"strict":true,"parameters":{"additionalProperties":false,"properties":{"content":{"description":"The content to write to the file","type":"string"},"path":{"description":"The file path to write","type":"string"}},"required":["content","path"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","type":"function"}],"stream":true}'
94+
body: '{"input":[{"content":[{"text":"You are a knowledgeable assistant that can write test files.","type":"input_text"}],"role":"system"},{"content":[{"text":"## Filesystem Tool Instructions\n\nThis toolset provides comprehensive filesystem operations.\n\n### Working Directory\n- Relative paths (like \".\" or \"src/main.go\") are resolved relative to the working directory\n- Absolute paths (like \"/etc/hosts\") access files directly\n- Paths starting with \"..\" can access parent directories\n\n### Common Patterns\n- Always check if directories exist before creating files\n- Prefer read_multiple_files for batch operations\n- Use search_files_content for finding specific code or text\n\n### Performance Tips\n- Use read_multiple_files instead of multiple read_file calls\n- Use directory_tree with max_depth to limit large traversals\n- Use appropriate exclude patterns in search operations\n\n### Reading Large Files\n- read_file and read_multiple_files support offset and line_count parameters for pagination\n- When a file is large, read it in chunks: start with offset=1 and a reasonable line_count\n- The response includes total_lines so you know how many more lines remain\n- Continue reading with an incremented offset until you have read all required content","type":"input_text"}],"role":"system"},{"content":"Create a hello.txt file with \"Hello, World!\" content. Try only once. On error, exit without further message.","role":"user"},{"arguments":"{\"content\":\"Hello, World!\",\"path\":\"hello.txt\"}","call_id":"call_5W18F6XkDh9NllAH9r0P9GuF","name":"write_file","type":"function_call"},{"call_id":"call_5W18F6XkDh9NllAH9r0P9GuF","output":"The user rejected the tool call.","type":"function_call_output"}],"model":"gpt-5-mini","tools":[{"strict":true,"parameters":{"additionalProperties":false,"properties":{"content":{"description":"The content to write to the file","type":"string"},"path":{"description":"The file path to write","type":"string"}},"required":["content","path"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","type":"function"}],"stream":true}'
9595
url: https://api.openai.com/v1/responses
9696
method: POST
9797
response:

pkg/tools/builtin/filesystem.go

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,13 @@ This toolset provides comprehensive filesystem operations.
9191
### Performance Tips
9292
- Use read_multiple_files instead of multiple read_file calls
9393
- Use directory_tree with max_depth to limit large traversals
94-
- Use appropriate exclude patterns in search operations`
94+
- Use appropriate exclude patterns in search operations
95+
96+
### Reading Large Files
97+
- read_file and read_multiple_files support offset and line_count parameters for pagination
98+
- When a file is large, read it in chunks: start with offset=1 and a reasonable line_count
99+
- The response includes total_lines so you know how many more lines remain
100+
- Continue reading with an incremented offset until you have read all required content`
95101
}
96102

97103
type DirectoryTreeArgs struct {
@@ -104,8 +110,10 @@ type WriteFileArgs struct {
104110
}
105111

106112
type ReadMultipleFilesArgs struct {
107-
Paths []string `json:"paths" jsonschema:"Array of file paths to read"`
108-
JSON bool `json:"json,omitempty" jsonschema:"Whether to return the result as JSON"`
113+
Paths []string `json:"paths" jsonschema:"Array of file paths to read"`
114+
JSON bool `json:"json,omitempty" jsonschema:"Whether to return the result as JSON"`
115+
Offset int `json:"offset,omitempty" jsonschema:"1-based line number to start reading from, applied to all files (default: 1)"`
116+
LineCount int `json:"line_count,omitempty" jsonschema:"Maximum number of lines to return per file (default: all remaining lines)"`
109117
}
110118

111119
type ReadMultipleFilesMeta struct {
@@ -141,14 +149,16 @@ type DirectoryTreeMeta struct {
141149
}
142150

143151
type ReadFileArgs struct {
144-
Path string `json:"path" jsonschema:"The file path to read"`
152+
Path string `json:"path" jsonschema:"The file path to read"`
153+
Offset int `json:"offset,omitempty" jsonschema:"1-based line number to start reading from (default: 1)"`
154+
LineCount int `json:"line_count,omitempty" jsonschema:"Maximum number of lines to return (default: all remaining lines)"`
145155
}
146156

147157
type ReadFileMeta struct {
148-
Path string `json:"path"`
149-
Content string `json:"content"`
150-
LineCount int `json:"lineCount"`
151-
Error string `json:"error,omitempty"`
158+
Path string `json:"path"`
159+
Content string `json:"content"`
160+
TotalLines int `json:"totalLines"`
161+
Error string `json:"error,omitempty"`
152162
}
153163

154164
type Edit struct {
@@ -226,7 +236,7 @@ func (t *FilesystemTool) Tools(context.Context) ([]tools.Tool, error) {
226236
{
227237
Name: ToolNameReadFile,
228238
Category: "filesystem",
229-
Description: "Read the complete contents of a file from the file system.",
239+
Description: "Read the contents of a file from the file system.",
230240
Parameters: tools.MustSchemaFor[ReadFileArgs](),
231241
OutputSchema: tools.MustSchemaFor[string](),
232242
Handler: tools.NewHandler(t.handleReadFile),
@@ -483,6 +493,38 @@ func (t *FilesystemTool) handleListDirectory(_ context.Context, args ListDirecto
483493
}, nil
484494
}
485495

496+
// applyLineWindow slices lines from a file's content according to offset (1-based) and
497+
// lineCount (0 means all remaining lines). It returns the windowed content and the total
498+
// line count of the original. A header is prepended when only a subset is returned.
499+
func applyLineWindow(content, path string, offset, lineCount int) (windowed string, totalLines int) {
500+
lines := strings.Split(content, "\n")
501+
totalLines = len(lines)
502+
503+
// Normalise offset: default to 1, clamp to valid range
504+
if offset <= 0 {
505+
offset = 1
506+
}
507+
if offset > totalLines {
508+
offset = totalLines
509+
}
510+
511+
// Convert to 0-based index
512+
from := offset - 1
513+
to := totalLines
514+
if lineCount > 0 && from+lineCount < totalLines {
515+
to = from + lineCount
516+
}
517+
518+
windowed = strings.Join(lines[from:to], "\n")
519+
520+
// Prepend a header only when we are returning a subset of the file
521+
if from > 0 || to < totalLines {
522+
windowed = fmt.Sprintf("[Showing lines %d-%d of %d from %s]\n%s", offset, to, totalLines, path, windowed)
523+
}
524+
525+
return windowed, totalLines
526+
}
527+
486528
func (t *FilesystemTool) handleReadFile(_ context.Context, args ReadFileArgs) (*tools.ToolCallResult, error) {
487529
resolvedPath := t.resolvePath(args.Path)
488530

@@ -504,10 +546,11 @@ func (t *FilesystemTool) handleReadFile(_ context.Context, args ReadFileArgs) (*
504546
}, nil
505547
}
506548

549+
windowed, totalLines := applyLineWindow(string(content), args.Path, args.Offset, args.LineCount)
507550
return &tools.ToolCallResult{
508-
Output: string(content),
551+
Output: windowed,
509552
Meta: ReadFileMeta{
510-
LineCount: strings.Count(string(content), "\n") + 1,
553+
TotalLines: totalLines,
511554
},
512555
}, nil
513556
}
@@ -545,12 +588,13 @@ func (t *FilesystemTool) handleReadMultipleFiles(ctx context.Context, args ReadM
545588
continue
546589
}
547590

591+
windowed, totalLines := applyLineWindow(string(content), path, args.Offset, args.LineCount)
548592
contents = append(contents, PathContent{
549593
Path: path,
550-
Content: string(content),
594+
Content: windowed,
551595
})
552-
entry.Content = string(content)
553-
entry.LineCount = strings.Count(string(content), "\n") + 1
596+
entry.Content = windowed
597+
entry.TotalLines = totalLines
554598
meta.Files = append(meta.Files, entry)
555599
}
556600

pkg/tui/components/tool/readfile/readfile.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,5 @@ func extractResult(msg *types.Message) string {
2828
if meta.Error != "" {
2929
return meta.Error
3030
}
31-
return fmt.Sprintf("%d lines", meta.LineCount)
31+
return fmt.Sprintf("%d lines", meta.TotalLines)
3232
}

pkg/tui/components/tool/readmultiplefiles/readmultiplefiles.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func formatSummaryLines(meta *builtin.ReadMultipleFilesMeta) []fileSummary {
9999
if file.Error != "" {
100100
output = " " + file.Error
101101
} else {
102-
output = fmt.Sprintf(" %d lines", file.LineCount)
102+
output = fmt.Sprintf(" %d lines", file.TotalLines)
103103
}
104104

105105
summaries = append(summaries, fileSummary{

0 commit comments

Comments
 (0)