1use crate::error::Result;
2use agtrace_engine::{AgentSession, SessionSummary};
3
4#[derive(Debug, Clone, Copy)]
5pub enum Lens {
6 Failures,
7 Loops,
8 Bottlenecks,
9}
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Severity {
13 Info,
14 Warning,
15 Critical,
16}
17
18#[derive(Debug, Clone)]
19pub struct Insight {
20 pub turn_index: usize,
21 pub lens: Lens,
22 pub message: String,
23 pub severity: Severity,
24}
25
26pub struct SessionAnalyzer {
27 session: AgentSession,
28 lenses: Vec<Lens>,
29}
30
31impl SessionAnalyzer {
32 pub fn new(session: AgentSession) -> Self {
33 Self {
34 session,
35 lenses: vec![],
36 }
37 }
38
39 pub fn through(mut self, lens: Lens) -> Self {
40 self.lenses.push(lens);
41 self
42 }
43
44 pub fn report(self) -> Result<AnalysisReport> {
45 let summary = agtrace_engine::session::summarize(&self.session);
46 let mut insights = Vec::new();
47
48 for lens in &self.lenses {
49 match lens {
50 Lens::Failures => {
51 insights.extend(self.analyze_failures());
52 }
53 Lens::Loops => {
54 insights.extend(self.analyze_loops());
55 }
56 Lens::Bottlenecks => {
57 insights.extend(self.analyze_bottlenecks());
58 }
59 }
60 }
61
62 let score = self.calculate_health_score(&summary, &insights);
63
64 Ok(AnalysisReport {
65 score,
66 insights,
67 summary,
68 })
69 }
70
71 fn analyze_failures(&self) -> Vec<Insight> {
72 let mut insights = Vec::new();
73 for (idx, turn) in self.session.turns.iter().enumerate() {
74 let failed_tools: Vec<_> = turn
75 .steps
76 .iter()
77 .flat_map(|step| &step.tools)
78 .filter(|tool_exec| tool_exec.is_error)
79 .collect();
80
81 if !failed_tools.is_empty() {
82 insights.push(Insight {
83 turn_index: idx,
84 lens: Lens::Failures,
85 message: format!("{} tool execution(s) failed", failed_tools.len()),
86 severity: Severity::Critical,
87 });
88 }
89 }
90 insights
91 }
92
93 fn analyze_loops(&self) -> Vec<Insight> {
94 let mut insights = Vec::new();
95 let mut prev_tool_sequence: Option<Vec<String>> = None;
96 let mut repeat_count = 0;
97
98 for (idx, turn) in self.session.turns.iter().enumerate() {
99 let tool_sequence: Vec<String> = turn
100 .steps
101 .iter()
102 .flat_map(|step| &step.tools)
103 .map(|tool_exec| format!("{:?}", tool_exec.call.content.kind()))
104 .collect();
105
106 if let Some(prev) = &prev_tool_sequence {
107 if prev == &tool_sequence && !tool_sequence.is_empty() {
108 repeat_count += 1;
109 if repeat_count >= 2 {
110 insights.push(Insight {
111 turn_index: idx,
112 lens: Lens::Loops,
113 message: "Repeated tool sequence detected".to_string(),
114 severity: Severity::Warning,
115 });
116 }
117 } else {
118 repeat_count = 0;
119 }
120 }
121
122 prev_tool_sequence = Some(tool_sequence);
123 }
124 insights
125 }
126
127 fn analyze_bottlenecks(&self) -> Vec<Insight> {
128 let mut insights = Vec::new();
129 for (idx, turn) in self.session.turns.iter().enumerate() {
130 for step in &turn.steps {
131 for tool_exec in &step.tools {
132 if let Some(duration_ms) = tool_exec.duration_ms.filter(|&d| d > 10_000) {
133 insights.push(Insight {
134 turn_index: idx,
135 lens: Lens::Bottlenecks,
136 message: format!(
137 "Slow tool execution ({:?} took {}s)",
138 tool_exec.call.content.kind(),
139 duration_ms / 1000
140 ),
141 severity: Severity::Warning,
142 });
143 }
144 }
145 }
146 }
147 insights
148 }
149
150 fn calculate_health_score(&self, _summary: &SessionSummary, insights: &[Insight]) -> u8 {
151 let base_score = 100;
152 let penalty_per_insight = 10;
153 let total_penalty = (insights.len() as i32) * penalty_per_insight;
154 let score = base_score - total_penalty;
155 score.max(0) as u8
156 }
157}
158
159#[derive(Debug)]
160pub struct AnalysisReport {
161 pub score: u8,
162 pub insights: Vec<Insight>,
163 pub summary: SessionSummary,
164}