1use std::collections::HashMap;
2
3use crate::data::models::SessionData;
4use crate::pricing::calculator::PricingCalculator;
5
6use super::{AgentDetail, AgentSummary, AggregatedTokens, SessionResult, TurnCostBreakdown, TurnDetail};
7
8#[derive(Debug, Clone, Default)]
10pub struct AgentMeta {
11 pub agent_type: String,
12 pub description: String,
13}
14
15pub fn analyze_session(
16 session: &SessionData,
17 calc: &PricingCalculator,
18 agent_meta: &std::collections::HashMap<String, AgentMeta>,
19) -> SessionResult {
20 let all_turns = session.all_responses();
21
22 let mut turn_details = Vec::new();
23 let mut total_tokens = AggregatedTokens::default();
24 let mut total_cost = 0.0;
25 let mut stop_reason_counts: HashMap<String, usize> = HashMap::new();
26 let mut agent_summary = AgentSummary::default();
27 let mut model_counts: HashMap<&str, usize> = HashMap::new();
28 let mut max_context: u64 = 0;
29 let mut prev_context_size: Option<u64> = None;
30 let mut agent_acc: HashMap<String, (usize, u64, f64)> = HashMap::new();
31
32 for (i, turn) in all_turns.iter().enumerate() {
33 let input = turn.usage.input_tokens.unwrap_or(0);
34 let output = turn.usage.output_tokens.unwrap_or(0);
35 let cache_create = turn.usage.cache_creation_input_tokens.unwrap_or(0);
36 let cache_read = turn.usage.cache_read_input_tokens.unwrap_or(0);
37
38 let (cache_write_5m, cache_write_1h) = if let Some(ref detail) = turn.usage.cache_creation {
40 (
41 detail.ephemeral_5m_input_tokens.unwrap_or(0),
42 detail.ephemeral_1h_input_tokens.unwrap_or(0),
43 )
44 } else {
45 (0, 0)
46 };
47
48 let context_size = input + cache_create + cache_read;
49 let cache_hit_rate = if context_size > 0 {
50 (cache_read as f64 / context_size as f64) * 100.0
51 } else {
52 0.0
53 };
54
55 if context_size > max_context {
57 max_context = context_size;
58 }
59
60 let is_compaction = match prev_context_size {
62 Some(prev) => prev > 0 && (context_size as f64) < (prev as f64 * 0.9),
63 None => false,
64 };
65 let context_delta = match prev_context_size {
66 Some(prev) => context_size as i64 - prev as i64,
67 None => 0,
68 };
69 prev_context_size = Some(context_size);
70
71 let pricing_cost = calc.calculate_turn_cost(&turn.model, &turn.usage);
72
73 let cost_breakdown = TurnCostBreakdown {
74 input_cost: pricing_cost.input_cost,
75 output_cost: pricing_cost.output_cost,
76 cache_write_5m_cost: pricing_cost.cache_write_5m_cost,
77 cache_write_1h_cost: pricing_cost.cache_write_1h_cost,
78 cache_read_cost: pricing_cost.cache_read_cost,
79 total: pricing_cost.total,
80 };
81
82 if let Some(ref reason) = turn.stop_reason {
84 *stop_reason_counts.entry(reason.clone()).or_insert(0) += 1;
85 }
86
87 total_tokens.add_usage(&turn.usage);
89 total_cost += pricing_cost.total;
90
91 *model_counts.entry(&turn.model).or_insert(0) += 1;
93
94 let is_agent = turn.is_agent;
96 if is_agent {
97 agent_summary.total_agent_turns += 1;
98 agent_summary.agent_output_tokens += output;
99 agent_summary.agent_cost += pricing_cost.total;
100
101 let aid = turn.agent_id.clone().unwrap_or_default();
103 if !aid.is_empty() {
104 let entry = agent_acc.entry(aid).or_insert((0usize, 0u64, 0.0f64));
105 entry.0 += 1;
106 entry.1 += output;
107 entry.2 += pricing_cost.total;
108 }
109 }
110
111 turn_details.push(TurnDetail {
112 turn_number: i + 1,
113 timestamp: turn.timestamp,
114 model: turn.model.clone(),
115 input_tokens: input,
116 output_tokens: output,
117 cache_write_5m_tokens: cache_write_5m,
118 cache_write_1h_tokens: cache_write_1h,
119 cache_read_tokens: cache_read,
120 context_size,
121 cache_hit_rate,
122 cost: pricing_cost.total,
123 cost_breakdown,
124 stop_reason: turn.stop_reason.clone(),
125 is_agent,
126 is_compaction,
127 context_delta,
128 user_text: turn.user_text.clone(),
129 assistant_text: turn.assistant_text.clone(),
130 tool_names: turn.tool_names.clone(),
131 });
132 }
133
134 let duration_minutes = match (session.first_timestamp, session.last_timestamp) {
136 (Some(first), Some(last)) => (last - first).num_seconds() as f64 / 60.0,
137 _ => 0.0,
138 };
139
140 let compaction_count = turn_details.iter().filter(|t| t.is_compaction).count();
142
143 let total_5m = total_tokens.cache_write_5m_tokens;
145 let total_1h = total_tokens.cache_write_1h_tokens;
146 let total_cache_write = total_5m + total_1h;
147 let cache_write_5m_pct = if total_cache_write > 0 {
148 (total_5m as f64 / total_cache_write as f64) * 100.0
149 } else {
150 0.0
151 };
152 let cache_write_1h_pct = if total_cache_write > 0 {
153 (total_1h as f64 / total_cache_write as f64) * 100.0
154 } else {
155 0.0
156 };
157
158 let mut agents: Vec<AgentDetail> = agent_acc.into_iter().map(|(aid, (turns, output, cost))| {
160 let meta = agent_meta.get(&aid);
161 AgentDetail {
162 agent_id: aid,
163 agent_type: meta.map_or_else(|| "unknown".into(), |m| m.agent_type.clone()),
164 description: meta.map_or_else(|| "".into(), |m| m.description.clone()),
165 turns,
166 output_tokens: output,
167 cost,
168 }
169 }).collect();
170 agents.sort_by(|a, b| b.cost.partial_cmp(&a.cost).unwrap_or(std::cmp::Ordering::Equal));
171 agent_summary.agents = agents;
172
173 let model = model_counts
175 .into_iter()
176 .max_by_key(|(_, count)| *count)
177 .map(|(m, _)| m.to_string())
178 .unwrap_or_default();
179
180 SessionResult {
181 session_id: session.session_id.clone(),
182 project: session
183 .project
184 .clone()
185 .unwrap_or_else(|| "(unknown)".to_string()),
186 turn_details,
187 agent_summary,
188 total_tokens,
189 total_cost,
190 stop_reason_counts,
191 duration_minutes,
192 max_context,
193 compaction_count,
194 cache_write_5m_pct,
195 cache_write_1h_pct,
196 model,
197 }
198}