on main help sign in
handle errors correctly in the other tools
commit0b3e5f5bd42a02c2a15b394b3768e517dc43f39c authorKujtim Hoxha <kujtimii.h@gmail.com> dateMon 14 Apr 2025, 11:24 (2 years ago) parent921f5ee
8 files changed+72−26
internal/llm/tools/edit.go+6−2 internal/llm/tools/fetch.go+4−3 internal/llm/tools/glob.go+13−2 internal/llm/tools/grep.go+13−2 internal/llm/tools/ls.go+13−2 internal/llm/tools/sourcegraph.go+10−5 internal/llm/tools/view.go+3−2 internal/llm/tools/write.go+10−8
internal/llm/tools/edit.go+6−2
@@ -27,8 +27,9 @@ type EditPermissionsParams struct {
2727}
2828
2929type EditResponseMetadata struct {
30− Additions int `json:"additions"`
31− Removals int `json:"removals"`
30+ Diff string `json:"diff"`
31+ Additions int `json:"additions"`
32+ Removals int `json:"removals"`
3233}
3334
3435type editTool struct {
@@ -216,6 +217,7 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string)
216217 return WithResponseMetadata(
217218 NewTextResponse("File created: "+filePath),
218219 EditResponseMetadata{
220+ Diff: diff,
219221 Additions: stats.Additions,
220222 Removals: stats.Removals,
221223 },
@@ -308,6 +310,7 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string
308310 return WithResponseMetadata(
309311 NewTextResponse("Content deleted from file: "+filePath),
310312 EditResponseMetadata{
313+ Diff: diff,
311314 Additions: stats.Additions,
312315 Removals: stats.Removals,
313316 },
@@ -401,6 +404,7 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS
401404 return WithResponseMetadata(
402405 NewTextResponse("Content replaced in file: "+filePath),
403406 EditResponseMetadata{
407+ Diff: diff,
404408 Additions: stats.Additions,
405409 Removals: stats.Removals,
406410 }), nil
internal/llm/tools/fetch.go+4−3
@@ -86,6 +86,7 @@ func (t *fetchTool) Info() ToolInfo {
8686 "format": map[string]any{
8787 "type": "string",
8888 "description": "The format to return the content in (text, markdown, or html)",
89+ "enum": []string{"text", "markdown", "html"},
8990 },
9091 "timeout": map[string]any{
9192 "type": "number",
@@ -126,7 +127,7 @@ func (t *fetchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
126127 )
127128
128129 if !p {
129− return NewTextErrorResponse("Permission denied to fetch from URL: " + params.URL), nil
130+ return ToolResponse{}, permission.ErrorPermissionDenied
130131 }
131132
132133 client := t.client
@@ -142,14 +143,14 @@ func (t *fetchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
142143
143144 req, err := http.NewRequestWithContext(ctx, "GET", params.URL, nil)
144145 if err != nil {
145− return NewTextErrorResponse("Failed to create request: " + err.Error()), nil
146+ return ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
146147 }
147148
148149 req.Header.Set("User-Agent", "termai/1.0")
149150
150151 resp, err := client.Do(req)
151152 if err != nil {
152− return NewTextErrorResponse("Failed to execute request: " + err.Error()), nil
153+ return ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
153154 }
154155 defer resp.Body.Close()
155156
internal/llm/tools/glob.go+13−2
@@ -63,6 +63,11 @@ type GlobParams struct {
6363 Path string `json:"path"`
6464}
6565
66+type GlobMetadata struct {
67+ NumberOfFiles int `json:"number_of_files"`
68+ Truncated bool `json:"truncated"`
69+}
70+
6671type globTool struct{}
6772
6873func NewGlobTool() BaseTool {
@@ -104,7 +109,7 @@ func (g *globTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
104109
105110 files, truncated, err := globFiles(params.Pattern, searchPath, 100)
106111 if err != nil {
107− return NewTextErrorResponse(fmt.Sprintf("error performing glob search: %s", err)), nil
112+ return ToolResponse{}, fmt.Errorf("error finding files: %w", err)
108113 }
109114
110115 var output string
@@ -117,7 +122,13 @@ func (g *globTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
117122 }
118123 }
119124
120− return NewTextResponse(output), nil
125+ return WithResponseMetadata(
126+ NewTextResponse(output),
127+ GlobMetadata{
128+ NumberOfFiles: len(files),
129+ Truncated: truncated,
130+ },
131+ ), nil
121132}
122133
123134func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
internal/llm/tools/grep.go+13−2
@@ -27,6 +27,11 @@ type grepMatch struct {
2727 modTime time.Time
2828}
2929
30+type GrepMetadata struct {
31+ NumberOfMatches int `json:"number_of_matches"`
32+ Truncated bool `json:"truncated"`
33+}
34+
3035type grepTool struct{}
3136
3237const (
@@ -110,7 +115,7 @@ func (g *grepTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
110115
111116 matches, truncated, err := searchFiles(params.Pattern, searchPath, params.Include, 100)
112117 if err != nil {
113− return NewTextErrorResponse(fmt.Sprintf("error searching files: %s", err)), nil
118+ return ToolResponse{}, fmt.Errorf("error searching files: %w", err)
114119 }
115120
116121 var output string
@@ -127,7 +132,13 @@ func (g *grepTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
127132 }
128133 }
129134
130− return NewTextResponse(output), nil
135+ return WithResponseMetadata(
136+ NewTextResponse(output),
137+ GrepMetadata{
138+ NumberOfMatches: len(matches),
139+ Truncated: truncated,
140+ },
141+ ), nil
131142}
132143
133144func pluralize(count int) string {
internal/llm/tools/ls.go+13−2
@@ -23,6 +23,11 @@ type TreeNode struct {
2323 Children []*TreeNode `json:"children,omitempty"`
2424}
2525
26+type LSMetadata struct {
27+ NumberOfFiles int `json:"number_of_files"`
28+ Truncated bool `json:"truncated"`
29+}
30+
2631type lsTool struct{}
2732
2833const (
@@ -104,7 +109,7 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
104109
105110 files, truncated, err := listDirectory(searchPath, params.Ignore, MaxLSFiles)
106111 if err != nil {
107− return NewTextErrorResponse(fmt.Sprintf("error listing directory: %s", err)), nil
112+ return ToolResponse{}, fmt.Errorf("error listing directory: %w", err)
108113 }
109114
110115 tree := createFileTree(files)
@@ -114,7 +119,13 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
114119 output = fmt.Sprintf("There are more than %d files in the directory. Use a more specific path or use the Glob tool to find specific files. The first %d files and directories are included below:\n\n%s", MaxLSFiles, MaxLSFiles, output)
115120 }
116121
117− return NewTextResponse(output), nil
122+ return WithResponseMetadata(
123+ NewTextResponse(output),
124+ LSMetadata{
125+ NumberOfFiles: len(files),
126+ Truncated: truncated,
127+ },
128+ ), nil
118129}
119130
120131func listDirectory(initialPath string, ignorePatterns []string, limit int) ([]string, bool, error) {
internal/llm/tools/sourcegraph.go+10−5
@@ -18,6 +18,11 @@ type SourcegraphParams struct {
1818 Timeout int `json:"timeout,omitempty"`
1919}
2020
21+type SourcegraphMetadata struct {
22+ NumberOfMatches int `json:"number_of_matches"`
23+ Truncated bool `json:"truncated"`
24+}
25+
2126type sourcegraphTool struct {
2227 client *http.Client
2328}
@@ -198,7 +203,7 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
198203
199204 graphqlQueryBytes, err := json.Marshal(request)
200205 if err != nil {
201− return NewTextErrorResponse("Failed to create GraphQL request: " + err.Error()), nil
206+ return ToolResponse{}, fmt.Errorf("failed to marshal GraphQL request: %w", err)
202207 }
203208 graphqlQuery := string(graphqlQueryBytes)
204209
@@ -209,7 +214,7 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
209214 bytes.NewBuffer([]byte(graphqlQuery)),
210215 )
211216 if err != nil {
212− return NewTextErrorResponse("Failed to create request: " + err.Error()), nil
217+ return ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
213218 }
214219
215220 req.Header.Set("Content-Type", "application/json")
@@ -217,7 +222,7 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
217222
218223 resp, err := client.Do(req)
219224 if err != nil {
220− return NewTextErrorResponse("Failed to execute request: " + err.Error()), nil
225+ return ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
221226 }
222227 defer resp.Body.Close()
223228
@@ -231,12 +236,12 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
231236 }
232237 body, err := io.ReadAll(resp.Body)
233238 if err != nil {
234− return NewTextErrorResponse("Failed to read response body: " + err.Error()), nil
239+ return ToolResponse{}, fmt.Errorf("failed to read response body: %w", err)
235240 }
236241
237242 var result map[string]any
238243 if err = json.Unmarshal(body, &result); err != nil {
239− return NewTextErrorResponse("Failed to parse response: " + err.Error()), nil
244+ return ToolResponse{}, fmt.Errorf("failed to unmarshal response: %w", err)
240245 }
241246
242247 formattedResults, err := formatSourcegraphResults(result, params.ContextWindow)
internal/llm/tools/view.go+3−2
@@ -135,7 +135,7 @@ func (v *viewTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
135135
136136 return NewTextErrorResponse(fmt.Sprintf("File not found: %s", filePath)), nil
137137 }
138− return NewTextErrorResponse(fmt.Sprintf("Failed to access file: %s", err)), nil
138+ return ToolResponse{}, fmt.Errorf("error accessing file: %w", err)
139139 }
140140
141141 // Check if it's a directory
@@ -156,6 +156,7 @@ func (v *viewTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
156156
157157 // Check if it's an image file
158158 isImage, imageType := isImageFile(filePath)
159+ // TODO: handle images
159160 if isImage {
160161 return NewTextErrorResponse(fmt.Sprintf("This is an image file of type: %s\nUse a different tool to process images", imageType)), nil
161162 }
@@ -163,7 +164,7 @@ func (v *viewTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
163164 // Read the file content
164165 content, lineCount, err := readTextFile(filePath, params.Offset, params.Limit)
165166 if err != nil {
166− return NewTextErrorResponse(fmt.Sprintf("Failed to read file: %s", err)), nil
167+ return ToolResponse{}, fmt.Errorf("error reading file: %w", err)
167168 }
168169
169170 notifyLspOpenFile(ctx, filePath, v.lspClients)
internal/llm/tools/write.go+10−8
@@ -30,8 +30,9 @@ type writeTool struct {
3030}
3131
3232type WriteResponseMetadata struct {
33− Additions int `json:"additions"`
34− Removals int `json:"removals"`
33+ Diff string `json:"diff"`
34+ Additions int `json:"additions"`
35+ Removals int `json:"removals"`
3536}
3637
3738const (
@@ -128,12 +129,12 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
128129 return NewTextErrorResponse(fmt.Sprintf("File %s already contains the exact content. No changes made.", filePath)), nil
129130 }
130131 } else if !os.IsNotExist(err) {
131− return NewTextErrorResponse(fmt.Sprintf("Failed to access file: %s", err)), nil
132+ return ToolResponse{}, fmt.Errorf("error checking file: %w", err)
132133 }
133134
134135 dir := filepath.Dir(filePath)
135136 if err = os.MkdirAll(dir, 0o755); err != nil {
136− return NewTextErrorResponse(fmt.Sprintf("Failed to create parent directories: %s", err)), nil
137+ return ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
137138 }
138139
139140 oldContent := ""
@@ -146,7 +147,7 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
146147
147148 sessionID, messageID := GetContextValues(ctx)
148149 if sessionID == "" || messageID == "" {
149− return NewTextErrorResponse("session ID or message ID is missing"), nil
150+ return ToolResponse{}, fmt.Errorf("session_id and message_id are required")
150151 }
151152 diff, stats, err := git.GenerateGitDiffWithStats(
152153 removeWorkingDirectoryPrefix(filePath),
@@ -154,7 +155,7 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
154155 params.Content,
155156 )
156157 if err != nil {
157− return NewTextErrorResponse(fmt.Sprintf("Failed to get file diff: %s", err)), nil
158+ return ToolResponse{}, fmt.Errorf("error generating diff: %w", err)
158159 }
159160 p := w.permissions.Request(
160161 permission.CreatePermissionRequest{
@@ -169,12 +170,12 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
169170 },
170171 )
171172 if !p {
172− return NewTextErrorResponse(fmt.Sprintf("Permission denied to create file: %s", filePath)), nil
173+ return ToolResponse{}, permission.ErrorPermissionDenied
173174 }
174175
175176 err = os.WriteFile(filePath, []byte(params.Content), 0o644)
176177 if err != nil {
177− return NewTextErrorResponse(fmt.Sprintf("Failed to write file: %s", err)), nil
178+ return ToolResponse{}, fmt.Errorf("error writing file: %w", err)
178179 }
179180
180181 recordFileWrite(filePath)
@@ -186,6 +187,7 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
186187 result += getDiagnostics(filePath, w.lspClients)
187188 return WithResponseMetadata(NewTextResponse(result),
188189 WriteResponseMetadata{
190+ Diff: diff,
189191 Additions: stats.Additions,
190192 Removals: stats.Removals,
191193 },