1use std::collections::HashMap;
2
3use crate::data::models::SessionData;
4use crate::pricing::calculator::PricingCalculator;
5
6use super::{
7 AgentDetail, AgentSummary, AggregatedTokens, SessionResult, TurnCostBreakdown, TurnDetail,
8};
9
10#[derive(Debug, Clone, Default)]
12pub struct AgentMeta {
13 pub agent_type: String,
14 pub description: String,
15}
16
17pub fn analyze_session(
18 session: &SessionData,
19 calc: &PricingCalculator,
20 agent_meta: &std::collections::HashMap<String, AgentMeta>,
21) -> SessionResult {
22 let all_turns = session.all_responses();
23
24 let mut turn_details = Vec::new();
25 let mut total_tokens = AggregatedTokens::default();
26 let mut total_cost = 0.0;
27 let mut stop_reason_counts: HashMap<String, usize> = HashMap::new();
28 let mut agent_summary = AgentSummary::default();
29 let mut model_counts: HashMap<&str, usize> = HashMap::new();
30 let mut max_context: u64 = 0;
31 let mut prev_context_size: Option<u64> = None;
32 let mut agent_acc: HashMap<String, (usize, u64, f64)> = HashMap::new();
33
34 let mut tool_error_count: usize = 0;
36 let mut truncated_count: usize = 0;
37 let mut service_tiers: HashMap<String, usize> = HashMap::new();
38 let mut speeds: HashMap<String, usize> = HashMap::new();
39 let mut inference_geos: HashMap<String, usize> = HashMap::new();
40 let mut git_branches: HashMap<String, usize> = HashMap::new();
41
42 for (i, turn) in all_turns.iter().enumerate() {
43 let input = turn.usage.input_tokens.unwrap_or(0);
44 let output = turn.usage.output_tokens.unwrap_or(0);
45 let cache_create = turn.usage.cache_creation_input_tokens.unwrap_or(0);
46 let cache_read = turn.usage.cache_read_input_tokens.unwrap_or(0);
47
48 let (cache_write_5m, cache_write_1h) = if let Some(ref detail) = turn.usage.cache_creation {
50 (
51 detail.ephemeral_5m_input_tokens.unwrap_or(0),
52 detail.ephemeral_1h_input_tokens.unwrap_or(0),
53 )
54 } else {
55 (0, 0)
56 };
57
58 let context_size = input + cache_create + cache_read;
59 let cache_hit_rate = if context_size > 0 {
60 (cache_read as f64 / context_size as f64) * 100.0
61 } else {
62 0.0
63 };
64
65 if context_size > max_context {
67 max_context = context_size;
68 }
69
70 let is_compaction = match prev_context_size {
72 Some(prev) => prev > 0 && (context_size as f64) < (prev as f64 * 0.9),
73 None => false,
74 };
75 let context_delta = match prev_context_size {
76 Some(prev) => context_size as i64 - prev as i64,
77 None => 0,
78 };
79 prev_context_size = Some(context_size);
80
81 let pricing_cost = calc.calculate_turn_cost(&turn.model, &turn.usage);
82
83 let cost_breakdown = TurnCostBreakdown {
84 input_cost: pricing_cost.input_cost,
85 output_cost: pricing_cost.output_cost,
86 cache_write_5m_cost: pricing_cost.cache_write_5m_cost,
87 cache_write_1h_cost: pricing_cost.cache_write_1h_cost,
88 cache_read_cost: pricing_cost.cache_read_cost,
89 total: pricing_cost.total,
90 };
91
92 if let Some(ref reason) = turn.stop_reason {
94 *stop_reason_counts.entry(reason.clone()).or_insert(0) += 1;
95 if reason == "max_tokens" {
96 truncated_count += 1;
97 }
98 }
99
100 tool_error_count += turn.tool_error_count;
102 if let Some(ref tier) = turn.service_tier {
103 if !tier.is_empty() {
104 *service_tiers.entry(tier.clone()).or_insert(0) += 1;
105 }
106 }
107 if let Some(ref spd) = turn.speed {
108 if !spd.is_empty() {
109 *speeds.entry(spd.clone()).or_insert(0) += 1;
110 }
111 }
112 if let Some(ref geo) = turn.inference_geo {
113 if !geo.is_empty() {
114 *inference_geos.entry(geo.clone()).or_insert(0) += 1;
115 }
116 }
117 if let Some(ref branch) = turn.git_branch {
118 if !branch.is_empty() {
119 *git_branches.entry(branch.clone()).or_insert(0) += 1;
120 }
121 }
122
123 total_tokens.add_usage(&turn.usage);
125 total_cost += pricing_cost.total;
126
127 *model_counts.entry(&turn.model).or_insert(0) += 1;
129
130 let is_agent = turn.is_agent;
132 if is_agent {
133 agent_summary.total_agent_turns += 1;
134 agent_summary.agent_output_tokens += output;
135 agent_summary.agent_cost += pricing_cost.total;
136
137 let aid = turn.agent_id.clone().unwrap_or_default();
139 if !aid.is_empty() {
140 let entry = agent_acc.entry(aid).or_insert((0usize, 0u64, 0.0f64));
141 entry.0 += 1;
142 entry.1 += output;
143 entry.2 += pricing_cost.total;
144 }
145 }
146
147 turn_details.push(TurnDetail {
148 turn_number: i + 1,
149 timestamp: turn.timestamp,
150 model: turn.model.clone(),
151 input_tokens: input,
152 output_tokens: output,
153 cache_write_5m_tokens: cache_write_5m,
154 cache_write_1h_tokens: cache_write_1h,
155 cache_read_tokens: cache_read,
156 context_size,
157 cache_hit_rate,
158 cost: pricing_cost.total,
159 cost_breakdown,
160 stop_reason: turn.stop_reason.clone(),
161 is_agent,
162 is_compaction,
163 context_delta,
164 user_text: turn.user_text.clone(),
165 assistant_text: turn.assistant_text.clone(),
166 tool_names: turn.tool_names.clone(),
167 });
168 }
169
170 let duration_minutes = match (session.first_timestamp, session.last_timestamp) {
172 (Some(first), Some(last)) => (last - first).num_seconds() as f64 / 60.0,
173 _ => 0.0,
174 };
175
176 let compaction_count = turn_details.iter().filter(|t| t.is_compaction).count();
178
179 let total_5m = total_tokens.cache_write_5m_tokens;
181 let total_1h = total_tokens.cache_write_1h_tokens;
182 let total_cache_write = total_5m + total_1h;
183 let cache_write_5m_pct = if total_cache_write > 0 {
184 (total_5m as f64 / total_cache_write as f64) * 100.0
185 } else {
186 0.0
187 };
188 let cache_write_1h_pct = if total_cache_write > 0 {
189 (total_1h as f64 / total_cache_write as f64) * 100.0
190 } else {
191 0.0
192 };
193
194 let mut agents: Vec<AgentDetail> = agent_acc
196 .into_iter()
197 .map(|(aid, (turns, output, cost))| {
198 let meta = agent_meta.get(&aid);
199 AgentDetail {
200 agent_id: aid,
201 agent_type: meta.map_or_else(|| "unknown".into(), |m| m.agent_type.clone()),
202 description: meta.map_or_else(|| "".into(), |m| m.description.clone()),
203 turns,
204 output_tokens: output,
205 cost,
206 }
207 })
208 .collect();
209 agents.sort_by(|a, b| {
210 b.cost
211 .partial_cmp(&a.cost)
212 .unwrap_or(std::cmp::Ordering::Equal)
213 });
214 agent_summary.agents = agents;
215
216 let model = model_counts
218 .into_iter()
219 .max_by_key(|(_, count)| *count)
220 .map(|(m, _)| m.to_string())
221 .unwrap_or_default();
222
223 let total_turn_count = session.total_turn_count();
225 let user_prompt_count = session.metadata.user_prompt_count;
226 let autonomy_ratio = if user_prompt_count > 0 {
227 total_turn_count as f64 / user_prompt_count as f64
228 } else {
229 0.0
230 };
231
232 SessionResult {
233 session_id: session.session_id.clone(),
234 project: session
235 .project
236 .clone()
237 .unwrap_or_else(|| "(unknown)".to_string()),
238 turn_details,
239 agent_summary,
240 total_tokens,
241 total_cost,
242 stop_reason_counts,
243 duration_minutes,
244 max_context,
245 compaction_count,
246 cache_write_5m_pct,
247 cache_write_1h_pct,
248 model,
249 title: session.metadata.title.clone(),
251 tags: session.metadata.tags.clone(),
252 mode: session.metadata.mode.clone(),
253 pr_links: session.metadata.pr_links.clone(),
254 user_prompt_count,
256 autonomy_ratio,
257 api_error_count: session.metadata.api_error_count,
259 tool_error_count,
260 truncated_count,
261 speculation_accepts: session.metadata.speculation_accepts,
263 speculation_time_saved_ms: session.metadata.speculation_time_saved_ms,
264 service_tiers,
266 speeds,
267 inference_geos,
268 git_branches,
270 collapse_count: session.metadata.collapse_commits.len(),
272 collapse_summaries: session
273 .metadata
274 .collapse_commits
275 .iter()
276 .map(|c| c.summary.clone())
277 .collect(),
278 collapse_avg_risk: session
279 .metadata
280 .collapse_snapshot
281 .as_ref()
282 .map_or(0.0, |s| s.avg_risk),
283 collapse_max_risk: session
284 .metadata
285 .collapse_snapshot
286 .as_ref()
287 .map_or(0.0, |s| s.max_risk),
288 attribution: session.metadata.attribution.clone(),
290 }
291}