| 133 | } |
| 134 | |
| 135 | func runRaw(parent context.Context, call chmctx.ToolCall) string { |
| 136 | // A truncated/oversized tool call leaves llm.resolve()'s _parse_error |
| 137 | // sentinel where real args should be. Without this guard the call falls |
| 138 | // through to an empty path/cmd and returns a misleading "(empty path)", |
| 139 | // hiding that the server cut the arguments at its output-token limit, the |
| 140 | // failure that makes a model re-emit the same too-large write for minutes. |
| 141 | // Name the real cause and the recovery so it self-corrects in one step. |
| 142 | if msg, ok := call.Arguments["_parse_error"].(string); ok { |
| 143 | return fmt.Sprintf("(tool arguments were not valid JSON: %s, most likely the "+ |
| 144 | "content was too large and the server truncated the call at its output-token "+ |
| 145 | "limit. Do NOT retry the same whole-file write. Build the file in chunks with "+ |
| 146 | "bash heredoc append: `cat > path <<'EOF'` … `EOF` for the first part, then "+ |
| 147 | "repeated `cat >> path <<'EOF'` … `EOF` for each next part, then verify with "+ |
| 148 | "`wc -c path`.)", msg) |
| 149 | } |
| 150 | switch call.Name { |
| 151 | case BashName: |
| 152 | cmd, _ := call.Arguments["cmd"].(string) |
| 153 | // Default 2m, overridable per call up to 1h. Clamp seconds BEFORE the |
| 154 | // Duration multiply: 1e18 would overflow int64 into a negative duration, |
| 155 | // and 0.5 would truncate to 0 and cancel before the shell runs, so |
| 156 | // floor at 1. |
| 157 | timeout := 2 * time.Minute |
| 158 | if secs, ok := call.Arguments["timeout_seconds"].(float64); ok && secs > 0 { |
| 159 | secs = min(max(secs, 1), maxBashTimeoutSeconds) |
| 160 | timeout = time.Duration(secs) * time.Second |
| 161 | } |
| 162 | return Bash(parent, cmd, timeout) |
| 163 | case WriteFileName: |
| 164 | path, _ := call.Arguments["path"].(string) |
| 165 | // A missing/non-string content (valid JSON, so no _parse_error; schema |
| 166 | // `required` is not enforced by open-source backends) must not decode |
| 167 | // to "" and silently truncate an existing file to 0 bytes behind a |
| 168 | // success-shaped result. An explicit `"content": ""` still writes. |
| 169 | content, ok := call.Arguments["content"].(string) |
| 170 | if !ok { |
| 171 | return `(missing content argument: the call carried no string "content", refusing to write - resend with the full content; an intentionally empty file needs an explicit "content": "")` |
| 172 | } |
| 173 | return WriteFile(path, content) |
| 174 | case EditFileName: |
| 175 | path, _ := call.Arguments["path"].(string) |
| 176 | oldString, _ := call.Arguments["old_string"].(string) |
| 177 | // Same guard as write_file's content: a dropped new_string must not |
| 178 | // decode to "" and silently delete the matched text. An explicit |
| 179 | // `"new_string": ""` still deletes. |
| 180 | newString, ok := call.Arguments["new_string"].(string) |
| 181 | if !ok { |
| 182 | return `(missing new_string argument: the call carried no string "new_string", refusing to edit - resend it; deleting the match needs an explicit "new_string": "")` |
| 183 | } |
| 184 | return EditFile(path, oldString, newString) |
| 185 | case ReadFileName: |
| 186 | path, _ := call.Arguments["path"].(string) |
| 187 | return ReadFile(path) |
| 188 | default: |
| 189 | return fmt.Sprintf("(unknown tool: %s)", call.Name) |
| 190 | } |
| 191 | } |
| 192 | |