1use 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
20pub struct SkillStatistics {
21 pub creation_to_reuse_time: Option<Duration>,
23 pub avg_lifecycle: Option<Duration>,
25 pub reuse_ratio_by_tag: HashMap<String, f64>,
27 pub most_effective_skills: Vec<String>,
29 pub rarely_used_skills: Vec<String>,
31 pub total_skills: usize,
33 pub reused_skills: usize,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ToolStatistics {
40 pub discovery_success_rate: f64,
42 pub usage_frequency: HashMap<String, u64>,
44 pub common_tool_chains: Vec<Vec<String>>,
46 pub typical_discovery_queries: Vec<String>,
48 pub total_discoveries: u64,
50 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#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CodePattern {
70 pub language: String,
72 pub pattern: String,
74 pub failure_rate: f64,
76 pub example_failures: Vec<String>,
78 pub occurrences: u64,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct RecoveryPattern {
85 pub error_type: String,
87 pub recovery_action: String,
89 pub success_rate: f64,
91 pub attempts: u64,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct AppliedRecovery {
98 pub error_type: String,
100 pub recovery_action: String,
102 pub success_rate: f64,
104 pub attempts: u64,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
110pub struct FailurePatterns {
111 pub high_failure_tools: Vec<(String, f64)>,
113 pub high_failure_patterns: Vec<CodePattern>,
115 pub common_errors: Vec<(String, u64)>,
117 pub recovery_patterns: Vec<RecoveryPattern>,
119}
120
121#[derive(Default)]
123pub struct AgentBehaviorAnalyzer {
124 skill_stats: SkillStatistics,
125 tool_stats: ToolStatistics,
126 failure_patterns: FailurePatterns,
127}
128
129impl AgentBehaviorAnalyzer {
130 pub fn new() -> Self {
132 Self::default()
133 }
134
135 pub fn skill_stats(&self) -> &SkillStatistics {
137 &self.skill_stats
138 }
139
140 pub fn tool_stats(&self) -> &ToolStatistics {
142 &self.tool_stats
143 }
144
145 pub fn failure_patterns(&self) -> &FailurePatterns {
147 &self.failure_patterns
148 }
149
150 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 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 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 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 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 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 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 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 pub fn record_tool_failure(&mut self, tool_name: &str, error_msg: &str) {
210 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 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; 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 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 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 pub fn export_metrics(&self) -> HashMap<String, serde_json::Value> {
261 let mut metrics = HashMap::new();
262
263 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 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 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 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 pub fn apply_recovery_pattern(&mut self, error_type: &str) -> Option<AppliedRecovery> {
294 let pattern = self
296 .failure_patterns
297 .recovery_patterns
298 .iter()
299 .find(|p| p.error_type == error_type)?;
300
301 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 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 let alpha = 0.3; pattern.success_rate = alpha + (1.0 - alpha) * pattern.success_rate;
332 } else {
333 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 pub fn add_recovery_pattern(&mut self, error_type: String, recovery_action: String, initial_success_rate: f64) {
349 if let Some(pattern) = self
351 .failure_patterns
352 .recovery_patterns
353 .iter_mut()
354 .find(|p| p.error_type == error_type)
355 {
356 pattern.recovery_action = recovery_action;
358 pattern.success_rate = initial_success_rate;
359 } else {
360 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 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 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 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 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 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 assert!(top_tools.is_object());
648
649 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}