use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[cfg(test)]
use crate::config::constants::tools;
use crate::utils::tokens::estimate_tokens;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub tool_name: String,
pub llm_content: String,
pub ui_content: String,
pub success: bool,
pub error: Option<String>,
pub metadata: ToolMetadata,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ToolMetadata {
pub files: Vec<PathBuf>,
pub lines: Vec<usize>,
pub data: HashMap<String, serde_json::Value>,
pub token_counts: TokenCounts,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TokenCounts {
pub llm_tokens: usize,
pub ui_tokens: usize,
pub savings_tokens: usize,
pub savings_percent: f32,
}
impl ToolResult {
pub fn new(
tool_name: impl Into<String>,
llm_content: impl Into<String>,
ui_content: impl Into<String>,
) -> Self {
let llm_str = llm_content.into();
let ui_str = ui_content.into();
let llm_tokens = estimate_tokens(&llm_str);
let ui_tokens = estimate_tokens(&ui_str);
let savings = ui_tokens.saturating_sub(llm_tokens);
let savings_pct = if ui_tokens > 0 {
(savings as f32 / ui_tokens as f32) * 100.0
} else {
0.0
};
Self {
tool_name: tool_name.into(),
llm_content: llm_str,
ui_content: ui_str,
success: true,
error: None,
metadata: ToolMetadata {
token_counts: TokenCounts {
llm_tokens,
ui_tokens,
savings_tokens: savings,
savings_percent: savings_pct,
},
..Default::default()
},
}
}
pub fn error(tool_name: impl Into<String>, error: impl Into<String>) -> Self {
let error_msg = error.into();
Self {
tool_name: tool_name.into(),
llm_content: format!("Tool failed: {}", error_msg),
ui_content: format!("Error: {}", error_msg),
success: false,
error: Some(error_msg),
metadata: ToolMetadata::default(),
}
}
pub fn simple(tool_name: impl Into<String>, content: impl Into<String>) -> Self {
let content_str = content.into();
Self::new(tool_name, content_str.clone(), content_str)
}
pub fn with_metadata(mut self, metadata: ToolMetadata) -> Self {
let token_counts = self.metadata.token_counts.clone();
self.metadata = metadata;
self.metadata.token_counts = token_counts;
self
}
pub fn with_files(mut self, files: Vec<PathBuf>) -> Self {
self.metadata.files = files;
self
}
pub fn with_data(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.metadata.data.insert(key.into(), value);
self
}
pub fn savings_summary(&self) -> String {
let counts = &self.metadata.token_counts;
format!(
"{} → {} tokens ({:.1}% saved)",
counts.ui_tokens, counts.llm_tokens, counts.savings_percent
)
}
pub fn has_significant_savings(&self) -> bool {
self.metadata.token_counts.savings_percent > 50.0
}
}
pub struct ToolMetadataBuilder {
files: Vec<PathBuf>,
lines: Vec<usize>,
data: HashMap<String, serde_json::Value>,
}
impl ToolMetadataBuilder {
pub fn new() -> Self {
Self {
files: Vec::new(),
lines: Vec::new(),
data: HashMap::new(),
}
}
pub fn file(mut self, path: PathBuf) -> Self {
self.files.push(path);
self
}
pub fn files(mut self, paths: Vec<PathBuf>) -> Self {
self.files.extend(paths);
self
}
pub fn line(mut self, line: usize) -> Self {
self.lines.push(line);
self
}
pub fn lines(mut self, lines: Vec<usize>) -> Self {
self.lines.extend(lines);
self
}
pub fn data(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.data.insert(key.into(), value);
self
}
pub fn build(self) -> ToolMetadata {
ToolMetadata {
files: self.files,
lines: self.lines,
data: self.data,
token_counts: TokenCounts::default(), }
}
}
impl Default for ToolMetadataBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_result_creation() {
let result = ToolResult::new(
tools::GREP_FILE,
"Found 127 matches in 15 files",
"Very long output with 127 full match listings...",
);
assert_eq!(result.tool_name, tools::GREP_FILE);
assert!(result.success);
assert!(result.error.is_none());
assert!(result.metadata.token_counts.llm_tokens > 0);
assert!(result.metadata.token_counts.ui_tokens > 0);
assert!(result.metadata.token_counts.savings_tokens > 0);
}
#[test]
fn test_error_result() {
let result = ToolResult::error(tools::GREP_FILE, "Pattern invalid");
assert_eq!(result.tool_name, tools::GREP_FILE);
assert!(!result.success);
assert_eq!(result.error, Some("Pattern invalid".to_string()));
assert!(result.llm_content.contains("failed"));
}
#[test]
fn test_simple_result() {
let result = ToolResult::simple("test_tool", "Same content");
assert_eq!(result.llm_content, result.ui_content);
assert_eq!(result.metadata.token_counts.savings_tokens, 0);
}
#[test]
fn test_token_estimation() {
let text = "Hello world";
let tokens = estimate_tokens(text);
assert_eq!(tokens, 3);
let long_text = "a".repeat(1000);
let long_tokens = estimate_tokens(&long_text);
assert_eq!(long_tokens, 250);
}
#[test]
fn test_metadata_builder() {
let metadata = ToolMetadataBuilder::new()
.file(PathBuf::from("src/main.rs"))
.file(PathBuf::from("src/lib.rs"))
.line(42)
.line(100)
.data("match_count", serde_json::json!(127))
.data("files_searched", serde_json::json!(50))
.build();
assert_eq!(metadata.files.len(), 2);
assert_eq!(metadata.lines.len(), 2);
assert_eq!(metadata.data.len(), 2);
assert_eq!(metadata.data["match_count"], 127);
}
#[test]
fn test_with_methods() {
let result = ToolResult::new("test", "llm", "ui")
.with_files(vec![PathBuf::from("test.rs")])
.with_data("key", serde_json::json!("value"));
assert_eq!(result.metadata.files.len(), 1);
assert_eq!(result.metadata.data["key"], "value");
}
#[test]
fn test_savings_calculation() {
let result = ToolResult::new(
"grep",
"Short summary", "a".repeat(1000), );
assert!(result.metadata.token_counts.savings_tokens > 200);
assert!(result.metadata.token_counts.savings_percent > 90.0);
assert!(result.has_significant_savings());
}
#[test]
fn test_savings_summary() {
let result = ToolResult::new("grep", "Short", "Long content here");
let summary = result.savings_summary();
assert!(summary.contains("→"));
assert!(summary.contains("tokens"));
assert!(summary.contains("%"));
}
}