44package aiusechat
55
66import (
7- "bufio"
8- "bytes"
97 "fmt"
108 "io"
119 "os"
1210 "strings"
1311
1412 "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
13+ "github.com/wavetermdev/waveterm/pkg/util/readutil"
1514 "github.com/wavetermdev/waveterm/pkg/util/utilfn"
1615)
1716
17+ const StopReasonMaxBytes = "max_bytes"
18+
1819type readTextFileParams struct {
19- Filename string `json:"filename"`
20- LineStart * int `json:"line_start"`
21- LineEnd * int `json:"line_end"`
22- MaxBytes * int `json:"max_bytes"`
23- FromEnd bool `json:"from_end"`
20+ Filename string `json:"filename"`
21+ Origin * string `json:"origin"` // "start" or "end", defaults to "start"
22+ Offset * int `json:"offset"` // lines to skip, defaults to 0
23+ Count * int `json:"count"` // number of lines to read, defaults to DefaultLineCount
24+ MaxBytes * int `json:"max_bytes"`
25+ }
26+
27+ // truncateData truncates data to maxBytes while respecting line boundaries.
28+ // For origin "start", keeps the beginning and truncates at last newline before maxBytes.
29+ // For origin "end", keeps the end and truncates from beginning at first newline after removing excess.
30+ func truncateData (data string , origin string , maxBytes int ) string {
31+ if len (data ) <= maxBytes {
32+ return data
33+ }
34+
35+ if origin == "end" {
36+ excessBytes := len (data ) - maxBytes
37+ truncateIdx := strings .Index (data [excessBytes :], "\n " )
38+ if truncateIdx == - 1 {
39+ return data [excessBytes :]
40+ }
41+ return data [excessBytes + truncateIdx + 1 :]
42+ }
43+
44+ truncateIdx := strings .LastIndex (data [:maxBytes ], "\n " )
45+ if truncateIdx == - 1 {
46+ return data [:maxBytes ]
47+ }
48+ return data [:truncateIdx + 1 ]
2449}
2550
2651func readTextFileCallback (input any ) (any , error ) {
27- const DEFAULT_LINE_COUNT = 100
28- const DEFAULT_MAX_BYTES = 50 * 1024
52+ const DefaultLineCount = 100
53+ const DefaultMaxBytes = 50 * 1024
54+ const ReadLimit = 1024 * 1024 * 1024
2955
3056 var params readTextFileParams
3157 if err := utilfn .ReUnmarshal (& params , input ); err != nil {
@@ -36,7 +62,7 @@ func readTextFileCallback(input any) (any, error) {
3662 return nil , fmt .Errorf ("missing filename parameter" )
3763 }
3864
39- maxBytes := DEFAULT_MAX_BYTES
65+ maxBytes := DefaultMaxBytes
4066 if params .MaxBytes != nil {
4167 maxBytes = * params .MaxBytes
4268 }
@@ -66,90 +92,70 @@ func readTextFileCallback(input any) (any, error) {
6692 return nil , fmt .Errorf ("file appears to be binary content" )
6793 }
6894
69- file .Seek (0 , 0 )
70-
71- var lines []string
72- scanner := bufio .NewScanner (file )
73- buf := make ([]byte , 0 , 64 * 1024 )
74- scanner .Buffer (buf , maxBytes )
75-
76- for scanner .Scan () {
77- lines = append (lines , scanner .Text ())
95+ origin := "start"
96+ if params .Origin != nil {
97+ origin = * params .Origin
7898 }
7999
80- if err := scanner . Err (); err != nil {
81- return nil , fmt .Errorf ("error reading file: %w " , err )
100+ if origin != "start" && origin != "end" {
101+ return nil , fmt .Errorf ("invalid origin value '%s': must be 'start' or 'end' " , origin )
82102 }
83103
84- totalLines := len (lines )
104+ offset := 0
105+ if params .Offset != nil {
106+ offset = * params .Offset
107+ }
85108
86- if params .FromEnd {
87- for i , j := 0 , len (lines )- 1 ; i < j ; i , j = i + 1 , j - 1 {
88- lines [i ], lines [j ] = lines [j ], lines [i ]
109+ count := DefaultLineCount
110+ if params .Count != nil {
111+ count = * params .Count
112+ if count < 1 {
113+ return nil , fmt .Errorf ("count must be at least 1, got %d" , count )
89114 }
90115 }
91116
92- start := 0
93- count := DEFAULT_LINE_COUNT
94- if params .LineStart != nil {
95- start = * params .LineStart
96- }
97- if params .LineEnd != nil {
98- count = * params .LineEnd - start
117+ if offset < 0 {
118+ offset = 0
99119 }
100120
101- if start < 0 {
102- start = 0
103- }
104- if start > len (lines ) {
105- start = len (lines )
106- }
121+ var lines []string
122+ var stopReason string
107123
108- end := start + count
109- if end > len (lines ) {
110- end = len (lines )
124+ if _ , err := file .Seek (0 , 0 ); err != nil {
125+ return nil , fmt .Errorf ("failed to seek to start of file: %w" , err )
111126 }
112- if end < start {
113- end = start
114- }
115-
116- selectedLines := lines [start :end ]
117127
118- var dataBuilder strings. Builder
119- for i , line := range selectedLines {
120- if i > 0 {
121- dataBuilder . WriteString ( " \n " )
128+ if origin == "end" {
129+ lines , stopReason , err = readutil . ReadTailLines ( file , count , offset , int64 ( ReadLimit ))
130+ if err != nil {
131+ return nil , fmt . Errorf ( "error reading file from end: %w" , err )
122132 }
123- dataBuilder .WriteString (line )
124- }
125- data := dataBuilder .String ()
126-
127- truncated := ""
128- currentBytes := len (data )
129- if currentBytes >= maxBytes {
130- truncated = "max_bytes"
131- lastNewline := bytes .LastIndexByte ([]byte (data [:maxBytes ]), '\n' )
132- if lastNewline > 0 {
133- data = data [:lastNewline ]
134- } else {
135- data = data [:maxBytes ]
133+ } else {
134+ lines , stopReason , err = readutil .ReadLines (file , count , offset , ReadLimit )
135+ if err != nil {
136+ return nil , fmt .Errorf ("error reading file: %w" , err )
136137 }
137- } else if end >= len (lines ) {
138- truncated = "eof"
139138 }
140139
141- if truncated == "" {
142- truncated = "null"
140+ data := strings .Join (lines , "" )
141+ data = strings .TrimSuffix (data , "\n " )
142+
143+ if len (data ) > maxBytes {
144+ data = truncateData (data , origin , maxBytes )
145+ stopReason = StopReasonMaxBytes
143146 }
144147
145- return map [string ]any {
148+ result := map [string ]any {
146149 "total_size" : totalSize ,
147- "line_count" : totalLines ,
148150 "data" : data ,
149151 "modified" : utilfn .FormatRelativeTime (modTime ),
150152 "modified_time" : modTime .UTC ().Format ("2006-01-02 15:04:05 UTC" ),
151- "truncated" : truncated ,
152- }, nil
153+ }
154+ if stopReason != "" {
155+ result ["truncated" ] = stopReason
156+ }
157+
158+ return result , nil
153159}
154160
155161func GetReadTextFileToolDefinition () uctypes.ToolDefinition {
@@ -165,29 +171,34 @@ func GetReadTextFileToolDefinition() uctypes.ToolDefinition {
165171 "type" : "string" ,
166172 "description" : "Path to the file to read" ,
167173 },
168- "line_start" : map [string ]any {
174+ "origin" : map [string ]any {
175+ "type" : "string" ,
176+ "enum" : []string {"start" , "end" },
177+ "default" : "start" ,
178+ "description" : "Where to read from: 'start' (default) or 'end' of file" ,
179+ },
180+ "offset" : map [string ]any {
169181 "type" : "integer" ,
170182 "minimum" : 0 ,
171- "description" : "Starting line number (0-based). If from_end is true, this is lines from the end." ,
183+ "default" : 0 ,
184+ "description" : "Lines to skip. From 'start': 0-based line index. From 'end': lines to skip from the end (0 = very last line)" ,
172185 },
173- "line_end " : map [string ]any {
186+ "count " : map [string ]any {
174187 "type" : "integer" ,
175- "minimum" : 0 ,
176- "description" : "Ending line number (exclusive). If from_end is true, this is lines from the end." ,
188+ "minimum" : 1 ,
189+ "default" : 100 ,
190+ "description" : "Number of lines to return" ,
177191 },
178192 "max_bytes" : map [string ]any {
179193 "type" : "integer" ,
180194 "minimum" : 1 ,
181- "description" : "Maximum bytes to return (default: 51200). Data will be truncated if it exceeds this." ,
182- },
183- "from_end" : map [string ]any {
184- "type" : "boolean" ,
185- "description" : "If true, read lines from the end of the file instead of the beginning (default: false)" ,
195+ "default" : 51200 ,
196+ "description" : "Maximum bytes to return. If the result exceeds this, it will be truncated at line boundaries" ,
186197 },
187198 },
188199 "required" : []string {"filename" },
189200 "additionalProperties" : false ,
190201 },
191202 ToolAnyCallback : readTextFileCallback ,
192203 }
193- }
204+ }
0 commit comments