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