diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index 0b0b68f..0f24c51 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -21,6 +21,7 @@ pub mod ui_writer; pub mod streaming; pub mod utils; pub mod webdriver_session; +pub mod stats; pub use task_result::TaskResult; pub use retry::{RetryConfig, RetryResult, execute_with_retry, retry_operation}; @@ -1129,162 +1130,18 @@ impl Agent { /// Get detailed context statistics pub fn get_stats(&self) -> String { - let mut stats = String::new(); - use std::time::Duration; - - stats.push_str("\nšŸ“Š Context Window Statistics\n"); - stats.push_str(&"=".repeat(60)); - stats.push_str("\n\n"); - - // Context window usage - stats.push_str("šŸ—‚ļø Context Window:\n"); - stats.push_str(&format!( - " • Used Tokens: {:>10} / {}\n", - self.context_window.used_tokens, self.context_window.total_tokens - )); - stats.push_str(&format!( - " • Usage Percentage: {:>10.1}%\n", - self.context_window.percentage_used() - )); - stats.push_str(&format!( - " • Remaining Tokens: {:>10}\n", - self.context_window.remaining_tokens() - )); - stats.push_str(&format!( - " • Cumulative Tokens: {:>10}\n", - self.context_window.cumulative_tokens - )); - stats.push_str(&format!( - " • Last Thinning: {:>10}%\n", - self.context_window.last_thinning_percentage - )); - stats.push('\n'); - - // Context optimization metrics - stats.push_str("šŸ—œļø Context Optimization:\n"); - stats.push_str(&format!( - " • Thinning Events: {:>10}\n", - self.thinning_events.len() - )); - if !self.thinning_events.is_empty() { - let total_thinned: usize = self.thinning_events.iter().sum(); - let avg_thinned = total_thinned / self.thinning_events.len(); - stats.push_str(&format!(" • Total Chars Saved: {:>10}\n", total_thinned)); - stats.push_str(&format!(" • Avg Chars/Event: {:>10}\n", avg_thinned)); - } - - stats.push_str(&format!( - " • Compactions: {:>10}\n", - self.compaction_events.len() - )); - if !self.compaction_events.is_empty() { - let total_compacted: usize = self.compaction_events.iter().sum(); - let avg_compacted = total_compacted / self.compaction_events.len(); - stats.push_str(&format!( - " • Total Chars Saved: {:>10}\n", - total_compacted - )); - stats.push_str(&format!(" • Avg Chars/Event: {:>10}\n", avg_compacted)); - } - stats.push('\n'); - - // Performance metrics - stats.push_str("⚔ Performance:\n"); - if !self.first_token_times.is_empty() { - let avg_ttft = self.first_token_times.iter().sum::() - / self.first_token_times.len() as u32; - let mut sorted_times = self.first_token_times.clone(); - sorted_times.sort(); - let median_ttft = sorted_times[sorted_times.len() / 2]; - stats.push_str(&format!( - " • Avg Time to First Token: {:>6.3}s\n", - avg_ttft.as_secs_f64() - )); - stats.push_str(&format!( - " • Median Time to First Token: {:>6.3}s\n", - median_ttft.as_secs_f64() - )); - } - stats.push('\n'); - - // Conversation history - stats.push_str("šŸ’¬ Conversation History:\n"); - stats.push_str(&format!( - " • Total Messages: {:>10}\n", - self.context_window.conversation_history.len() - )); - - // Count messages by role - let mut system_count = 0; - let mut user_count = 0; - let mut assistant_count = 0; - - for msg in &self.context_window.conversation_history { - match msg.role { - MessageRole::System => system_count += 1, - MessageRole::User => user_count += 1, - MessageRole::Assistant => assistant_count += 1, - } - } - - stats.push_str(&format!(" • System Messages: {:>10}\n", system_count)); - stats.push_str(&format!(" • User Messages: {:>10}\n", user_count)); - stats.push_str(&format!( - " • Assistant Messages:{:>10}\n", - assistant_count - )); - stats.push('\n'); - - // Tool call metrics - stats.push_str("šŸ”§ Tool Call Metrics:\n"); - stats.push_str(&format!( - " • Total Tool Calls: {:>10}\n", - self.tool_call_metrics.len() - )); - - let successful_calls = self - .tool_call_metrics - .iter() - .filter(|(_, _, success)| *success) - .count(); - let failed_calls = self.tool_call_metrics.len() - successful_calls; - - stats.push_str(&format!( - " • Successful: {:>10}\n", - successful_calls - )); - stats.push_str(&format!(" • Failed: {:>10}\n", failed_calls)); - - if !self.tool_call_metrics.is_empty() { - let total_duration: Duration = self - .tool_call_metrics - .iter() - .map(|(_, duration, _)| *duration) - .sum(); - let avg_duration = total_duration / self.tool_call_metrics.len() as u32; - - stats.push_str(&format!( - " • Total Duration: {:>10.2}s\n", - total_duration.as_secs_f64() - )); - stats.push_str(&format!( - " • Average Duration: {:>10.2}s\n", - avg_duration.as_secs_f64() - )); - } - stats.push('\n'); - - // Provider info - stats.push_str("šŸ”Œ Provider:\n"); - if let Ok((provider, model)) = self.get_provider_info() { - stats.push_str(&format!(" • Provider: {}\n", provider)); - stats.push_str(&format!(" • Model: {}\n", model)); - } - - stats.push_str(&"=".repeat(60)); - stats.push('\n'); - - stats + use crate::stats::AgentStatsSnapshot; + + let snapshot = AgentStatsSnapshot { + context_window: &self.context_window, + thinning_events: &self.thinning_events, + compaction_events: &self.compaction_events, + first_token_times: &self.first_token_times, + tool_call_metrics: &self.tool_call_metrics, + provider_info: self.get_provider_info().ok(), + }; + + snapshot.format() } pub fn get_tool_call_metrics(&self) -> &Vec<(String, Duration, bool)> { diff --git a/crates/g3-core/src/stats.rs b/crates/g3-core/src/stats.rs new file mode 100644 index 0000000..c78d384 --- /dev/null +++ b/crates/g3-core/src/stats.rs @@ -0,0 +1,263 @@ +//! Agent statistics formatting module. +//! +//! This module provides functionality for formatting detailed statistics +//! about the agent's context window, performance, and tool usage. + +use g3_providers::MessageRole; +use std::time::Duration; + +use crate::context_window::ContextWindow; + +/// Data required to format agent statistics. +/// This struct captures a snapshot of agent state for formatting. +pub struct AgentStatsSnapshot<'a> { + pub context_window: &'a ContextWindow, + pub thinning_events: &'a [usize], + pub compaction_events: &'a [usize], + pub first_token_times: &'a [Duration], + pub tool_call_metrics: &'a [(String, Duration, bool)], + pub provider_info: Option<(String, String)>, +} + +impl<'a> AgentStatsSnapshot<'a> { + /// Format detailed context statistics as a string. + pub fn format(&self) -> String { + let mut stats = String::new(); + + stats.push_str("\nšŸ“Š Context Window Statistics\n"); + stats.push_str(&"=".repeat(60)); + stats.push_str("\n\n"); + + self.format_context_window(&mut stats); + self.format_optimization_metrics(&mut stats); + self.format_performance_metrics(&mut stats); + self.format_conversation_history(&mut stats); + self.format_tool_call_metrics(&mut stats); + self.format_provider_info(&mut stats); + + stats.push_str(&"=".repeat(60)); + stats.push('\n'); + + stats + } + + fn format_context_window(&self, stats: &mut String) { + stats.push_str("šŸ—‚ļø Context Window:\n"); + stats.push_str(&format!( + " • Used Tokens: {:>10} / {}\n", + self.context_window.used_tokens, self.context_window.total_tokens + )); + stats.push_str(&format!( + " • Usage Percentage: {:>10.1}%\n", + self.context_window.percentage_used() + )); + stats.push_str(&format!( + " • Remaining Tokens: {:>10}\n", + self.context_window.remaining_tokens() + )); + stats.push_str(&format!( + " • Cumulative Tokens: {:>10}\n", + self.context_window.cumulative_tokens + )); + stats.push_str(&format!( + " • Last Thinning: {:>10}%\n", + self.context_window.last_thinning_percentage + )); + stats.push('\n'); + } + + fn format_optimization_metrics(&self, stats: &mut String) { + stats.push_str("šŸ—œļø Context Optimization:\n"); + stats.push_str(&format!( + " • Thinning Events: {:>10}\n", + self.thinning_events.len() + )); + if !self.thinning_events.is_empty() { + let total_thinned: usize = self.thinning_events.iter().sum(); + let avg_thinned = total_thinned / self.thinning_events.len(); + stats.push_str(&format!(" • Total Chars Saved: {:>10}\n", total_thinned)); + stats.push_str(&format!(" • Avg Chars/Event: {:>10}\n", avg_thinned)); + } + + stats.push_str(&format!( + " • Compactions: {:>10}\n", + self.compaction_events.len() + )); + if !self.compaction_events.is_empty() { + let total_compacted: usize = self.compaction_events.iter().sum(); + let avg_compacted = total_compacted / self.compaction_events.len(); + stats.push_str(&format!( + " • Total Chars Saved: {:>10}\n", + total_compacted + )); + stats.push_str(&format!(" • Avg Chars/Event: {:>10}\n", avg_compacted)); + } + stats.push('\n'); + } + + fn format_performance_metrics(&self, stats: &mut String) { + stats.push_str("⚔ Performance:\n"); + if !self.first_token_times.is_empty() { + let avg_ttft = self.first_token_times.iter().sum::() + / self.first_token_times.len() as u32; + let mut sorted_times = self.first_token_times.to_vec(); + sorted_times.sort(); + let median_ttft = sorted_times[sorted_times.len() / 2]; + stats.push_str(&format!( + " • Avg Time to First Token: {:>6.3}s\n", + avg_ttft.as_secs_f64() + )); + stats.push_str(&format!( + " • Median Time to First Token: {:>6.3}s\n", + median_ttft.as_secs_f64() + )); + } + stats.push('\n'); + } + + fn format_conversation_history(&self, stats: &mut String) { + stats.push_str("šŸ’¬ Conversation History:\n"); + stats.push_str(&format!( + " • Total Messages: {:>10}\n", + self.context_window.conversation_history.len() + )); + + // Count messages by role + let mut system_count = 0; + let mut user_count = 0; + let mut assistant_count = 0; + + for msg in &self.context_window.conversation_history { + match msg.role { + MessageRole::System => system_count += 1, + MessageRole::User => user_count += 1, + MessageRole::Assistant => assistant_count += 1, + } + } + + stats.push_str(&format!(" • System Messages: {:>10}\n", system_count)); + stats.push_str(&format!(" • User Messages: {:>10}\n", user_count)); + stats.push_str(&format!( + " • Assistant Messages:{:>10}\n", + assistant_count + )); + stats.push('\n'); + } + + fn format_tool_call_metrics(&self, stats: &mut String) { + stats.push_str("šŸ”§ Tool Call Metrics:\n"); + stats.push_str(&format!( + " • Total Tool Calls: {:>10}\n", + self.tool_call_metrics.len() + )); + + let successful_calls = self + .tool_call_metrics + .iter() + .filter(|(_, _, success)| *success) + .count(); + let failed_calls = self.tool_call_metrics.len() - successful_calls; + + stats.push_str(&format!( + " • Successful: {:>10}\n", + successful_calls + )); + stats.push_str(&format!(" • Failed: {:>10}\n", failed_calls)); + + if !self.tool_call_metrics.is_empty() { + let total_duration: Duration = self + .tool_call_metrics + .iter() + .map(|(_, duration, _)| *duration) + .sum(); + let avg_duration = total_duration / self.tool_call_metrics.len() as u32; + + stats.push_str(&format!( + " • Total Duration: {:>10.2}s\n", + total_duration.as_secs_f64() + )); + stats.push_str(&format!( + " • Average Duration: {:>10.2}s\n", + avg_duration.as_secs_f64() + )); + } + stats.push('\n'); + } + + fn format_provider_info(&self, stats: &mut String) { + stats.push_str("šŸ”Œ Provider:\n"); + if let Some((provider, model)) = &self.provider_info { + stats.push_str(&format!(" • Provider: {}\n", provider)); + stats.push_str(&format!(" • Model: {}\n", model)); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::context_window::ContextWindow; + + #[test] + fn test_format_stats_empty() { + let context_window = ContextWindow::new(100000); + let snapshot = AgentStatsSnapshot { + context_window: &context_window, + thinning_events: &[], + compaction_events: &[], + first_token_times: &[], + tool_call_metrics: &[], + provider_info: None, + }; + + let stats = snapshot.format(); + assert!(stats.contains("Context Window Statistics")); + assert!(stats.contains("Used Tokens")); + assert!(stats.contains("Thinning Events")); + assert!(stats.contains("Tool Call Metrics")); + } + + #[test] + fn test_format_stats_with_data() { + let context_window = ContextWindow::new(100000); + let thinning_events = vec![1000, 2000, 1500]; + let compaction_events = vec![5000]; + let first_token_times = vec![ + Duration::from_millis(100), + Duration::from_millis(150), + Duration::from_millis(120), + ]; + let tool_call_metrics = vec![ + ("shell".to_string(), Duration::from_millis(500), true), + ("read_file".to_string(), Duration::from_millis(100), true), + ("write_file".to_string(), Duration::from_millis(200), false), + ]; + + let snapshot = AgentStatsSnapshot { + context_window: &context_window, + thinning_events: &thinning_events, + compaction_events: &compaction_events, + first_token_times: &first_token_times, + tool_call_metrics: &tool_call_metrics, + provider_info: Some(("anthropic".to_string(), "claude-3".to_string())), + }; + + let stats = snapshot.format(); + + // Check thinning stats + assert!(stats.contains("Thinning Events: 3")); + assert!(stats.contains("Total Chars Saved: 4500")); // 1000+2000+1500 + + // Check compaction stats + assert!(stats.contains("Compactions: 1")); + + // Check tool call stats + assert!(stats.contains("Total Tool Calls: 3")); + assert!(stats.contains("Successful: 2")); + assert!(stats.contains("Failed: 1")); + + // Check provider info + assert!(stats.contains("Provider: anthropic")); + assert!(stats.contains("Model: claude-3")); + } +}