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 #[serde(default)]
82 pub timed_out: bool,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct TodoListResult {
88 pub todos: Vec<TodoItem>,
89}
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct TodoWriteResult {
93 pub todos: Vec<TodoItem>,
94 pub operation: TodoWriteFileOperation,
95}
96
97#[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
107pub trait ToolOutput: Serialize + Send + Sync + 'static {}
109
110impl 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
128impl 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 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 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 if lines.len() > max_lines || char_count > max_chars {
284 let head_lines = max_lines / 2;
286 let tail_lines = max_lines - head_lines;
287
288 let mut result = String::new();
289
290 for line in lines.iter().take(head_lines) {
292 result.push_str(line);
293 result.push('\n');
294 }
295
296 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 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; const MAX_STDOUT_LINES: usize = 2000;
318 const MAX_STDERR_CHARS: usize = 64 * 1024; 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 serde_json::to_string_pretty(&r.todos)
362 .unwrap_or_else(|_| "Failed to format todos".to_string())
363 }
364 ToolResult::TodoWrite(r) => {
365 serde_json::to_string_pretty(&r.todos)
366 .unwrap_or_else(|_| "Failed to format todos".to_string())
367 }
368 ToolResult::Fetch(r) => {
369 format!("Fetched content from {}:\n{}", r.url, r.content)
370 }
371 ToolResult::Agent(r) => r.session_id.as_ref().map_or_else(
372 || r.content.clone(),
373 |session_id| format!("{}\n\nsession_id: {}", r.content, session_id),
374 ),
375 ToolResult::External(r) => r.payload.clone(),
376 ToolResult::Error(e) => format!("Error: {e}"),
377 }
378 }
379
380 pub fn variant_name(&self) -> &'static str {
382 match self {
383 ToolResult::Search(_) => "Search",
384 ToolResult::FileList(_) => "FileList",
385 ToolResult::FileContent(_) => "FileContent",
386 ToolResult::Edit(_) => "Edit",
387 ToolResult::Bash(_) => "Bash",
388 ToolResult::Glob(_) => "Glob",
389 ToolResult::TodoRead(_) => "TodoRead",
390 ToolResult::TodoWrite(_) => "TodoWrite",
391 ToolResult::Fetch(_) => "Fetch",
392 ToolResult::Agent(_) => "Agent",
393 ToolResult::External(_) => "External",
394 ToolResult::Error(_) => "Error",
395 }
396 }
397}