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