Skip to main content

steer_tools/
result.rs

1use crate::{
2    error::ToolError,
3    tools::todo::{TodoItem, TodoWriteFileOperation},
4};
5use serde::{Deserialize, Serialize};
6
7pub use steer_workspace::result::{
8    EditResult, FileContentResult, FileEntry, FileListResult, GlobResult, SearchMatch, SearchResult,
9};
10
11/// Core enum for all tool results
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum ToolResult {
14    // One variant per built-in tool
15    Search(SearchResult),     // grep / astgrep
16    FileList(FileListResult), // ls / glob
17    FileContent(FileContentResult),
18    Edit(EditResult),
19    Bash(BashResult),
20    Glob(GlobResult),
21    TodoRead(TodoListResult),
22    TodoWrite(TodoWriteResult),
23    Fetch(FetchResult),
24    Agent(AgentResult),
25
26    // Unknown or remote (MCP) tool payload
27    External(ExternalResult),
28
29    // Failure (any tool)
30    Error(ToolError),
31}
32
33/// Result for the fetch tool
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct FetchResult {
36    pub url: String,
37    pub content: String,
38}
39
40/// Workspace revision metadata for dispatched agents.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct AgentWorkspaceRevision {
43    pub vcs_kind: String,
44    pub revision_id: String,
45    pub summary: String,
46    #[serde(default)]
47    pub change_id: Option<String>,
48}
49
50/// Workspace context for dispatched agents.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct AgentWorkspaceInfo {
53    pub workspace_id: Option<String>,
54    pub revision: Option<AgentWorkspaceRevision>,
55}
56
57/// Result for the dispatch_agent tool
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct AgentResult {
60    pub content: String,
61    #[serde(default)]
62    pub session_id: Option<String>,
63    #[serde(default)]
64    pub workspace: Option<AgentWorkspaceInfo>,
65}
66
67/// Result for external/MCP tools
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct ExternalResult {
70    pub tool_name: String, // name reported by the MCP server
71    pub payload: String,   // raw, opaque blob (usually JSON or text)
72}
73
74/// Result for bash command execution
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct BashResult {
77    pub stdout: String,
78    pub stderr: String,
79    pub exit_code: i32,
80    pub command: String,
81    #[serde(default)]
82    pub timed_out: bool,
83}
84
85/// Result for todo operations
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct TodoListResult {
88    pub todos: Vec<TodoItem>,
89}
90/// Result for todo write operations
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct TodoWriteResult {
93    pub todos: Vec<TodoItem>,
94    pub operation: TodoWriteFileOperation,
95}
96
97// Newtype wrappers to avoid conflicting From impls
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct MultiEditResult(pub EditResult);
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct ReplaceResult(pub EditResult);
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct AstGrepResult(pub SearchResult);
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct GrepResult(pub SearchResult);
106
107// Trait for typed tool outputs
108pub trait ToolOutput: Serialize + Send + Sync + 'static {}
109
110// Implement ToolOutput for all result types
111impl ToolOutput for SearchResult {}
112impl ToolOutput for GrepResult {}
113impl ToolOutput for FileListResult {}
114impl ToolOutput for FileContentResult {}
115impl ToolOutput for EditResult {}
116impl ToolOutput for BashResult {}
117impl ToolOutput for GlobResult {}
118impl ToolOutput for TodoListResult {}
119impl ToolOutput for TodoWriteResult {}
120impl ToolOutput for MultiEditResult {}
121impl ToolOutput for ReplaceResult {}
122impl ToolOutput for AstGrepResult {}
123impl ToolOutput for ExternalResult {}
124impl ToolOutput for FetchResult {}
125impl ToolOutput for AgentResult {}
126impl ToolOutput for ToolResult {}
127
128// Manual From implementations to support BuiltinTool::Output conversions
129impl From<SearchResult> for ToolResult {
130    fn from(r: SearchResult) -> Self {
131        Self::Search(r)
132    }
133}
134
135impl From<GrepResult> for ToolResult {
136    fn from(r: GrepResult) -> Self {
137        Self::Search(r.0)
138    }
139}
140
141impl From<AstGrepResult> for ToolResult {
142    fn from(r: AstGrepResult) -> Self {
143        Self::Search(r.0)
144    }
145}
146
147impl From<FileListResult> for ToolResult {
148    fn from(r: FileListResult) -> Self {
149        Self::FileList(r)
150    }
151}
152
153impl From<FileContentResult> for ToolResult {
154    fn from(r: FileContentResult) -> Self {
155        Self::FileContent(r)
156    }
157}
158
159impl From<EditResult> for ToolResult {
160    fn from(r: EditResult) -> Self {
161        Self::Edit(r)
162    }
163}
164
165impl From<MultiEditResult> for ToolResult {
166    fn from(r: MultiEditResult) -> Self {
167        Self::Edit(r.0)
168    }
169}
170
171impl From<ReplaceResult> for ToolResult {
172    fn from(r: ReplaceResult) -> Self {
173        Self::Edit(r.0)
174    }
175}
176
177impl From<BashResult> for ToolResult {
178    fn from(r: BashResult) -> Self {
179        Self::Bash(r)
180    }
181}
182
183impl From<GlobResult> for ToolResult {
184    fn from(r: GlobResult) -> Self {
185        Self::Glob(r)
186    }
187}
188
189impl From<TodoListResult> for ToolResult {
190    fn from(r: TodoListResult) -> Self {
191        Self::TodoRead(r)
192    }
193}
194
195impl From<TodoWriteResult> for ToolResult {
196    fn from(r: TodoWriteResult) -> Self {
197        Self::TodoWrite(r)
198    }
199}
200
201impl From<FetchResult> for ToolResult {
202    fn from(r: FetchResult) -> Self {
203        Self::Fetch(r)
204    }
205}
206
207impl From<AgentResult> for ToolResult {
208    fn from(r: AgentResult) -> Self {
209        Self::Agent(r)
210    }
211}
212
213impl From<ExternalResult> for ToolResult {
214    fn from(r: ExternalResult) -> Self {
215        Self::External(r)
216    }
217}
218
219impl From<ToolError> for ToolResult {
220    fn from(e: ToolError) -> Self {
221        Self::Error(e)
222    }
223}
224
225impl ToolResult {
226    /// Format the result for LLM consumption
227    pub fn llm_format(&self) -> String {
228        match self {
229            ToolResult::Search(r) => {
230                if r.matches.is_empty() {
231                    "No matches found.".to_string()
232                } else {
233                    let mut output = Vec::new();
234                    let mut current_file = "";
235
236                    for match_item in &r.matches {
237                        if match_item.file_path != current_file {
238                            if !output.is_empty() {
239                                output.push(String::new());
240                            }
241                            current_file = &match_item.file_path;
242                        }
243                        output.push(format!(
244                            "{}:{}: {}",
245                            match_item.file_path, match_item.line_number, match_item.line_content
246                        ));
247                    }
248
249                    output.join("\n")
250                }
251            }
252            ToolResult::FileList(r) => {
253                if r.entries.is_empty() {
254                    format!("No entries found in {}", r.base_path)
255                } else {
256                    let mut lines = Vec::new();
257                    for entry in &r.entries {
258                        let type_indicator = if entry.is_directory { "/" } else { "" };
259                        let size_str = entry.size.map(|s| format!(" ({s})")).unwrap_or_default();
260                        lines.push(format!("{}{}{}", entry.path, type_indicator, size_str));
261                    }
262                    lines.join("\n")
263                }
264            }
265            ToolResult::FileContent(r) => r.content.clone(),
266            ToolResult::Edit(r) => {
267                if r.file_created {
268                    format!("Successfully created {}", r.file_path)
269                } else {
270                    format!(
271                        "Successfully edited {}: {} change(s) made",
272                        r.file_path, r.changes_made
273                    )
274                }
275            }
276            ToolResult::Bash(r) => {
277                // Helper to truncate long outputs
278                fn truncate_output(s: &str, max_chars: usize, max_lines: usize) -> String {
279                    let lines: Vec<&str> = s.lines().collect();
280                    let char_count = s.len();
281
282                    // Check both line and character limits
283                    if lines.len() > max_lines || char_count > max_chars {
284                        // Take first and last portions of output
285                        let head_lines = max_lines / 2;
286                        let tail_lines = max_lines - head_lines;
287
288                        let mut result = String::new();
289
290                        // Add head lines
291                        for line in lines.iter().take(head_lines) {
292                            result.push_str(line);
293                            result.push('\n');
294                        }
295
296                        // Add truncation marker
297                        let omitted_lines = lines.len().saturating_sub(max_lines);
298                        result.push_str(&format!(
299                            "\n[... {omitted_lines} lines omitted ({char_count} total chars) ...]\n\n"
300                        ));
301
302                        // Add tail lines
303                        if tail_lines > 0 && lines.len() > head_lines {
304                            for line in lines.iter().skip(lines.len().saturating_sub(tail_lines)) {
305                                result.push_str(line);
306                                result.push('\n');
307                            }
308                        }
309
310                        result
311                    } else {
312                        s.to_string()
313                    }
314                }
315
316                const MAX_STDOUT_CHARS: usize = 128 * 1024; // 128KB
317                const MAX_STDOUT_LINES: usize = 2000;
318                const MAX_STDERR_CHARS: usize = 64 * 1024; // 64KB  
319                const MAX_STDERR_LINES: usize = 500;
320
321                let stdout_truncated =
322                    truncate_output(&r.stdout, MAX_STDOUT_CHARS, MAX_STDOUT_LINES);
323                let stderr_truncated =
324                    truncate_output(&r.stderr, MAX_STDERR_CHARS, MAX_STDERR_LINES);
325
326                let mut output = stdout_truncated;
327
328                if r.timed_out {
329                    if !output.is_empty() && !output.ends_with('\n') {
330                        output.push('\n');
331                    }
332                    output.push_str("Command timed out.");
333                }
334
335                if r.exit_code != 0 {
336                    if !output.is_empty() && !output.ends_with('\n') {
337                        output.push('\n');
338                    }
339                    output.push_str(&format!("Exit code: {}", r.exit_code));
340
341                    if !stderr_truncated.is_empty() {
342                        output.push_str(&format!("\nError output:\n{stderr_truncated}"));
343                    }
344                } else if !stderr_truncated.is_empty() {
345                    if !output.is_empty() && !output.ends_with('\n') {
346                        output.push('\n');
347                    }
348                    output.push_str(&format!("Error output:\n{stderr_truncated}"));
349                }
350
351                output
352            }
353            ToolResult::Glob(r) => {
354                if r.matches.is_empty() {
355                    format!("No files matching pattern: {}", r.pattern)
356                } else {
357                    r.matches.join("\n")
358                }
359            }
360            ToolResult::TodoRead(r) => {
361                if r.todos.is_empty() {
362                    "No todos found.".to_string()
363                } else {
364                    format!(
365                        "Remember to continue to update and read from the todo list as you make progress. Here is the current list:\n{}",
366                        serde_json::to_string_pretty(&r.todos)
367                            .unwrap_or_else(|_| "Failed to format todos".to_string())
368                    )
369                }
370            }
371            ToolResult::TodoWrite(r) => {
372                format!(
373                    "Todos have been {:?} successfully. Ensure that you continue to read and update the todo list as you work on tasks.\n{}",
374                    r.operation,
375                    serde_json::to_string_pretty(&r.todos)
376                        .unwrap_or_else(|_| "Failed to format todos".to_string())
377                )
378            }
379            ToolResult::Fetch(r) => {
380                format!("Fetched content from {}:\n{}", r.url, r.content)
381            }
382            ToolResult::Agent(r) => r.session_id.as_ref().map_or_else(
383                || r.content.clone(),
384                |session_id| format!("{}\n\nsession_id: {}", r.content, session_id),
385            ),
386            ToolResult::External(r) => r.payload.clone(),
387            ToolResult::Error(e) => format!("Error: {e}"),
388        }
389    }
390
391    /// Get the variant name as a string for metadata
392    pub fn variant_name(&self) -> &'static str {
393        match self {
394            ToolResult::Search(_) => "Search",
395            ToolResult::FileList(_) => "FileList",
396            ToolResult::FileContent(_) => "FileContent",
397            ToolResult::Edit(_) => "Edit",
398            ToolResult::Bash(_) => "Bash",
399            ToolResult::Glob(_) => "Glob",
400            ToolResult::TodoRead(_) => "TodoRead",
401            ToolResult::TodoWrite(_) => "TodoWrite",
402            ToolResult::Fetch(_) => "Fetch",
403            ToolResult::Agent(_) => "Agent",
404            ToolResult::External(_) => "External",
405            ToolResult::Error(_) => "Error",
406        }
407    }
408}