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#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum ToolResult {
14 Search(SearchResult), FileList(FileListResult), 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 External(ExternalResult),
28
29 Error(ToolError),
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct FetchResult {
36 pub url: String,
37 pub content: String,
38}
39
40#[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#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct AgentWorkspaceInfo {
53 pub workspace_id: Option<String>,
54 pub revision: Option<AgentWorkspaceRevision>,
55}
56
57#[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#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct ExternalResult {
70 pub tool_name: String, pub payload: String, }
73
74#[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#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct TodoListResult {
86 pub todos: Vec<TodoItem>,
87}
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct TodoWriteResult {
91 pub todos: Vec<TodoItem>,
92 pub operation: TodoWriteFileOperation,
93}
94
95#[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
105pub trait ToolOutput: Serialize + Send + Sync + 'static {}
107
108impl 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
126impl 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 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 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 if lines.len() > max_lines || char_count > max_chars {
282 let head_lines = max_lines / 2;
284 let tail_lines = max_lines - head_lines;
285
286 let mut result = String::new();
287
288 for line in lines.iter().take(head_lines) {
290 result.push_str(line);
291 result.push('\n');
292 }
293
294 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 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; const MAX_STDOUT_LINES: usize = 2000;
316 const MAX_STDERR_CHARS: usize = 64 * 1024; 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 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}