1use hashbrown::HashMap;
10use serde::{Deserialize, Serialize};
11use std::path::PathBuf;
12
13#[cfg(test)]
14use crate::config::constants::tools;
15use crate::utils::tokens::estimate_tokens;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ToolResult {
26 pub tool_name: String,
28
29 pub llm_content: String,
34
35 pub ui_content: String,
40
41 pub success: bool,
43
44 pub error: Option<String>,
46
47 pub metadata: ToolMetadata,
49}
50
51#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct ToolMetadata {
57 pub files: Vec<PathBuf>,
59
60 pub lines: Vec<usize>,
62
63 pub data: HashMap<String, serde_json::Value>,
70
71 pub token_counts: TokenCounts,
73}
74
75#[derive(Debug, Clone, Default, Serialize, Deserialize)]
77pub struct TokenCounts {
78 pub llm_tokens: usize,
80
81 pub ui_tokens: usize,
83
84 pub savings_tokens: usize,
86
87 pub savings_percent: f32,
89}
90
91impl ToolResult {
92 pub fn new(
94 tool_name: impl Into<String>,
95 llm_content: impl Into<String>,
96 ui_content: impl Into<String>,
97 ) -> Self {
98 let llm_str = llm_content.into();
99 let ui_str = ui_content.into();
100
101 let llm_tokens = estimate_tokens(&llm_str);
102 let ui_tokens = estimate_tokens(&ui_str);
103 let savings = ui_tokens.saturating_sub(llm_tokens);
104 let savings_pct = if ui_tokens > 0 {
105 (savings as f32 / ui_tokens as f32) * 100.0
106 } else {
107 0.0
108 };
109
110 Self {
111 tool_name: tool_name.into(),
112 llm_content: llm_str,
113 ui_content: ui_str,
114 success: true,
115 error: None,
116 metadata: ToolMetadata {
117 token_counts: TokenCounts {
118 llm_tokens,
119 ui_tokens,
120 savings_tokens: savings,
121 savings_percent: savings_pct,
122 },
123 ..Default::default()
124 },
125 }
126 }
127
128 pub fn error(tool_name: impl Into<String>, error: impl Into<String>) -> Self {
130 let error_msg = error.into();
131 Self {
132 tool_name: tool_name.into(),
133 llm_content: format!("Tool failed: {}", error_msg),
134 ui_content: format!("Error: {}", error_msg),
135 success: false,
136 error: Some(error_msg),
137 metadata: ToolMetadata::default(),
138 }
139 }
140
141 pub fn simple(tool_name: impl Into<String>, content: impl Into<String>) -> Self {
145 let content_str = content.into();
146 Self::new(tool_name, content_str.clone(), content_str)
147 }
148
149 pub fn with_metadata(mut self, metadata: ToolMetadata) -> Self {
151 let token_counts = std::mem::take(&mut self.metadata.token_counts);
153 self.metadata = metadata;
154 self.metadata.token_counts = token_counts;
155 self
156 }
157
158 pub fn with_files(mut self, files: Vec<PathBuf>) -> Self {
160 self.metadata.files = files;
161 self
162 }
163
164 pub fn with_data(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
166 self.metadata.data.insert(key.into(), value);
167 self
168 }
169
170 pub fn savings_summary(&self) -> String {
172 let counts = &self.metadata.token_counts;
173 format!(
174 "{} → {} tokens ({:.1}% saved)",
175 counts.ui_tokens, counts.llm_tokens, counts.savings_percent
176 )
177 }
178
179 pub fn has_significant_savings(&self) -> bool {
181 self.metadata.token_counts.savings_percent > 50.0
182 }
183}
184
185pub struct ToolMetadataBuilder {
187 files: Vec<PathBuf>,
188 lines: Vec<usize>,
189 data: HashMap<String, serde_json::Value>,
190}
191
192impl ToolMetadataBuilder {
193 pub fn new() -> Self {
194 Self {
195 files: Vec::new(),
196 lines: Vec::new(),
197 data: HashMap::new(),
198 }
199 }
200
201 pub fn file(mut self, path: PathBuf) -> Self {
202 self.files.push(path);
203 self
204 }
205
206 pub fn files(mut self, paths: Vec<PathBuf>) -> Self {
207 self.files.extend(paths);
208 self
209 }
210
211 pub fn line(mut self, line: usize) -> Self {
212 self.lines.push(line);
213 self
214 }
215
216 pub fn lines(mut self, lines: Vec<usize>) -> Self {
217 self.lines.extend(lines);
218 self
219 }
220
221 pub fn data(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
222 self.data.insert(key.into(), value);
223 self
224 }
225
226 pub fn build(self) -> ToolMetadata {
227 ToolMetadata {
228 files: self.files,
229 lines: self.lines,
230 data: self.data,
231 token_counts: TokenCounts::default(), }
233 }
234}
235
236impl Default for ToolMetadataBuilder {
237 fn default() -> Self {
238 Self::new()
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn test_tool_result_creation() {
248 let result = ToolResult::new(
249 tools::GREP_FILE,
250 "Found 127 matches in 15 files",
251 "Very long output with 127 full match listings...",
252 );
253
254 assert_eq!(result.tool_name, tools::GREP_FILE);
255 assert!(result.success);
256 assert!(result.error.is_none());
257 assert!(result.metadata.token_counts.llm_tokens > 0);
258 assert!(result.metadata.token_counts.ui_tokens > 0);
259 assert!(result.metadata.token_counts.savings_tokens > 0);
260 }
261
262 #[test]
263 fn test_error_result() {
264 let result = ToolResult::error(tools::GREP_FILE, "Pattern invalid");
265
266 assert_eq!(result.tool_name, tools::GREP_FILE);
267 assert!(!result.success);
268 assert_eq!(result.error, Some("Pattern invalid".to_string()));
269 assert!(result.llm_content.contains("failed"));
270 }
271
272 #[test]
273 fn test_simple_result() {
274 let result = ToolResult::simple("test_tool", "Same content");
275
276 assert_eq!(result.llm_content, result.ui_content);
277 assert_eq!(result.metadata.token_counts.savings_tokens, 0);
278 }
279
280 #[test]
281 fn test_token_estimation() {
282 let text = "Hello world";
283 let tokens = estimate_tokens(text);
284 assert_eq!(tokens, 3);
286
287 let long_text = "a".repeat(1000);
288 let long_tokens = estimate_tokens(&long_text);
289 assert_eq!(long_tokens, 250);
291 }
292
293 #[test]
294 fn test_metadata_builder() {
295 let metadata = ToolMetadataBuilder::new()
296 .file(PathBuf::from("src/main.rs"))
297 .file(PathBuf::from("src/lib.rs"))
298 .line(42)
299 .line(100)
300 .data("match_count", serde_json::json!(127))
301 .data("files_searched", serde_json::json!(50))
302 .build();
303
304 assert_eq!(metadata.files.len(), 2);
305 assert_eq!(metadata.lines.len(), 2);
306 assert_eq!(metadata.data.len(), 2);
307 assert_eq!(metadata.data["match_count"], 127);
308 }
309
310 #[test]
311 fn test_with_methods() {
312 let result = ToolResult::new("test", "llm", "ui")
313 .with_files(vec![PathBuf::from("test.rs")])
314 .with_data("key", serde_json::json!("value"));
315
316 assert_eq!(result.metadata.files.len(), 1);
317 assert_eq!(result.metadata.data["key"], "value");
318 }
319
320 #[test]
321 fn test_savings_calculation() {
322 let result = ToolResult::new(
323 "grep",
324 "Short summary", "a".repeat(1000), );
327
328 assert!(result.metadata.token_counts.savings_tokens > 200);
329 assert!(result.metadata.token_counts.savings_percent > 90.0);
330 assert!(result.has_significant_savings());
331 }
332
333 #[test]
334 fn test_savings_summary() {
335 let result = ToolResult::new("grep", "Short", "Long content here");
336
337 let summary = result.savings_summary();
338 assert!(summary.contains("→"));
339 assert!(summary.contains("tokens"));
340 assert!(summary.contains("%"));
341 }
342}