agtrace_engine/analysis/
digest.rs

1use super::metrics::{SessionMetrics, compute_metrics};
2use crate::AgentSession;
3
4#[derive(Debug, Clone)]
5pub struct SessionDigest {
6    pub session_id: String,
7    pub provider: String,
8    pub session: AgentSession,
9    pub opening: Option<String>,
10    pub activation: Option<String>,
11    pub metrics: SessionMetrics,
12    pub recency_boost: u32,
13    pub selection_reason: Option<String>,
14}
15
16impl SessionDigest {
17    pub fn new(
18        session_id: &str,
19        provider: &str,
20        session: AgentSession,
21        recency_boost: u32,
22    ) -> Self {
23        let opening = session
24            .turns
25            .first()
26            .map(|t| clean_snippet(&t.user.content.text))
27            .filter(|s| !s.is_empty())
28            .map(|s| truncate_string(&s, 100));
29
30        let metrics = compute_metrics(&session);
31        let activation = find_activation(&session);
32
33        Self {
34            session_id: session_id.to_string(),
35            provider: provider.to_string(),
36            session,
37            opening,
38            activation,
39            metrics,
40            recency_boost,
41            selection_reason: None,
42        }
43    }
44}
45
46fn clean_snippet(text: &str) -> String {
47    let mut cleaned = text.to_string();
48
49    let noise_tags = [
50        ("<environment_context>", "</environment_context>"),
51        ("<command_output>", "</command_output>"),
52        ("<changed_files>", "</changed_files>"),
53    ];
54
55    for (start_tag, end_tag) in noise_tags {
56        while let Some(start_idx) = cleaned.find(start_tag) {
57            if let Some(end_idx) = cleaned[start_idx..].find(end_tag) {
58                let absolute_end = start_idx + end_idx + end_tag.len();
59                cleaned.replace_range(start_idx..absolute_end, " [..meta..] ");
60            } else {
61                break;
62            }
63        }
64    }
65
66    let fields: Vec<&str> = cleaned.split_whitespace().collect();
67    fields.join(" ")
68}
69
70fn find_activation(session: &AgentSession) -> Option<String> {
71    if session.turns.is_empty() {
72        return None;
73    }
74
75    let (best_idx, max_tools) = session
76        .turns
77        .iter()
78        .enumerate()
79        .map(|(i, turn)| {
80            let tool_count: usize = turn.steps.iter().map(|s| s.tools.len()).sum();
81            (i, tool_count)
82        })
83        .max_by_key(|(_, count)| *count)
84        .unwrap_or((0, 0));
85
86    if max_tools < 3 {
87        return None;
88    }
89
90    session
91        .turns
92        .get(best_idx)
93        .map(|turn| clean_snippet(&turn.user.content.text))
94        .map(|s| truncate_string(&s, 120))
95}
96
97fn truncate_string(s: &str, max_len: usize) -> String {
98    if s.chars().count() <= max_len {
99        s.to_string()
100    } else {
101        let chars: Vec<char> = s.chars().take(max_len).collect();
102        chars.iter().collect::<String>() + "..."
103    }
104}