1use crate::error::ToolError;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub enum ToolResult {
7 Search(SearchResult), FileList(FileListResult), FileContent(FileContentResult),
11 Edit(EditResult),
12 Bash(BashResult),
13 Glob(GlobResult),
14 TodoRead(TodoListResult),
15 TodoWrite(TodoWriteResult),
16 Fetch(FetchResult),
17 Agent(AgentResult),
18
19 External(ExternalResult),
21
22 Error(ToolError),
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct FetchResult {
29 pub url: String,
30 pub content: String,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct AgentResult {
36 pub content: String,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ExternalResult {
42 pub tool_name: String, pub payload: String, }
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct SearchResult {
49 pub matches: Vec<SearchMatch>,
50 pub total_files_searched: usize,
51 pub search_completed: bool,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct SearchMatch {
56 pub file_path: String,
57 pub line_number: usize,
58 pub line_content: String,
59 pub column_range: Option<(usize, usize)>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct FileListResult {
65 pub entries: Vec<FileEntry>,
66 pub base_path: String,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct FileEntry {
71 pub path: String,
72 pub is_directory: bool,
73 pub size: Option<u64>,
74 pub permissions: Option<String>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct FileContentResult {
80 pub content: String,
81 pub file_path: String,
82 pub line_count: usize,
83 pub truncated: bool,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct EditResult {
89 pub file_path: String,
90 pub changes_made: usize,
91 pub file_created: bool,
92 pub old_content: Option<String>,
93 pub new_content: Option<String>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct BashResult {
99 pub stdout: String,
100 pub stderr: String,
101 pub exit_code: i32,
102 pub command: String,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct GlobResult {
108 pub matches: Vec<String>,
109 pub pattern: String,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct TodoListResult {
115 pub todos: Vec<TodoItem>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct TodoItem {
120 pub id: String,
121 pub content: String,
122 pub status: String,
123 pub priority: String,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct TodoWriteResult {
129 pub todos: Vec<TodoItem>,
130 pub operation: String, }
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct MultiEditResult(pub EditResult);
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct ReplaceResult(pub EditResult);
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct AstGrepResult(pub SearchResult);
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct GrepResult(pub SearchResult);
142
143pub trait ToolOutput: Serialize + Send + Sync + 'static {}
145
146impl ToolOutput for SearchResult {}
148impl ToolOutput for GrepResult {}
149impl ToolOutput for FileListResult {}
150impl ToolOutput for FileContentResult {}
151impl ToolOutput for EditResult {}
152impl ToolOutput for BashResult {}
153impl ToolOutput for GlobResult {}
154impl ToolOutput for TodoListResult {}
155impl ToolOutput for TodoWriteResult {}
156impl ToolOutput for MultiEditResult {}
157impl ToolOutput for ReplaceResult {}
158impl ToolOutput for AstGrepResult {}
159impl ToolOutput for ExternalResult {}
160impl ToolOutput for FetchResult {}
161impl ToolOutput for AgentResult {}
162impl ToolOutput for ToolResult {}
163
164impl From<ExternalResult> for ToolResult {
166 fn from(r: ExternalResult) -> Self {
167 Self::External(r)
168 }
169}
170
171impl From<ToolError> for ToolResult {
172 fn from(e: ToolError) -> Self {
173 Self::Error(e)
174 }
175}
176
177impl ToolResult {
178 pub fn llm_format(&self) -> String {
180 match self {
181 ToolResult::Search(r) => {
182 if r.matches.is_empty() {
183 "No matches found.".to_string()
184 } else {
185 let mut output = Vec::new();
186 let mut current_file = "";
187
188 for match_item in &r.matches {
189 if match_item.file_path != current_file {
190 if !output.is_empty() {
191 output.push("".to_string());
192 }
193 current_file = &match_item.file_path;
194 }
195 output.push(format!(
196 "{}:{}: {}",
197 match_item.file_path, match_item.line_number, match_item.line_content
198 ));
199 }
200
201 output.join("\n")
202 }
203 }
204 ToolResult::FileList(r) => {
205 if r.entries.is_empty() {
206 format!("No entries found in {}", r.base_path)
207 } else {
208 let mut lines = Vec::new();
209 for entry in &r.entries {
210 let type_indicator = if entry.is_directory { "/" } else { "" };
211 let size_str = entry.size.map(|s| format!(" ({s})")).unwrap_or_default();
212 lines.push(format!("{}{}{}", entry.path, type_indicator, size_str));
213 }
214 lines.join("\n")
215 }
216 }
217 ToolResult::FileContent(r) => r.content.clone(),
218 ToolResult::Edit(r) => {
219 if r.file_created {
220 format!("Successfully created {}", r.file_path)
221 } else {
222 format!(
223 "Successfully edited {}: {} change(s) made",
224 r.file_path, r.changes_made
225 )
226 }
227 }
228 ToolResult::Bash(r) => {
229 let mut output = r.stdout.clone();
230
231 if r.exit_code != 0 {
232 if !output.is_empty() && !output.ends_with('\n') {
233 output.push('\n');
234 }
235 output.push_str(&format!("Exit code: {}", r.exit_code));
236
237 if !r.stderr.is_empty() {
238 output.push_str(&format!("\nError output:\n{}", r.stderr));
239 }
240 } else if !r.stderr.is_empty() {
241 if !output.is_empty() && !output.ends_with('\n') {
242 output.push('\n');
243 }
244 output.push_str(&format!("Error output:\n{}", r.stderr));
245 }
246
247 output
248 }
249 ToolResult::Glob(r) => {
250 if r.matches.is_empty() {
251 format!("No files matching pattern: {}", r.pattern)
252 } else {
253 r.matches.join("\n")
254 }
255 }
256 ToolResult::TodoRead(r) => {
257 if r.todos.is_empty() {
258 "No todos found.".to_string()
259 } else {
260 format!(
261 "Remember to continue to use update and read from the todo list as you make progress. Here is the current list:\n{}",
262 serde_json::to_string_pretty(&r.todos)
263 .unwrap_or_else(|_| "Failed to format todos".to_string())
264 )
265 }
266 }
267 ToolResult::TodoWrite(r) => {
268 format!(
269 "Todos have been {} successfully. Ensure that you continue to read and update the todo list as you work on tasks.\n{}",
270 r.operation,
271 serde_json::to_string_pretty(&r.todos)
272 .unwrap_or_else(|_| "Failed to format todos".to_string())
273 )
274 }
275 ToolResult::Fetch(r) => {
276 format!("Fetched content from {}:\n{}", r.url, r.content)
277 }
278 ToolResult::Agent(r) => r.content.clone(),
279 ToolResult::External(r) => r.payload.clone(),
280 ToolResult::Error(e) => format!("Error: {e}"),
281 }
282 }
283
284 pub fn variant_name(&self) -> &'static str {
286 match self {
287 ToolResult::Search(_) => "Search",
288 ToolResult::FileList(_) => "FileList",
289 ToolResult::FileContent(_) => "FileContent",
290 ToolResult::Edit(_) => "Edit",
291 ToolResult::Bash(_) => "Bash",
292 ToolResult::Glob(_) => "Glob",
293 ToolResult::TodoRead(_) => "TodoRead",
294 ToolResult::TodoWrite(_) => "TodoWrite",
295 ToolResult::Fetch(_) => "Fetch",
296 ToolResult::Agent(_) => "Agent",
297 ToolResult::External(_) => "External",
298 ToolResult::Error(_) => "Error",
299 }
300 }
301}