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