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}
82
83/// Result for todo operations
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct TodoListResult {
86    pub todos: Vec<TodoItem>,
87}
88/// Result for todo write operations
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct TodoWriteResult {
91    pub todos: Vec<TodoItem>,
92    pub operation: TodoWriteFileOperation,
93}
94
95// Newtype wrappers to avoid conflicting From impls
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct MultiEditResult(pub EditResult);
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct ReplaceResult(pub EditResult);
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct AstGrepResult(pub SearchResult);
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct GrepResult(pub SearchResult);
104
105// Trait for typed tool outputs
106pub trait ToolOutput: Serialize + Send + Sync + 'static {}
107
108// Implement ToolOutput for all result types
109impl ToolOutput for SearchResult {}
110impl ToolOutput for GrepResult {}
111impl ToolOutput for FileListResult {}
112impl ToolOutput for FileContentResult {}
113impl ToolOutput for EditResult {}
114impl ToolOutput for BashResult {}
115impl ToolOutput for GlobResult {}
116impl ToolOutput for TodoListResult {}
117impl ToolOutput for TodoWriteResult {}
118impl ToolOutput for MultiEditResult {}
119impl ToolOutput for ReplaceResult {}
120impl ToolOutput for AstGrepResult {}
121impl ToolOutput for ExternalResult {}
122impl ToolOutput for FetchResult {}
123impl ToolOutput for AgentResult {}
124impl ToolOutput for ToolResult {}
125
126// Manual From implementations to support StaticTool::Output conversions
127impl From<SearchResult> for ToolResult {
128    fn from(r: SearchResult) -> Self {
129        Self::Search(r)
130    }
131}
132
133impl From<GrepResult> for ToolResult {
134    fn from(r: GrepResult) -> Self {
135        Self::Search(r.0)
136    }
137}
138
139impl From<AstGrepResult> for ToolResult {
140    fn from(r: AstGrepResult) -> Self {
141        Self::Search(r.0)
142    }
143}
144
145impl From<FileListResult> for ToolResult {
146    fn from(r: FileListResult) -> Self {
147        Self::FileList(r)
148    }
149}
150
151impl From<FileContentResult> for ToolResult {
152    fn from(r: FileContentResult) -> Self {
153        Self::FileContent(r)
154    }
155}
156
157impl From<EditResult> for ToolResult {
158    fn from(r: EditResult) -> Self {
159        Self::Edit(r)
160    }
161}
162
163impl From<MultiEditResult> for ToolResult {
164    fn from(r: MultiEditResult) -> Self {
165        Self::Edit(r.0)
166    }
167}
168
169impl From<ReplaceResult> for ToolResult {
170    fn from(r: ReplaceResult) -> Self {
171        Self::Edit(r.0)
172    }
173}
174
175impl From<BashResult> for ToolResult {
176    fn from(r: BashResult) -> Self {
177        Self::Bash(r)
178    }
179}
180
181impl From<GlobResult> for ToolResult {
182    fn from(r: GlobResult) -> Self {
183        Self::Glob(r)
184    }
185}
186
187impl From<TodoListResult> for ToolResult {
188    fn from(r: TodoListResult) -> Self {
189        Self::TodoRead(r)
190    }
191}
192
193impl From<TodoWriteResult> for ToolResult {
194    fn from(r: TodoWriteResult) -> Self {
195        Self::TodoWrite(r)
196    }
197}
198
199impl From<FetchResult> for ToolResult {
200    fn from(r: FetchResult) -> Self {
201        Self::Fetch(r)
202    }
203}
204
205impl From<AgentResult> for ToolResult {
206    fn from(r: AgentResult) -> Self {
207        Self::Agent(r)
208    }
209}
210
211impl From<ExternalResult> for ToolResult {
212    fn from(r: ExternalResult) -> Self {
213        Self::External(r)
214    }
215}
216
217impl From<ToolError> for ToolResult {
218    fn from(e: ToolError) -> Self {
219        Self::Error(e)
220    }
221}
222
223impl ToolResult {
224    /// Format the result for LLM consumption
225    pub fn llm_format(&self) -> String {
226        match self {
227            ToolResult::Search(r) => {
228                if r.matches.is_empty() {
229                    "No matches found.".to_string()
230                } else {
231                    let mut output = Vec::new();
232                    let mut current_file = "";
233
234                    for match_item in &r.matches {
235                        if match_item.file_path != current_file {
236                            if !output.is_empty() {
237                                output.push(String::new());
238                            }
239                            current_file = &match_item.file_path;
240                        }
241                        output.push(format!(
242                            "{}:{}: {}",
243                            match_item.file_path, match_item.line_number, match_item.line_content
244                        ));
245                    }
246
247                    output.join("\n")
248                }
249            }
250            ToolResult::FileList(r) => {
251                if r.entries.is_empty() {
252                    format!("No entries found in {}", r.base_path)
253                } else {
254                    let mut lines = Vec::new();
255                    for entry in &r.entries {
256                        let type_indicator = if entry.is_directory { "/" } else { "" };
257                        let size_str = entry.size.map(|s| format!(" ({s})")).unwrap_or_default();
258                        lines.push(format!("{}{}{}", entry.path, type_indicator, size_str));
259                    }
260                    lines.join("\n")
261                }
262            }
263            ToolResult::FileContent(r) => r.content.clone(),
264            ToolResult::Edit(r) => {
265                if r.file_created {
266                    format!("Successfully created {}", r.file_path)
267                } else {
268                    format!(
269                        "Successfully edited {}: {} change(s) made",
270                        r.file_path, r.changes_made
271                    )
272                }
273            }
274            ToolResult::Bash(r) => {
275                // Helper to truncate long outputs
276                fn truncate_output(s: &str, max_chars: usize, max_lines: usize) -> String {
277                    let lines: Vec<&str> = s.lines().collect();
278                    let char_count = s.len();
279
280                    // Check both line and character limits
281                    if lines.len() > max_lines || char_count > max_chars {
282                        // Take first and last portions of output
283                        let head_lines = max_lines / 2;
284                        let tail_lines = max_lines - head_lines;
285
286                        let mut result = String::new();
287
288                        // Add head lines
289                        for line in lines.iter().take(head_lines) {
290                            result.push_str(line);
291                            result.push('\n');
292                        }
293
294                        // Add truncation marker
295                        let omitted_lines = lines.len().saturating_sub(max_lines);
296                        result.push_str(&format!(
297                            "\n[... {omitted_lines} lines omitted ({char_count} total chars) ...]\n\n"
298                        ));
299
300                        // Add tail lines
301                        if tail_lines > 0 && lines.len() > head_lines {
302                            for line in lines.iter().skip(lines.len().saturating_sub(tail_lines)) {
303                                result.push_str(line);
304                                result.push('\n');
305                            }
306                        }
307
308                        result
309                    } else {
310                        s.to_string()
311                    }
312                }
313
314                const MAX_STDOUT_CHARS: usize = 128 * 1024; // 128KB
315                const MAX_STDOUT_LINES: usize = 2000;
316                const MAX_STDERR_CHARS: usize = 64 * 1024; // 64KB  
317                const MAX_STDERR_LINES: usize = 500;
318
319                let stdout_truncated =
320                    truncate_output(&r.stdout, MAX_STDOUT_CHARS, MAX_STDOUT_LINES);
321                let stderr_truncated =
322                    truncate_output(&r.stderr, MAX_STDERR_CHARS, MAX_STDERR_LINES);
323
324                let mut output = stdout_truncated;
325
326                if r.exit_code != 0 {
327                    if !output.is_empty() && !output.ends_with('\n') {
328                        output.push('\n');
329                    }
330                    output.push_str(&format!("Exit code: {}", r.exit_code));
331
332                    if !stderr_truncated.is_empty() {
333                        output.push_str(&format!("\nError output:\n{stderr_truncated}"));
334                    }
335                } else if !stderr_truncated.is_empty() {
336                    if !output.is_empty() && !output.ends_with('\n') {
337                        output.push('\n');
338                    }
339                    output.push_str(&format!("Error output:\n{stderr_truncated}"));
340                }
341
342                output
343            }
344            ToolResult::Glob(r) => {
345                if r.matches.is_empty() {
346                    format!("No files matching pattern: {}", r.pattern)
347                } else {
348                    r.matches.join("\n")
349                }
350            }
351            ToolResult::TodoRead(r) => {
352                if r.todos.is_empty() {
353                    "No todos found.".to_string()
354                } else {
355                    format!(
356                        "Remember to continue to update and read from the todo list as you make progress. Here is the current list:\n{}",
357                        serde_json::to_string_pretty(&r.todos)
358                            .unwrap_or_else(|_| "Failed to format todos".to_string())
359                    )
360                }
361            }
362            ToolResult::TodoWrite(r) => {
363                format!(
364                    "Todos have been {:?} successfully. Ensure that you continue to read and update the todo list as you work on tasks.\n{}",
365                    r.operation,
366                    serde_json::to_string_pretty(&r.todos)
367                        .unwrap_or_else(|_| "Failed to format todos".to_string())
368                )
369            }
370            ToolResult::Fetch(r) => {
371                format!("Fetched content from {}:\n{}", r.url, r.content)
372            }
373            ToolResult::Agent(r) => r.session_id.as_ref().map_or_else(
374                || r.content.clone(),
375                |session_id| format!("{}\n\nsession_id: {}", r.content, session_id),
376            ),
377            ToolResult::External(r) => r.payload.clone(),
378            ToolResult::Error(e) => format!("Error: {e}"),
379        }
380    }
381
382    /// Get the variant name as a string for metadata
383    pub fn variant_name(&self) -> &'static str {
384        match self {
385            ToolResult::Search(_) => "Search",
386            ToolResult::FileList(_) => "FileList",
387            ToolResult::FileContent(_) => "FileContent",
388            ToolResult::Edit(_) => "Edit",
389            ToolResult::Bash(_) => "Bash",
390            ToolResult::Glob(_) => "Glob",
391            ToolResult::TodoRead(_) => "TodoRead",
392            ToolResult::TodoWrite(_) => "TodoWrite",
393            ToolResult::Fetch(_) => "Fetch",
394            ToolResult::Agent(_) => "Agent",
395            ToolResult::External(_) => "External",
396            ToolResult::Error(_) => "Error",
397        }
398    }
399}