on main help sign in
handle errors correctly in the other tools
0b3e5f5Kujtim Hoxha
internal/llm/tools/edit.go+6−2
@@ -27,8 +27,9 @@ type EditPermissionsParams struct {
27}
27}
28
28
29type EditResponseMetadata struct {
29type EditResponseMetadata struct {
30+ Diff string `json:"diff"`
30− Additions int `json:"additions"`
31+ Additions int `json:"additions"`
31− Removals int `json:"removals"`
32+ Removals int `json:"removals"`
32}
33}
33
34
34type editTool struct {
35type editTool struct {
@@ -216,6 +217,7 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string)
216 return WithResponseMetadata(
217 return WithResponseMetadata(
217 NewTextResponse("File created: "+filePath),
218 NewTextResponse("File created: "+filePath),
218 EditResponseMetadata{
219 EditResponseMetadata{
220+ Diff: diff,
219 Additions: stats.Additions,
221 Additions: stats.Additions,
220 Removals: stats.Removals,
222 Removals: stats.Removals,
221 },
223 },
@@ -308,6 +310,7 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string
308 return WithResponseMetadata(
310 return WithResponseMetadata(
309 NewTextResponse("Content deleted from file: "+filePath),
311 NewTextResponse("Content deleted from file: "+filePath),
310 EditResponseMetadata{
312 EditResponseMetadata{
313+ Diff: diff,
311 Additions: stats.Additions,
314 Additions: stats.Additions,
312 Removals: stats.Removals,
315 Removals: stats.Removals,
313 },
316 },
@@ -401,6 +404,7 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS
401 return WithResponseMetadata(
404 return WithResponseMetadata(
402 NewTextResponse("Content replaced in file: "+filePath),
405 NewTextResponse("Content replaced in file: "+filePath),
403 EditResponseMetadata{
406 EditResponseMetadata{
407+ Diff: diff,
404 Additions: stats.Additions,
408 Additions: stats.Additions,
405 Removals: stats.Removals,
409 Removals: stats.Removals,
406 }), nil
410 }), nil
internal/llm/tools/fetch.go+4−3
@@ -86,6 +86,7 @@ func (t *fetchTool) Info() ToolInfo {
86 "format": map[string]any{
86 "format": map[string]any{
87 "type": "string",
87 "type": "string",
88 "description": "The format to return the content in (text, markdown, or html)",
88 "description": "The format to return the content in (text, markdown, or html)",
89+ "enum": []string{"text", "markdown", "html"},
89 },
90 },
90 "timeout": map[string]any{
91 "timeout": map[string]any{
91 "type": "number",
92 "type": "number",
@@ -126,7 +127,7 @@ func (t *fetchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
126 )
127 )
127
128
128 if !p {
129 if !p {
129− return NewTextErrorResponse("Permission denied to fetch from URL: " + params.URL), nil
130+ return ToolResponse{}, permission.ErrorPermissionDenied
130 }
131 }
131
132
132 client := t.client
133 client := t.client
@@ -142,14 +143,14 @@ func (t *fetchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
142
143
143 req, err := http.NewRequestWithContext(ctx, "GET", params.URL, nil)
144 req, err := http.NewRequestWithContext(ctx, "GET", params.URL, nil)
144 if err != nil {
145 if err != nil {
145− return NewTextErrorResponse("Failed to create request: " + err.Error()), nil
146+ return ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
146 }
147 }
147
148
148 req.Header.Set("User-Agent", "termai/1.0")
149 req.Header.Set("User-Agent", "termai/1.0")
149
150
150 resp, err := client.Do(req)
151 resp, err := client.Do(req)
151 if err != nil {
152 if err != nil {
152− return NewTextErrorResponse("Failed to execute request: " + err.Error()), nil
153+ return ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
153 }
154 }
154 defer resp.Body.Close()
155 defer resp.Body.Close()
155
156
internal/llm/tools/glob.go+13−2
@@ -63,6 +63,11 @@ type GlobParams struct {
63 Path string `json:"path"`
63 Path string `json:"path"`
64}
64}
65
65
66+type GlobMetadata struct {
67+ NumberOfFiles int `json:"number_of_files"`
68+ Truncated bool `json:"truncated"`
69+}
70+
66type globTool struct{}
71type globTool struct{}
67
72
68func NewGlobTool() BaseTool {
73func NewGlobTool() BaseTool {
@@ -104,7 +109,7 @@ func (g *globTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
104
109
105 files, truncated, err := globFiles(params.Pattern, searchPath, 100)
110 files, truncated, err := globFiles(params.Pattern, searchPath, 100)
106 if err != nil {
111 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)
108 }
113 }
109
114
110 var output string
115 var output string
@@ -117,7 +122,13 @@ func (g *globTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
117 }
122 }
118 }
123 }
119
124
125+ return WithResponseMetadata(
120− return NewTextResponse(output), nil
126+ NewTextResponse(output),
127+ GlobMetadata{
128+ NumberOfFiles: len(files),
129+ Truncated: truncated,
130+ },
131+ ), nil
121}
132}
122
133
123func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
134func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
internal/llm/tools/grep.go+13−2
@@ -27,6 +27,11 @@ type grepMatch struct {
27 modTime time.Time
27 modTime time.Time
28}
28}
29
29
30+type GrepMetadata struct {
31+ NumberOfMatches int `json:"number_of_matches"`
32+ Truncated bool `json:"truncated"`
33+}
34+
30type grepTool struct{}
35type grepTool struct{}
31
36
32const (
37const (
@@ -110,7 +115,7 @@ func (g *grepTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
110
115
111 matches, truncated, err := searchFiles(params.Pattern, searchPath, params.Include, 100)
116 matches, truncated, err := searchFiles(params.Pattern, searchPath, params.Include, 100)
112 if err != nil {
117 if err != nil {
113− return NewTextErrorResponse(fmt.Sprintf("error searching files: %s", err)), nil
118+ return ToolResponse{}, fmt.Errorf("error searching files: %w", err)
114 }
119 }
115
120
116 var output string
121 var output string
@@ -127,7 +132,13 @@ func (g *grepTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
127 }
132 }
128 }
133 }
129
134
135+ return WithResponseMetadata(
130− return NewTextResponse(output), nil
136+ NewTextResponse(output),
137+ GrepMetadata{
138+ NumberOfMatches: len(matches),
139+ Truncated: truncated,
140+ },
141+ ), nil
131}
142}
132
143
133func pluralize(count int) string {
144func pluralize(count int) string {
internal/llm/tools/ls.go+13−2
@@ -23,6 +23,11 @@ type TreeNode struct {
23 Children []*TreeNode `json:"children,omitempty"`
23 Children []*TreeNode `json:"children,omitempty"`
24}
24}
25
25
26+type LSMetadata struct {
27+ NumberOfFiles int `json:"number_of_files"`
28+ Truncated bool `json:"truncated"`
29+}
30+
26type lsTool struct{}
31type lsTool struct{}
27
32
28const (
33const (
@@ -104,7 +109,7 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
104
109
105 files, truncated, err := listDirectory(searchPath, params.Ignore, MaxLSFiles)
110 files, truncated, err := listDirectory(searchPath, params.Ignore, MaxLSFiles)
106 if err != nil {
111 if err != nil {
107− return NewTextErrorResponse(fmt.Sprintf("error listing directory: %s", err)), nil
112+ return ToolResponse{}, fmt.Errorf("error listing directory: %w", err)
108 }
113 }
109
114
110 tree := createFileTree(files)
115 tree := createFileTree(files)
@@ -114,7 +119,13 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
114 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)
119 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)
115 }
120 }
116
121
122+ return WithResponseMetadata(
117− return NewTextResponse(output), nil
123+ NewTextResponse(output),
124+ LSMetadata{
125+ NumberOfFiles: len(files),
126+ Truncated: truncated,
127+ },
128+ ), nil
118}
129}
119
130
120func listDirectory(initialPath string, ignorePatterns []string, limit int) ([]string, bool, error) {
131func listDirectory(initialPath string, ignorePatterns []string, limit int) ([]string, bool, error) {
internal/llm/tools/sourcegraph.go+10−5
@@ -18,6 +18,11 @@ type SourcegraphParams struct {
18 Timeout int `json:"timeout,omitempty"`
18 Timeout int `json:"timeout,omitempty"`
19}
19}
20
20
21+type SourcegraphMetadata struct {
22+ NumberOfMatches int `json:"number_of_matches"`
23+ Truncated bool `json:"truncated"`
24+}
25+
21type sourcegraphTool struct {
26type sourcegraphTool struct {
22 client *http.Client
27 client *http.Client
23}
28}
@@ -198,7 +203,7 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
198
203
199 graphqlQueryBytes, err := json.Marshal(request)
204 graphqlQueryBytes, err := json.Marshal(request)
200 if err != nil {
205 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)
202 }
207 }
203 graphqlQuery := string(graphqlQueryBytes)
208 graphqlQuery := string(graphqlQueryBytes)
204
209
@@ -209,7 +214,7 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
209 bytes.NewBuffer([]byte(graphqlQuery)),
214 bytes.NewBuffer([]byte(graphqlQuery)),
210 )
215 )
211 if err != nil {
216 if err != nil {
212− return NewTextErrorResponse("Failed to create request: " + err.Error()), nil
217+ return ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
213 }
218 }
214
219
215 req.Header.Set("Content-Type", "application/json")
220 req.Header.Set("Content-Type", "application/json")
@@ -217,7 +222,7 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
217
222
218 resp, err := client.Do(req)
223 resp, err := client.Do(req)
219 if err != nil {
224 if err != nil {
220− return NewTextErrorResponse("Failed to execute request: " + err.Error()), nil
225+ return ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
221 }
226 }
222 defer resp.Body.Close()
227 defer resp.Body.Close()
223
228
@@ -231,12 +236,12 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
231 }
236 }
232 body, err := io.ReadAll(resp.Body)
237 body, err := io.ReadAll(resp.Body)
233 if err != nil {
238 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)
235 }
240 }
236
241
237 var result map[string]any
242 var result map[string]any
238 if err = json.Unmarshal(body, &result); err != nil {
243 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)
240 }
245 }
241
246
242 formattedResults, err := formatSourcegraphResults(result, params.ContextWindow)
247 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)
135
135
136 return NewTextErrorResponse(fmt.Sprintf("File not found: %s", filePath)), nil
136 return NewTextErrorResponse(fmt.Sprintf("File not found: %s", filePath)), nil
137 }
137 }
138− return NewTextErrorResponse(fmt.Sprintf("Failed to access file: %s", err)), nil
138+ return ToolResponse{}, fmt.Errorf("error accessing file: %w", err)
139 }
139 }
140
140
141 // Check if it's a directory
141 // Check if it's a directory
@@ -156,6 +156,7 @@ func (v *viewTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
156
156
157 // Check if it's an image file
157 // Check if it's an image file
158 isImage, imageType := isImageFile(filePath)
158 isImage, imageType := isImageFile(filePath)
159+ // TODO: handle images
159 if isImage {
160 if isImage {
160 return NewTextErrorResponse(fmt.Sprintf("This is an image file of type: %s\nUse a different tool to process images", imageType)), nil
161 return NewTextErrorResponse(fmt.Sprintf("This is an image file of type: %s\nUse a different tool to process images", imageType)), nil
161 }
162 }
@@ -163,7 +164,7 @@ func (v *viewTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
163 // Read the file content
164 // Read the file content
164 content, lineCount, err := readTextFile(filePath, params.Offset, params.Limit)
165 content, lineCount, err := readTextFile(filePath, params.Offset, params.Limit)
165 if err != nil {
166 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)
167 }
168 }
168
169
169 notifyLspOpenFile(ctx, filePath, v.lspClients)
170 notifyLspOpenFile(ctx, filePath, v.lspClients)
internal/llm/tools/write.go+10−8
@@ -30,8 +30,9 @@ type writeTool struct {
30}
30}
31
31
32type WriteResponseMetadata struct {
32type WriteResponseMetadata struct {
33+ Diff string `json:"diff"`
33− Additions int `json:"additions"`
34+ Additions int `json:"additions"`
34− Removals int `json:"removals"`
35+ Removals int `json:"removals"`
35}
36}
36
37
37const (
38const (
@@ -128,12 +129,12 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
128 return NewTextErrorResponse(fmt.Sprintf("File %s already contains the exact content. No changes made.", filePath)), nil
129 return NewTextErrorResponse(fmt.Sprintf("File %s already contains the exact content. No changes made.", filePath)), nil
129 }
130 }
130 } else if !os.IsNotExist(err) {
131 } 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)
132 }
133 }
133
134
134 dir := filepath.Dir(filePath)
135 dir := filepath.Dir(filePath)
135 if err = os.MkdirAll(dir, 0o755); err != nil {
136 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)
137 }
138 }
138
139
139 oldContent := ""
140 oldContent := ""
@@ -146,7 +147,7 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
146
147
147 sessionID, messageID := GetContextValues(ctx)
148 sessionID, messageID := GetContextValues(ctx)
148 if sessionID == "" || messageID == "" {
149 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")
150 }
151 }
151 diff, stats, err := git.GenerateGitDiffWithStats(
152 diff, stats, err := git.GenerateGitDiffWithStats(
152 removeWorkingDirectoryPrefix(filePath),
153 removeWorkingDirectoryPrefix(filePath),
@@ -154,7 +155,7 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
154 params.Content,
155 params.Content,
155 )
156 )
156 if err != nil {
157 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)
158 }
159 }
159 p := w.permissions.Request(
160 p := w.permissions.Request(
160 permission.CreatePermissionRequest{
161 permission.CreatePermissionRequest{
@@ -169,12 +170,12 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
169 },
170 },
170 )
171 )
171 if !p {
172 if !p {
172− return NewTextErrorResponse(fmt.Sprintf("Permission denied to create file: %s", filePath)), nil
173+ return ToolResponse{}, permission.ErrorPermissionDenied
173 }
174 }
174
175
175 err = os.WriteFile(filePath, []byte(params.Content), 0o644)
176 err = os.WriteFile(filePath, []byte(params.Content), 0o644)
176 if err != nil {
177 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)
178 }
179 }
179
180
180 recordFileWrite(filePath)
181 recordFileWrite(filePath)
@@ -186,6 +187,7 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
186 result += getDiagnostics(filePath, w.lspClients)
187 result += getDiagnostics(filePath, w.lspClients)
187 return WithResponseMetadata(NewTextResponse(result),
188 return WithResponseMetadata(NewTextResponse(result),
188 WriteResponseMetadata{
189 WriteResponseMetadata{
190+ Diff: diff,
189 Additions: stats.Additions,
191 Additions: stats.Additions,
190 Removals: stats.Removals,
192 Removals: stats.Removals,
191 },
193 },