diff --git a/pkg/tools/filesystem.go b/pkg/tools/filesystem.go index d82100970..de9749121 100644 --- a/pkg/tools/filesystem.go +++ b/pkg/tools/filesystem.go @@ -569,7 +569,7 @@ func (t *ReadFileLinesTool) Execute(ctx context.Context, args map[string]any) *T reader := bufio.NewReaderSize(io.MultiReader(bytes.NewReader(sample), file), 32*1024) var content strings.Builder - var lineIndex int64 + lineIndex := int64(1) var linesRead int64 var bytesRead int64 var reachedEOF bool @@ -589,7 +589,8 @@ func (t *ReadFileLinesTool) Execute(ctx context.Context, args map[string]any) *T } for !reachedEOF && (limit < 0 || linesRead < limit) { - remaining := t.maxSize - bytesRead + prefix := formatReadFileLinePrefix(lineIndex) + remaining := t.maxSize - bytesRead - int64(len(prefix)) if remaining <= 0 { byteBudgetTruncated = true break @@ -604,8 +605,9 @@ func (t *ReadFileLinesTool) Execute(ctx context.Context, args map[string]any) *T break } + content.WriteString(prefix) content.Write(line) - bytesRead += int64(len(line)) + bytesRead += int64(len(prefix) + len(line)) linesRead++ lineIndex++ @@ -648,12 +650,12 @@ func (t *ReadFileLinesTool) Execute(ctx context.Context, args map[string]any) *T ) case byteBudgetTruncated: header += fmt.Sprintf( - "\n[TRUNCATED - byte budget reached. Call read_file again with offset=%d to continue at the next line.]", + "\n[TRUNCATED - byte budget reached. Call read_file again with start_line=%d to continue at the next line.]", startLine+linesRead, ) case !reachedEOF && limit > 0 && linesRead >= limit: header += fmt.Sprintf( - "\n[PARTIAL - more content remains. Call read_file again with offset=%d to continue.]", + "\n[PARTIAL - more content remains. Call read_file again with start_line=%d to continue.]", startLine+linesRead, ) default: @@ -672,6 +674,10 @@ func (t *ReadFileLinesTool) Execute(ctx context.Context, args map[string]any) *T return NewToolResult(header + "\n\n" + content.String()) } +func formatReadFileLinePrefix(lineNumber int64) string { + return strconv.FormatInt(lineNumber, 10) + "|" +} + func isBinaryReadFileData(data []byte) bool { if len(data) == 0 { return false diff --git a/pkg/tools/filesystem_test.go b/pkg/tools/filesystem_test.go index 2507042ec..14b34b604 100644 --- a/pkg/tools/filesystem_test.go +++ b/pkg/tools/filesystem_test.go @@ -888,14 +888,14 @@ func TestReadFileLinesTool_ChunkedReading(t *testing.T) { if result1.IsError { t.Fatalf("Chunk 1 failed: %s", result1.ForLLM) } - if !strings.Contains(result1.ForLLM, "line 2\nline 3\n") { + if !strings.Contains(result1.ForLLM, "1|line 1\n2|line 2\n") { t.Fatalf("expected first two lines, got: %s", result1.ForLLM) } if !strings.Contains(result1.ForLLM, "lines 1-2") { t.Fatalf("expected line range 1-2, got: %s", result1.ForLLM) } - if !strings.Contains(result1.ForLLM, "offset=3") { - t.Fatalf("expected continuation offset=3, got: %s", result1.ForLLM) + if !strings.Contains(result1.ForLLM, "start_line=3") { + t.Fatalf("expected continuation start_line=3, got: %s", result1.ForLLM) } result2 := tool.Execute(context.Background(), map[string]any{ @@ -906,11 +906,11 @@ func TestReadFileLinesTool_ChunkedReading(t *testing.T) { if result2.IsError { t.Fatalf("Chunk 2 failed: %s", result2.ForLLM) } - if !strings.Contains(result2.ForLLM, "line 4\nline 5\n") { + if !strings.Contains(result2.ForLLM, "3|line 3\n4|line 4\n") { t.Fatalf("expected middle chunk, got: %s", result2.ForLLM) } - if !strings.Contains(result2.ForLLM, "offset=5") { - t.Fatalf("expected continuation offset=5, got: %s", result2.ForLLM) + if !strings.Contains(result2.ForLLM, "start_line=5") { + t.Fatalf("expected continuation start_line=5, got: %s", result2.ForLLM) } result3 := tool.Execute(context.Background(), map[string]any{ @@ -921,7 +921,7 @@ func TestReadFileLinesTool_ChunkedReading(t *testing.T) { if result3.IsError { t.Fatalf("Chunk 3 failed: %s", result3.ForLLM) } - if !strings.Contains(result3.ForLLM, "line 6\n") { + if !strings.Contains(result3.ForLLM, "5|line 5\n6|line 6\n") { t.Fatalf("expected final chunk, got: %s", result3.ForLLM) } if !strings.Contains(result3.ForLLM, "[END OF FILE") { @@ -946,11 +946,11 @@ func TestReadFileLinesTool_DefaultOffsetAndRemainingLines(t *testing.T) { if result.IsError { t.Fatalf("Execute() error = %s", result.ForLLM) } - if !strings.Contains(result.ForLLM, "line 2\nline 3\n") { + if !strings.Contains(result.ForLLM, "1|line 1\n2|line 2\n3|line 3\n") { t.Fatalf("expected remaining lines by default, got: %s", result.ForLLM) } - if !strings.Contains(result.ForLLM, "lines 1-2") { - t.Fatalf("expected line range 1-2, got: %s", result.ForLLM) + if !strings.Contains(result.ForLLM, "lines 1-3") { + t.Fatalf("expected line range 1-3, got: %s", result.ForLLM) } } @@ -1052,7 +1052,10 @@ func TestReadFileLinesTool_TruncatesSingleLongLineAtByteBudget(t *testing.T) { if !strings.Contains(result.ForLLM, "was cut mid-line") { t.Fatalf("expected explicit mid-line truncation warning, got: %s", result.ForLLM) } - if strings.Contains(result.ForLLM, "first line") { - t.Fatalf("did not expect the skipped first line in output, got: %s", result.ForLLM) + if !strings.Contains(result.ForLLM, "1|first line\n") { + t.Fatalf("expected the first line with line prefix, got: %s", result.ForLLM) + } + if !strings.Contains(result.ForLLM, "2|") { + t.Fatalf("expected line prefix for the truncated line, got: %s", result.ForLLM) } }