agtrace_engine/analysis/
digest.rs1use super::metrics::{SessionMetrics, compute_metrics};
2use crate::AgentSession;
3
4#[derive(Debug, Clone)]
5pub struct SessionDigest {
6 pub session_id: String,
7 pub source: 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 source: 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}