agtrace_sdk/
analysis.rs

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}