Skip to main content

vtcode_core/exec/
agent_optimization.rs

1//! Agent behavior optimization and learning system
2//!
3//! Analyzes metrics from Steps 1-8 to provide guidance on:
4//! - Tool discovery optimization
5//! - Code pattern effectiveness
6//! - Skill recommendations
7//! - Version compatibility predictions
8
9use hashbrown::HashMap;
10use serde::{Deserialize, Serialize};
11use std::fmt::Write;
12use std::time::Duration;
13use tracing::debug;
14
15#[cfg(test)]
16use crate::config::constants::tools;
17
18/// Statistics about skill usage patterns
19#[derive(Debug, Clone, Default, Serialize, Deserialize)]
20pub struct SkillStatistics {
21    /// Time from creation to first reuse
22    pub creation_to_reuse_time: Option<Duration>,
23    /// Average lifetime of a skill
24    pub avg_lifecycle: Option<Duration>,
25    /// Reuse ratio by tag (0.0-1.0)
26    pub reuse_ratio_by_tag: HashMap<String, f64>,
27    /// Most effective (frequently reused) skills
28    pub most_effective_skills: Vec<String>,
29    /// Rarely used skills
30    pub rarely_used_skills: Vec<String>,
31    /// Total skills tracked
32    pub total_skills: usize,
33    /// Skills reused more than once
34    pub reused_skills: usize,
35}
36
37/// Statistics about tool usage patterns
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ToolStatistics {
40    /// Success rate of tool discovery (0.0-1.0)
41    pub discovery_success_rate: f64,
42    /// How many times each tool was used
43    pub usage_frequency: HashMap<String, u64>,
44    /// Tools often discovered together
45    pub common_tool_chains: Vec<Vec<String>>,
46    /// Typical queries used to find tools
47    pub typical_discovery_queries: Vec<String>,
48    /// Total tool discovery attempts
49    pub total_discoveries: u64,
50    /// Successful discoveries
51    pub successful_discoveries: u64,
52}
53
54impl Default for ToolStatistics {
55    fn default() -> Self {
56        Self {
57            discovery_success_rate: 0.0,
58            usage_frequency: HashMap::new(),
59            common_tool_chains: vec![],
60            typical_discovery_queries: vec![],
61            total_discoveries: 0,
62            successful_discoveries: 0,
63        }
64    }
65}
66
67/// Pattern in code that is associated with failures
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CodePattern {
70    /// Programming language (python3, javascript)
71    pub language: String,
72    /// Pattern description or regex
73    pub pattern: String,
74    /// Failure rate when this pattern is used
75    pub failure_rate: f64,
76    /// Example code that failed with this pattern
77    pub example_failures: Vec<String>,
78    /// Times this pattern appeared
79    pub occurrences: u64,
80}
81
82/// Pattern for recovering from specific errors
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct RecoveryPattern {
85    /// Type of error (e.g., "timeout", "missing_tool")
86    pub error_type: String,
87    /// What action helps recover (e.g., "retry with increased timeout")
88    pub recovery_action: String,
89    /// Success rate of this recovery (0.0-1.0)
90    pub success_rate: f64,
91    /// How many times this recovery was tried
92    pub attempts: u64,
93}
94
95/// Result of applying a recovery pattern
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct AppliedRecovery {
98    /// Type of error being recovered from
99    pub error_type: String,
100    /// The recovery action to take
101    pub recovery_action: String,
102    /// Historical success rate of this recovery
103    pub success_rate: f64,
104    /// Number of times this pattern has been attempted
105    pub attempts: u64,
106}
107
108/// Patterns of failures and recovery
109#[derive(Debug, Clone, Serialize, Deserialize, Default)]
110pub struct FailurePatterns {
111    /// Tools with high failure rates
112    pub high_failure_tools: Vec<(String, f64)>,
113    /// Code patterns associated with failures
114    pub high_failure_patterns: Vec<CodePattern>,
115    /// Most common errors and their frequencies
116    pub common_errors: Vec<(String, u64)>,
117    /// Effective recovery patterns
118    pub recovery_patterns: Vec<RecoveryPattern>,
119}
120
121/// Analyzes agent behavior from metrics history
122#[derive(Default)]
123pub struct AgentBehaviorAnalyzer {
124    skill_stats: SkillStatistics,
125    tool_stats: ToolStatistics,
126    failure_patterns: FailurePatterns,
127}
128
129impl AgentBehaviorAnalyzer {
130    /// Create a new behavior analyzer
131    pub fn new() -> Self {
132        Self::default()
133    }
134
135    /// Get skill statistics
136    pub fn skill_stats(&self) -> &SkillStatistics {
137        &self.skill_stats
138    }
139
140    /// Get tool statistics
141    pub fn tool_stats(&self) -> &ToolStatistics {
142        &self.tool_stats
143    }
144
145    /// Get failure patterns
146    pub fn failure_patterns(&self) -> &FailurePatterns {
147        &self.failure_patterns
148    }
149
150    /// Recommend tools based on usage patterns
151    pub fn recommend_tools(&self, query: &str, limit: usize) -> Vec<String> {
152        let mut recommendations = vec![];
153        let query_lower = query.to_lowercase();
154
155        // Find tools that match the query
156        for (tool, _count) in self.tool_stats.usage_frequency.iter().take(limit) {
157            if tool.to_lowercase().contains(&query_lower) {
158                recommendations.push(tool.clone());
159            }
160        }
161
162        // If no exact matches, return most used tools
163        if recommendations.is_empty() {
164            let mut by_usage: Vec<_> = self.tool_stats.usage_frequency.iter().collect();
165            by_usage.sort_by(|a, b| b.1.cmp(a.1));
166            recommendations = by_usage.iter().take(limit).map(|pair| pair.0.clone()).collect();
167        }
168
169        recommendations
170    }
171
172    /// Warn about tools with high failure rates
173    pub fn identify_risky_tools(&self, failure_threshold: f64) -> Vec<(String, f64)> {
174        self.failure_patterns
175            .high_failure_tools
176            .iter()
177            .filter(|(_tool, rate)| *rate >= failure_threshold)
178            .cloned()
179            .collect()
180    }
181
182    /// Get recovery strategy for an error type
183    pub fn get_recovery_strategy(&self, error_type: &str) -> Option<RecoveryPattern> {
184        self.failure_patterns
185            .recovery_patterns
186            .iter()
187            .find(|p| p.error_type == error_type)
188            .cloned()
189    }
190
191    /// Record tool usage
192    pub fn record_tool_usage(&mut self, tool_name: &str) {
193        *self.tool_stats.usage_frequency.entry(tool_name.into()).or_insert(0) += 1;
194    }
195
196    /// Record skill reuse
197    pub fn record_skill_reuse(&mut self, skill_name: &str) {
198        if let Some(pos) = self.skill_stats.most_effective_skills.iter().position(|s| s == skill_name) {
199            // Move to front
200            let skill = self.skill_stats.most_effective_skills.remove(pos);
201            self.skill_stats.most_effective_skills.insert(0, skill);
202        } else {
203            self.skill_stats.most_effective_skills.insert(0, skill_name.into());
204        }
205        self.skill_stats.reused_skills += 1;
206    }
207
208    /// Record tool failure
209    pub fn record_tool_failure(&mut self, tool_name: &str, error_msg: &str) {
210        // Add to common errors
211        if let Some(pos) = self.failure_patterns.common_errors.iter().position(|(msg, _)| msg == error_msg) {
212            self.failure_patterns.common_errors[pos].1 += 1;
213        } else {
214            self.failure_patterns.common_errors.push((error_msg.into(), 1));
215        }
216
217        // Update high failure tools - find count
218        let count = self
219            .failure_patterns
220            .common_errors
221            .iter()
222            .find(|(msg, _)| msg == error_msg)
223            .map(|(_, c)| *c)
224            .unwrap_or(1);
225        let failure_rate = count as f64 / (count + 1) as f64; // Approximation
226
227        if let Some(pos) = self.failure_patterns.high_failure_tools.iter().position(|t| t.0 == tool_name) {
228            self.failure_patterns.high_failure_tools[pos].1 = failure_rate;
229        } else {
230            self.failure_patterns.high_failure_tools.push((tool_name.into(), failure_rate));
231        }
232
233        debug!("Recorded failure for {}: {} (failure_rate: {})", tool_name, error_msg, failure_rate);
234    }
235
236    /// Check if a tool should trigger a warning due to high failure rate
237    pub fn should_warn(&self, tool_name: &str) -> Option<String> {
238        for (tool, rate) in &self.failure_patterns.high_failure_tools {
239            if tool == tool_name && *rate >= 0.5 {
240                return Some(format!(
241                    "Tool '{}' has a high failure rate ({:.1}%). Consider alternative approaches.",
242                    tool_name,
243                    rate * 100.0
244                ));
245            }
246        }
247        None
248    }
249
250    /// Get recovery action for a known error type
251    pub fn get_recovery_action(&self, error_type: &str) -> Option<String> {
252        self.failure_patterns
253            .recovery_patterns
254            .iter()
255            .find(|p| p.error_type == error_type)
256            .map(|p| format!("{} (success rate: {:.1}%)", p.recovery_action, p.success_rate * 100.0))
257    }
258
259    /// Export metrics for monitoring integration
260    pub fn export_metrics(&self) -> HashMap<String, serde_json::Value> {
261        let mut metrics = HashMap::new();
262
263        // Skill metrics
264        metrics.insert("total_skills".to_string(), serde_json::json!(self.skill_stats.total_skills));
265        metrics.insert("reused_skills".to_string(), serde_json::json!(self.skill_stats.reused_skills));
266
267        // Tool metrics
268        metrics.insert("discovery_success_rate".to_string(), serde_json::json!(self.tool_stats.discovery_success_rate));
269        metrics.insert("total_tools_used".to_string(), serde_json::json!(self.tool_stats.usage_frequency.len()));
270
271        // Failure metrics
272        metrics.insert(
273            "high_failure_tools_count".to_string(),
274            serde_json::json!(self.failure_patterns.high_failure_tools.len()),
275        );
276        metrics.insert("common_errors_count".to_string(), serde_json::json!(self.failure_patterns.common_errors.len()));
277        metrics.insert(
278            "recovery_patterns_count".to_string(),
279            serde_json::json!(self.failure_patterns.recovery_patterns.len()),
280        );
281
282        // Tool usage frequency
283        let mut tool_usage: Vec<_> = self.tool_stats.usage_frequency.iter().collect();
284        tool_usage.sort_by(|a, b| b.1.cmp(a.1));
285        let top_tools: HashMap<String, u64> = tool_usage.into_iter().take(10).map(|(k, v)| (k.clone(), *v)).collect();
286        metrics.insert("top_tools".to_string(), serde_json::json!(top_tools));
287
288        metrics
289    }
290
291    /// Apply recovery pattern automatically for a known error type
292    /// Returns the recovery action to take, or None if no pattern exists
293    pub fn apply_recovery_pattern(&mut self, error_type: &str) -> Option<AppliedRecovery> {
294        // Find the best recovery pattern for this error type
295        let pattern = self
296            .failure_patterns
297            .recovery_patterns
298            .iter()
299            .find(|p| p.error_type == error_type)?;
300
301        // Record that we're applying this pattern
302        let applied = AppliedRecovery {
303            error_type: error_type.to_owned(),
304            recovery_action: pattern.recovery_action.clone(),
305            success_rate: pattern.success_rate,
306            attempts: pattern.attempts,
307        };
308
309        debug!(
310            "Applying recovery pattern for '{}': {} (success rate: {:.1}%)",
311            error_type,
312            pattern.recovery_action,
313            pattern.success_rate * 100.0
314        );
315
316        Some(applied)
317    }
318
319    /// Record the outcome of an applied recovery pattern
320    pub fn record_recovery_outcome(&mut self, error_type: &str, success: bool) {
321        if let Some(pattern) = self
322            .failure_patterns
323            .recovery_patterns
324            .iter_mut()
325            .find(|p| p.error_type == error_type)
326        {
327            pattern.attempts += 1;
328            if success {
329                // Update success rate using exponential moving average
330                let alpha = 0.3; // Weight for new observation
331                pattern.success_rate = alpha + (1.0 - alpha) * pattern.success_rate;
332            } else {
333                // Decrease success rate
334                let alpha = 0.3;
335                pattern.success_rate *= 1.0 - alpha;
336            }
337
338            debug!(
339                "Updated recovery pattern '{}': success_rate={:.1}%, attempts={}",
340                error_type,
341                pattern.success_rate * 100.0,
342                pattern.attempts
343            );
344        }
345    }
346
347    /// Add or update a recovery pattern
348    pub fn add_recovery_pattern(&mut self, error_type: String, recovery_action: String, initial_success_rate: f64) {
349        // Check if pattern already exists
350        if let Some(pattern) = self
351            .failure_patterns
352            .recovery_patterns
353            .iter_mut()
354            .find(|p| p.error_type == error_type)
355        {
356            // Update existing pattern
357            pattern.recovery_action = recovery_action;
358            pattern.success_rate = initial_success_rate;
359        } else {
360            // Add new pattern
361            self.failure_patterns.recovery_patterns.push(RecoveryPattern {
362                error_type,
363                recovery_action,
364                success_rate: initial_success_rate,
365                attempts: 0,
366            });
367        }
368    }
369
370    /// Get analysis summary as string
371    pub fn summary(&self) -> String {
372        let mut output = String::new();
373        output.push_str("=== Agent Behavior Analysis ===\n\n");
374
375        output.push_str("## Skill Statistics\n");
376        let _ = writeln!(output, "Total skills: {}", self.skill_stats.total_skills);
377        let _ = writeln!(output, "Reused skills: {}", self.skill_stats.reused_skills);
378        if let Some(top_skill) = self.skill_stats.most_effective_skills.first() {
379            let _ = writeln!(output, "Top skill: {top_skill}");
380        }
381
382        output.push_str("\n## Tool Statistics\n");
383        let _ = writeln!(output, "Tool discovery success rate: {:.1}%", self.tool_stats.discovery_success_rate * 100.0);
384        let _ = writeln!(output, "Total tools used: {}", self.tool_stats.usage_frequency.len());
385
386        if !self.failure_patterns.high_failure_tools.is_empty() {
387            output.push_str("\n## High-Risk Tools\n");
388            for (tool, rate) in self.failure_patterns.high_failure_tools.iter().take(5) {
389                let _ = writeln!(output, "- {} (failure rate: {:.1}%)", tool, rate * 100.0);
390            }
391        }
392
393        output
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn test_analyzer_creation() {
403        let analyzer = AgentBehaviorAnalyzer::new();
404        assert_eq!(analyzer.skill_stats.total_skills, 0);
405        assert_eq!(analyzer.tool_stats.total_discoveries, 0);
406    }
407
408    #[test]
409    fn test_recommend_tools() {
410        let mut analyzer = AgentBehaviorAnalyzer::new();
411        analyzer.record_tool_usage("read_file");
412        analyzer.record_tool_usage("read_file");
413        analyzer.record_tool_usage("write_file");
414        analyzer.record_tool_usage(tools::LIST_FILES);
415
416        let recommendations = analyzer.recommend_tools("read", 1);
417        assert!(recommendations.contains(&"read_file".to_owned()));
418    }
419
420    #[test]
421    fn test_record_skill_reuse() {
422        let mut analyzer = AgentBehaviorAnalyzer::new();
423        analyzer.record_skill_reuse("filter_skill");
424        analyzer.record_skill_reuse("filter_skill");
425        analyzer.record_skill_reuse("transform_skill");
426
427        assert_eq!(analyzer.skill_stats.reused_skills, 3);
428        assert!(analyzer.skill_stats.most_effective_skills.contains(&"filter_skill".to_owned()));
429    }
430
431    #[test]
432    fn test_tool_failure_tracking() {
433        let mut analyzer = AgentBehaviorAnalyzer::new();
434        analyzer.record_tool_failure(tools::GREP_FILE, "timeout");
435        analyzer.record_tool_failure(tools::GREP_FILE, "timeout");
436        analyzer.record_tool_failure(tools::GREP_FILE, "pattern_error");
437
438        assert!(!analyzer.failure_patterns.high_failure_tools.is_empty());
439        assert!(analyzer.failure_patterns.high_failure_tools[0].0 == tools::GREP_FILE);
440    }
441
442    #[test]
443    fn test_summary_generation() {
444        let mut analyzer = AgentBehaviorAnalyzer::new();
445        analyzer.skill_stats.total_skills = 5;
446        analyzer.record_skill_reuse("test_skill");
447
448        let summary = analyzer.summary();
449        assert!(summary.contains("Skill Statistics"));
450        assert!(summary.contains("Total skills: 5"));
451        assert!(summary.contains("Reused skills: 1"));
452    }
453
454    #[test]
455    fn test_identify_risky_tools() {
456        let mut analyzer = AgentBehaviorAnalyzer::new();
457        analyzer
458            .failure_patterns
459            .high_failure_tools
460            .push(("risky_tool".to_owned(), 0.8));
461        analyzer.failure_patterns.high_failure_tools.push(("safe_tool".to_owned(), 0.1));
462
463        let risky = analyzer.identify_risky_tools(0.5);
464        assert_eq!(risky.len(), 1);
465        assert_eq!(risky[0].0, "risky_tool");
466    }
467
468    #[test]
469    fn test_recovery_pattern_lookup() {
470        let mut analyzer = AgentBehaviorAnalyzer::new();
471        analyzer.failure_patterns.recovery_patterns.push(RecoveryPattern {
472            error_type: "timeout".to_owned(),
473            recovery_action: "retry with increased timeout".to_owned(),
474            success_rate: 0.85,
475            attempts: 20,
476        });
477
478        let recovery = analyzer.get_recovery_strategy("timeout");
479        assert!(recovery.is_some());
480        assert!((recovery.unwrap().success_rate - 0.85).abs() < f64::EPSILON);
481    }
482
483    #[test]
484    fn test_should_warn() {
485        let mut analyzer = AgentBehaviorAnalyzer::new();
486        analyzer
487            .failure_patterns
488            .high_failure_tools
489            .push(("risky_tool".to_owned(), 0.7));
490
491        let warning = analyzer.should_warn("risky_tool");
492        assert!(warning.is_some());
493        assert!(warning.unwrap().contains("high failure rate"));
494
495        let no_warning = analyzer.should_warn("safe_tool");
496        assert!(no_warning.is_none());
497    }
498
499    #[test]
500    fn test_get_recovery_action() {
501        let mut analyzer = AgentBehaviorAnalyzer::new();
502        analyzer.failure_patterns.recovery_patterns.push(RecoveryPattern {
503            error_type: "network_error".to_owned(),
504            recovery_action: "retry with exponential backoff".to_owned(),
505            success_rate: 0.9,
506            attempts: 15,
507        });
508
509        let action = analyzer.get_recovery_action("network_error");
510        assert!(action.is_some());
511        let action_str = action.unwrap();
512        assert!(action_str.contains("retry with exponential backoff"));
513        assert!(action_str.contains("90.0%"));
514    }
515
516    #[test]
517    fn test_export_metrics() {
518        let mut analyzer = AgentBehaviorAnalyzer::new();
519        analyzer.skill_stats.total_skills = 10;
520        analyzer.skill_stats.reused_skills = 5;
521        analyzer.record_tool_usage("test_tool");
522
523        let metrics = analyzer.export_metrics();
524        assert_eq!(metrics.get("total_skills").unwrap(), &serde_json::json!(10));
525        assert_eq!(metrics.get("reused_skills").unwrap(), &serde_json::json!(5));
526        assert_eq!(metrics.get("total_tools_used").unwrap(), &serde_json::json!(1));
527    }
528
529    #[test]
530    fn test_apply_recovery_pattern() {
531        let mut analyzer = AgentBehaviorAnalyzer::new();
532        analyzer.failure_patterns.recovery_patterns.push(RecoveryPattern {
533            error_type: "timeout".to_owned(),
534            recovery_action: "retry with increased timeout".to_owned(),
535            success_rate: 0.85,
536            attempts: 20,
537        });
538
539        let applied = analyzer.apply_recovery_pattern("timeout");
540        assert!(applied.is_some());
541        let applied = applied.unwrap();
542        assert_eq!(applied.error_type, "timeout");
543        assert_eq!(applied.recovery_action, "retry with increased timeout");
544        assert!((applied.success_rate - 0.85).abs() < f64::EPSILON);
545        assert_eq!(applied.attempts, 20);
546
547        // Non-existent error type
548        let no_pattern = analyzer.apply_recovery_pattern("unknown_error");
549        assert!(no_pattern.is_none());
550    }
551
552    #[test]
553    fn test_record_recovery_outcome_success() {
554        let mut analyzer = AgentBehaviorAnalyzer::new();
555        analyzer.failure_patterns.recovery_patterns.push(RecoveryPattern {
556            error_type: "network_error".to_owned(),
557            recovery_action: "retry".to_owned(),
558            success_rate: 0.5,
559            attempts: 10,
560        });
561
562        analyzer.record_recovery_outcome("network_error", true);
563
564        let pattern = &analyzer.failure_patterns.recovery_patterns[0];
565        assert_eq!(pattern.attempts, 11);
566        // Success rate should increase (exponential moving average)
567        assert!(pattern.success_rate > 0.5);
568    }
569
570    #[test]
571    fn test_record_recovery_outcome_failure() {
572        let mut analyzer = AgentBehaviorAnalyzer::new();
573        analyzer.failure_patterns.recovery_patterns.push(RecoveryPattern {
574            error_type: "parse_error".to_owned(),
575            recovery_action: "simplify input".to_owned(),
576            success_rate: 0.8,
577            attempts: 5,
578        });
579
580        analyzer.record_recovery_outcome("parse_error", false);
581
582        let pattern = &analyzer.failure_patterns.recovery_patterns[0];
583        assert_eq!(pattern.attempts, 6);
584        // Success rate should decrease
585        assert!(pattern.success_rate < 0.8);
586    }
587
588    #[test]
589    fn test_add_recovery_pattern_new() {
590        let mut analyzer = AgentBehaviorAnalyzer::new();
591
592        analyzer.add_recovery_pattern("new_error".to_owned(), "new recovery action".to_owned(), 0.75);
593
594        assert_eq!(analyzer.failure_patterns.recovery_patterns.len(), 1);
595        let pattern = &analyzer.failure_patterns.recovery_patterns[0];
596        assert_eq!(pattern.error_type, "new_error");
597        assert_eq!(pattern.recovery_action, "new recovery action");
598        assert!((pattern.success_rate - 0.75).abs() < f64::EPSILON);
599        assert_eq!(pattern.attempts, 0);
600    }
601
602    #[test]
603    fn test_add_recovery_pattern_update_existing() {
604        let mut analyzer = AgentBehaviorAnalyzer::new();
605        analyzer.failure_patterns.recovery_patterns.push(RecoveryPattern {
606            error_type: "existing_error".to_owned(),
607            recovery_action: "old action".to_owned(),
608            success_rate: 0.5,
609            attempts: 10,
610        });
611
612        analyzer.add_recovery_pattern("existing_error".to_owned(), "updated action".to_owned(), 0.9);
613
614        assert_eq!(analyzer.failure_patterns.recovery_patterns.len(), 1);
615        let pattern = &analyzer.failure_patterns.recovery_patterns[0];
616        assert_eq!(pattern.error_type, "existing_error");
617        assert_eq!(pattern.recovery_action, "updated action");
618        assert!((pattern.success_rate - 0.9).abs() < f64::EPSILON);
619        // Attempts should be preserved from original
620        assert_eq!(pattern.attempts, 10);
621    }
622
623    #[test]
624    fn test_export_metrics_with_recovery_patterns() {
625        let mut analyzer = AgentBehaviorAnalyzer::new();
626        analyzer.add_recovery_pattern("error1".to_owned(), "action1".to_owned(), 0.8);
627        analyzer.add_recovery_pattern("error2".to_owned(), "action2".to_owned(), 0.9);
628
629        let metrics = analyzer.export_metrics();
630        assert_eq!(metrics.get("recovery_patterns_count").unwrap(), &serde_json::json!(2));
631    }
632
633    #[test]
634    fn test_export_metrics_with_top_tools() {
635        let mut analyzer = AgentBehaviorAnalyzer::new();
636        analyzer.record_tool_usage("tool_a");
637        analyzer.record_tool_usage("tool_a");
638        analyzer.record_tool_usage("tool_a");
639        analyzer.record_tool_usage("tool_b");
640        analyzer.record_tool_usage("tool_b");
641        analyzer.record_tool_usage("tool_c");
642
643        let metrics = analyzer.export_metrics();
644        let top_tools = metrics.get("top_tools").unwrap();
645
646        // Verify it's a JSON object
647        assert!(top_tools.is_object());
648
649        // Verify tool_a has highest count
650        let top_tools_map = top_tools.as_object().unwrap();
651        assert_eq!(top_tools_map.get("tool_a").unwrap(), &serde_json::json!(3));
652        assert_eq!(top_tools_map.get("tool_b").unwrap(), &serde_json::json!(2));
653        assert_eq!(top_tools_map.get("tool_c").unwrap(), &serde_json::json!(1));
654    }
655}