1use crate::{
2 error::ToolError,
3 tools::todo::{TodoItem, TodoWriteFileOperation},
4};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub enum ToolResult {
10 Search(SearchResult), FileList(FileListResult), FileContent(FileContentResult),
14 Edit(EditResult),
15 Bash(BashResult),
16 Glob(GlobResult),
17 TodoRead(TodoListResult),
18 TodoWrite(TodoWriteResult),
19 Fetch(FetchResult),
20 Agent(AgentResult),
21
22 External(ExternalResult),
24
25 Error(ToolError),
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct FetchResult {
32 pub url: String,
33 pub content: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct AgentResult {
39 pub content: String,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ExternalResult {
45 pub tool_name: String, pub payload: String, }
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct SearchResult {
52 pub matches: Vec<SearchMatch>,
53 pub total_files_searched: usize,
54 pub search_completed: bool,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct SearchMatch {
59 pub file_path: String,
60 pub line_number: usize,
61 pub line_content: String,
62 pub column_range: Option<(usize, usize)>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct FileListResult {
68 pub entries: Vec<FileEntry>,
69 pub base_path: String,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct FileEntry {
74 pub path: String,
75 pub is_directory: bool,
76 pub size: Option<u64>,
77 pub permissions: Option<String>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct FileContentResult {
83 pub content: String,
84 pub file_path: String,
85 pub line_count: usize,
86 pub truncated: bool,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct EditResult {
92 pub file_path: String,
93 pub changes_made: usize,
94 pub file_created: bool,
95 pub old_content: Option<String>,
96 pub new_content: Option<String>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct BashResult {
102 pub stdout: String,
103 pub stderr: String,
104 pub exit_code: i32,
105 pub command: String,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct GlobResult {
111 pub matches: Vec<String>,
112 pub pattern: String,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct TodoListResult {
118 pub todos: Vec<TodoItem>,
119}
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct TodoWriteResult {
123 pub todos: Vec<TodoItem>,
124 pub operation: TodoWriteFileOperation,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct MultiEditResult(pub EditResult);
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct ReplaceResult(pub EditResult);
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct AstGrepResult(pub SearchResult);
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct GrepResult(pub SearchResult);
136
137pub trait ToolOutput: Serialize + Send + Sync + 'static {}
139
140impl ToolOutput for SearchResult {}
142impl ToolOutput for GrepResult {}
143impl ToolOutput for FileListResult {}
144impl ToolOutput for FileContentResult {}
145impl ToolOutput for EditResult {}
146impl ToolOutput for BashResult {}
147impl ToolOutput for GlobResult {}
148impl ToolOutput for TodoListResult {}
149impl ToolOutput for TodoWriteResult {}
150impl ToolOutput for MultiEditResult {}
151impl ToolOutput for ReplaceResult {}
152impl ToolOutput for AstGrepResult {}
153impl ToolOutput for ExternalResult {}
154impl ToolOutput for FetchResult {}
155impl ToolOutput for AgentResult {}
156impl ToolOutput for ToolResult {}
157
158impl From<ExternalResult> for ToolResult {
160 fn from(r: ExternalResult) -> Self {
161 Self::External(r)
162 }
163}
164
165impl From<ToolError> for ToolResult {
166 fn from(e: ToolError) -> Self {
167 Self::Error(e)
168 }
169}
170
171impl ToolResult {
172 pub fn llm_format(&self) -> String {
174 match self {
175 ToolResult::Search(r) => {
176 if r.matches.is_empty() {
177 "No matches found.".to_string()
178 } else {
179 let mut output = Vec::new();
180 let mut current_file = "";
181
182 for match_item in &r.matches {
183 if match_item.file_path != current_file {
184 if !output.is_empty() {
185 output.push("".to_string());
186 }
187 current_file = &match_item.file_path;
188 }
189 output.push(format!(
190 "{}:{}: {}",
191 match_item.file_path, match_item.line_number, match_item.line_content
192 ));
193 }
194
195 output.join("\n")
196 }
197 }
198 ToolResult::FileList(r) => {
199 if r.entries.is_empty() {
200 format!("No entries found in {}", r.base_path)
201 } else {
202 let mut lines = Vec::new();
203 for entry in &r.entries {
204 let type_indicator = if entry.is_directory { "/" } else { "" };
205 let size_str = entry.size.map(|s| format!(" ({s})")).unwrap_or_default();
206 lines.push(format!("{}{}{}", entry.path, type_indicator, size_str));
207 }
208 lines.join("\n")
209 }
210 }
211 ToolResult::FileContent(r) => r.content.clone(),
212 ToolResult::Edit(r) => {
213 if r.file_created {
214 format!("Successfully created {}", r.file_path)
215 } else {
216 format!(
217 "Successfully edited {}: {} change(s) made",
218 r.file_path, r.changes_made
219 )
220 }
221 }
222 ToolResult::Bash(r) => {
223 fn truncate_output(s: &str, max_chars: usize, max_lines: usize) -> String {
225 let lines: Vec<&str> = s.lines().collect();
226 let char_count = s.len();
227
228 if lines.len() > max_lines || char_count > max_chars {
230 let head_lines = max_lines / 2;
232 let tail_lines = max_lines - head_lines;
233
234 let mut result = String::new();
235
236 for line in lines.iter().take(head_lines) {
238 result.push_str(line);
239 result.push('\n');
240 }
241
242 let omitted_lines = lines.len().saturating_sub(max_lines);
244 result.push_str(&format!(
245 "\n[... {omitted_lines} lines omitted ({char_count} total chars) ...]\n\n"
246 ));
247
248 if tail_lines > 0 && lines.len() > head_lines {
250 for line in lines.iter().skip(lines.len().saturating_sub(tail_lines)) {
251 result.push_str(line);
252 result.push('\n');
253 }
254 }
255
256 result
257 } else {
258 s.to_string()
259 }
260 }
261
262 const MAX_STDOUT_CHARS: usize = 128 * 1024; const MAX_STDOUT_LINES: usize = 2000;
264 const MAX_STDERR_CHARS: usize = 64 * 1024; const MAX_STDERR_LINES: usize = 500;
266
267 let stdout_truncated =
268 truncate_output(&r.stdout, MAX_STDOUT_CHARS, MAX_STDOUT_LINES);
269 let stderr_truncated =
270 truncate_output(&r.stderr, MAX_STDERR_CHARS, MAX_STDERR_LINES);
271
272 let mut output = stdout_truncated;
273
274 if r.exit_code != 0 {
275 if !output.is_empty() && !output.ends_with('\n') {
276 output.push('\n');
277 }
278 output.push_str(&format!("Exit code: {}", r.exit_code));
279
280 if !stderr_truncated.is_empty() {
281 output.push_str(&format!("\nError output:\n{stderr_truncated}"));
282 }
283 } else if !stderr_truncated.is_empty() {
284 if !output.is_empty() && !output.ends_with('\n') {
285 output.push('\n');
286 }
287 output.push_str(&format!("Error output:\n{stderr_truncated}"));
288 }
289
290 output
291 }
292 ToolResult::Glob(r) => {
293 if r.matches.is_empty() {
294 format!("No files matching pattern: {}", r.pattern)
295 } else {
296 r.matches.join("\n")
297 }
298 }
299 ToolResult::TodoRead(r) => {
300 if r.todos.is_empty() {
301 "No todos found.".to_string()
302 } else {
303 format!(
304 "Remember to continue to update and read from the todo list as you make progress. Here is the current list:\n{}",
305 serde_json::to_string_pretty(&r.todos)
306 .unwrap_or_else(|_| "Failed to format todos".to_string())
307 )
308 }
309 }
310 ToolResult::TodoWrite(r) => {
311 format!(
312 "Todos have been {:?} successfully. Ensure that you continue to read and update the todo list as you work on tasks.\n{}",
313 r.operation,
314 serde_json::to_string_pretty(&r.todos)
315 .unwrap_or_else(|_| "Failed to format todos".to_string())
316 )
317 }
318 ToolResult::Fetch(r) => {
319 format!("Fetched content from {}:\n{}", r.url, r.content)
320 }
321 ToolResult::Agent(r) => r.content.clone(),
322 ToolResult::External(r) => r.payload.clone(),
323 ToolResult::Error(e) => format!("Error: {e}"),
324 }
325 }
326
327 pub fn variant_name(&self) -> &'static str {
329 match self {
330 ToolResult::Search(_) => "Search",
331 ToolResult::FileList(_) => "FileList",
332 ToolResult::FileContent(_) => "FileContent",
333 ToolResult::Edit(_) => "Edit",
334 ToolResult::Bash(_) => "Bash",
335 ToolResult::Glob(_) => "Glob",
336 ToolResult::TodoRead(_) => "TodoRead",
337 ToolResult::TodoWrite(_) => "TodoWrite",
338 ToolResult::Fetch(_) => "Fetch",
339 ToolResult::Agent(_) => "Agent",
340 ToolResult::External(_) => "External",
341 ToolResult::Error(_) => "Error",
342 }
343 }
344}