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(tool_name: impl Into<String>, llm_content: impl Into<String>, ui_content: impl Into<String>) -> Self {
94 let llm_str = llm_content.into();
95 let ui_str = ui_content.into();
96
97 let llm_tokens = estimate_tokens(&llm_str);
98 let ui_tokens = estimate_tokens(&ui_str);
99 let savings = ui_tokens.saturating_sub(llm_tokens);
100 let savings_pct = if ui_tokens > 0 {
101 (savings as f32 / ui_tokens as f32) * 100.0
102 } else {
103 0.0
104 };
105
106 Self {
107 tool_name: tool_name.into(),
108 llm_content: llm_str,
109 ui_content: ui_str,
110 success: true,
111 error: None,
112 metadata: ToolMetadata {
113 token_counts: TokenCounts {
114 llm_tokens,
115 ui_tokens,
116 savings_tokens: savings,
117 savings_percent: savings_pct,
118 },
119 ..Default::default()
120 },
121 }
122 }
123
124 pub fn error(tool_name: impl Into<String>, error: impl Into<String>) -> Self {
126 let error_msg = error.into();
127 Self {
128 tool_name: tool_name.into(),
129 llm_content: format!("Tool failed: {error_msg}"),
130 ui_content: format!("Error: {error_msg}"),
131 success: false,
132 error: Some(error_msg),
133 metadata: ToolMetadata::default(),
134 }
135 }
136
137 pub fn simple(tool_name: impl Into<String>, content: impl Into<String>) -> Self {
141 let content_str = content.into();
142 Self::new(tool_name, content_str.clone(), content_str)
143 }
144
145 pub fn with_metadata(mut self, metadata: ToolMetadata) -> Self {
147 let token_counts = std::mem::take(&mut self.metadata.token_counts);
149 self.metadata = metadata;
150 self.metadata.token_counts = token_counts;
151 self
152 }
153
154 pub fn with_files(mut self, files: Vec<PathBuf>) -> Self {
156 self.metadata.files = files;
157 self
158 }
159
160 pub fn with_data(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
162 self.metadata.data.insert(key.into(), value);
163 self
164 }
165
166 pub fn savings_summary(&self) -> String {
168 let counts = &self.metadata.token_counts;
169 format!("{} → {} tokens ({:.1}% saved)", counts.ui_tokens, counts.llm_tokens, counts.savings_percent)
170 }
171
172 pub fn has_significant_savings(&self) -> bool {
174 self.metadata.token_counts.savings_percent > 50.0
175 }
176}
177
178pub struct ToolMetadataBuilder {
180 files: Vec<PathBuf>,
181 lines: Vec<usize>,
182 data: HashMap<String, serde_json::Value>,
183}
184
185impl ToolMetadataBuilder {
186 pub fn new() -> Self {
187 Self {
188 files: Vec::new(),
189 lines: Vec::new(),
190 data: HashMap::new(),
191 }
192 }
193
194 pub fn file(mut self, path: PathBuf) -> Self {
195 self.files.push(path);
196 self
197 }
198
199 pub fn files(mut self, paths: Vec<PathBuf>) -> Self {
200 self.files.extend(paths);
201 self
202 }
203
204 pub fn line(mut self, line: usize) -> Self {
205 self.lines.push(line);
206 self
207 }
208
209 pub fn lines(mut self, lines: Vec<usize>) -> Self {
210 self.lines.extend(lines);
211 self
212 }
213
214 pub fn data(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
215 self.data.insert(key.into(), value);
216 self
217 }
218
219 pub fn build(self) -> ToolMetadata {
220 ToolMetadata {
221 files: self.files,
222 lines: self.lines,
223 data: self.data,
224 token_counts: TokenCounts::default(), }
226 }
227}
228
229impl Default for ToolMetadataBuilder {
230 fn default() -> Self {
231 Self::new()
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_tool_result_creation() {
241 let result = ToolResult::new(
242 tools::GREP_FILE,
243 "Found 127 matches in 15 files",
244 "Very long output with 127 full match listings...",
245 );
246
247 assert_eq!(result.tool_name, tools::GREP_FILE);
248 assert!(result.success);
249 assert!(result.error.is_none());
250 assert!(result.metadata.token_counts.llm_tokens > 0);
251 assert!(result.metadata.token_counts.ui_tokens > 0);
252 assert!(result.metadata.token_counts.savings_tokens > 0);
253 }
254
255 #[test]
256 fn test_error_result() {
257 let result = ToolResult::error(tools::GREP_FILE, "Pattern invalid");
258
259 assert_eq!(result.tool_name, tools::GREP_FILE);
260 assert!(!result.success);
261 assert_eq!(result.error, Some("Pattern invalid".to_string()));
262 assert!(result.llm_content.contains("failed"));
263 }
264
265 #[test]
266 fn test_simple_result() {
267 let result = ToolResult::simple("test_tool", "Same content");
268
269 assert_eq!(result.llm_content, result.ui_content);
270 assert_eq!(result.metadata.token_counts.savings_tokens, 0);
271 }
272
273 #[test]
274 fn test_token_estimation() {
275 let text = "Hello world";
276 let tokens = estimate_tokens(text);
277 assert_eq!(tokens, 2);
279
280 let long_text = "a".repeat(1000);
281 let long_tokens = estimate_tokens(&long_text);
282 assert_eq!(long_tokens, 125);
284 }
285
286 #[test]
287 fn test_metadata_builder() {
288 let metadata = ToolMetadataBuilder::new()
289 .file(PathBuf::from("src/main.rs"))
290 .file(PathBuf::from("src/lib.rs"))
291 .line(42)
292 .line(100)
293 .data("match_count", serde_json::json!(127))
294 .data("files_searched", serde_json::json!(50))
295 .build();
296
297 assert_eq!(metadata.files.len(), 2);
298 assert_eq!(metadata.lines.len(), 2);
299 assert_eq!(metadata.data.len(), 2);
300 assert_eq!(metadata.data["match_count"], 127);
301 }
302
303 #[test]
304 fn test_with_methods() {
305 let result = ToolResult::new("test", "llm", "ui")
306 .with_files(vec![PathBuf::from("test.rs")])
307 .with_data("key", serde_json::json!("value"));
308
309 assert_eq!(result.metadata.files.len(), 1);
310 assert_eq!(result.metadata.data["key"], "value");
311 }
312
313 #[test]
314 fn test_savings_calculation() {
315 let result = ToolResult::new(
316 "grep",
317 "Short summary", "a".repeat(1000), );
320
321 assert!(result.metadata.token_counts.savings_tokens > 100);
322 assert!(result.metadata.token_counts.savings_percent > 90.0);
323 assert!(result.has_significant_savings());
324 }
325
326 #[test]
327 fn test_savings_summary() {
328 let result = ToolResult::new("grep", "Short", "Long content here");
329
330 let summary = result.savings_summary();
331 assert!(summary.contains("→"));
332 assert!(summary.contains("tokens"));
333 assert!(summary.contains("%"));
334 }
335}