steer_tools/
result.rs

1use crate::error::ToolError;
2use serde::{Deserialize, Serialize};
3
4/// Core enum for all tool results
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub enum ToolResult {
7    // One variant per built-in tool
8    Search(SearchResult),     // grep / astgrep
9    FileList(FileListResult), // ls / glob
10    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    // Unknown or remote (MCP) tool payload
20    External(ExternalResult),
21
22    // Failure (any tool)
23    Error(ToolError),
24}
25
26/// Result for the fetch tool
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct FetchResult {
29    pub url: String,
30    pub content: String,
31}
32
33/// Result for the dispatch_agent tool
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct AgentResult {
36    pub content: String,
37}
38
39/// Result for external/MCP tools
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ExternalResult {
42    pub tool_name: String, // name reported by the MCP server
43    pub payload: String,   // raw, opaque blob (usually JSON or text)
44}
45
46/// Result for grep-like search tools
47#[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/// Result for file listing operations
63#[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/// Result for file content viewing
78#[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/// Result for edit operations
87#[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/// Result for bash command execution
97#[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/// Result for glob pattern matching
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct GlobResult {
108    pub matches: Vec<String>,
109    pub pattern: String,
110}
111
112/// Result for todo operations
113#[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/// Result for todo write operations
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct TodoWriteResult {
129    pub todos: Vec<TodoItem>,
130    pub operation: String, // e.g., "modified", "created"
131}
132
133// Newtype wrappers to avoid conflicting From impls
134#[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
143// Trait for typed tool outputs
144pub trait ToolOutput: Serialize + Send + Sync + 'static {}
145
146// Implement ToolOutput for all result types
147impl 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
164// Manual From implementations only for types not generated by the macro
165impl 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    /// Format the result for LLM consumption
179    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    /// Get the variant name as a string for metadata
285    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}