From 48a30a4804cd91f9f700753ebb73fa4f40b3c0b0 Mon Sep 17 00:00:00 2001 From: afjcjsbx Date: Tue, 24 Mar 2026 22:54:13 +0100 Subject: [PATCH] feat(tool): read_file tool by lines --- docs/configuration.md | 4 ++-- pkg/tools/filesystem.go | 2 +- pkg/tools/filesystem_test.go | 21 +++++++++++++++------ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9043f2d23..df63ee61b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -319,12 +319,12 @@ Text-oriented behavior, optimized for source files, markdown, logs, and configs. Parameters: * `path` (required): File path -* `start_line` (optional): Starting line number, 1-indexed and inclusive, default `0` +* `start_line` (optional): Starting line number, 1-indexed and inclusive, default `1` * `max_lines` (optional): Maximum number of lines to read, default = all remaining lines until EOF or byte budget Behavior notes: -* Binary-looking files are rejected with guidance to use `read_file` +* Binary-looking files are rejected with guidance to switch `read_file` to `mode = bytes` * Extremely long single lines are truncated rather than skipped Use `mode = lines` when: diff --git a/pkg/tools/filesystem.go b/pkg/tools/filesystem.go index 7e43440ac..0b9a16950 100644 --- a/pkg/tools/filesystem.go +++ b/pkg/tools/filesystem.go @@ -572,7 +572,7 @@ func (t *ReadFileLinesTool) Execute(ctx context.Context, args map[string]any) *T } sample = sample[:sampleN] if isBinaryReadFileData(sample) { - return ErrorResult("file appears to be binary; use read_file for byte-based inspection") + return ErrorResult("file appears to be binary; switch read_file mode to 'bytes' for byte-based inspection") } reader := bufio.NewReaderSize(io.MultiReader(bytes.NewReader(sample), file), 32*1024) diff --git a/pkg/tools/filesystem_test.go b/pkg/tools/filesystem_test.go index 341b04b18..bfbc1f46e 100644 --- a/pkg/tools/filesystem_test.go +++ b/pkg/tools/filesystem_test.go @@ -1135,11 +1135,11 @@ func TestReadFileLinesTool_BinaryFileRejected(t *testing.T) { if !result.IsError { t.Fatalf("expected binary file rejection in line mode, got: %s", result.ForLLM) } - if !strings.Contains(result.ForLLM, "file appears to be binary") { + if !strings.Contains(result.ForLLM, "switch read_file mode to 'bytes'") { t.Fatalf("expected binary file rejection message, got: %s", result.ForLLM) } - if !strings.Contains(result.ForLLM, "use read_file") { - t.Fatalf("expected suggestion to use read_file, got: %s", result.ForLLM) + if !strings.Contains(result.ForLLM, "mode to 'bytes'") { + t.Fatalf("expected suggestion to switch read_file mode, got: %s", result.ForLLM) } } @@ -1190,7 +1190,10 @@ func TestReadFileLinesTool_NoTrailingNewline(t *testing.T) { t.Fatalf("Execute() error = %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "1|line 1\n2|line 2") { - t.Fatalf("expected final line without trailing newline to be preserved, got: %s", result.ForLLM) + t.Fatalf( + "expected final line without trailing newline to be preserved, got: %s", + result.ForLLM, + ) } if !strings.Contains(result.ForLLM, "[END OF FILE - no further content.]") { t.Fatalf("expected EOF marker, got: %s", result.ForLLM) @@ -1215,10 +1218,16 @@ func TestReadFileLinesTool_ExactByteBudgetBoundaryIncludesPrefix(t *testing.T) { t.Fatalf("Execute() error = %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "1|1234567\n") { - t.Fatalf("expected first line to fit exactly in the byte budget with its prefix, got: %s", result.ForLLM) + t.Fatalf( + "expected first line to fit exactly in the byte budget with its prefix, got: %s", + result.ForLLM, + ) } if strings.Contains(result.ForLLM, "2|") { - t.Fatalf("expected second line to be excluded once the exact output byte budget was reached, got: %s", result.ForLLM) + t.Fatalf( + "expected second line to be excluded once the exact output byte budget was reached, got: %s", + result.ForLLM, + ) } if !strings.Contains(result.ForLLM, "file_bytes: 8 | output_bytes: 10") { t.Fatalf("expected separate file/output byte counters, got: %s", result.ForLLM)