1use std::collections::HashMap;
2use std::path::Path;
3
4use crate::data::models::SessionData;
5use crate::pricing::calculator::PricingCalculator;
6
7use super::{
8 AgentDetail, AgentSummary, AggregatedTokens, SessionResult, SubagentSummary, TurnCostBreakdown,
9 TurnDetail, WorkflowPhaseSummary, WorkflowSummary,
10};
11
12#[derive(Debug, Clone, Default)]
14pub struct AgentMeta {
15 pub agent_type: String,
16 pub description: String,
17}
18
19pub fn analyze_session(
20 session: &SessionData,
21 calc: &PricingCalculator,
22 agent_meta: &std::collections::HashMap<String, AgentMeta>,
23 claude_home: &Path,
24) -> SessionResult {
25 let all_turns = session.all_responses();
26
27 let mut turn_details = Vec::new();
28 let mut total_tokens = AggregatedTokens::default();
29 let mut total_cost = 0.0;
30 let mut stop_reason_counts: HashMap<String, usize> = HashMap::new();
31 let mut agent_summary = AgentSummary::default();
32 let mut model_counts: HashMap<&str, usize> = HashMap::new();
33 let mut max_context: u64 = 0;
34 let mut prev_context_size: Option<u64> = None;
35 let mut agent_acc: HashMap<String, (usize, u64, f64)> = HashMap::new();
36
37 let mut tool_error_count: usize = 0;
39 let mut truncated_count: usize = 0;
40 let mut service_tiers: HashMap<String, usize> = HashMap::new();
41 let mut speeds: HashMap<String, usize> = HashMap::new();
42 let mut inference_geos: HashMap<String, usize> = HashMap::new();
43 let mut git_branches: HashMap<String, usize> = HashMap::new();
44
45 for (i, turn) in all_turns.iter().enumerate() {
46 let input = turn.usage.input_tokens.unwrap_or(0);
47 let output = turn.usage.output_tokens.unwrap_or(0);
48 let cache_create = turn.usage.cache_creation_input_tokens.unwrap_or(0);
49 let cache_read = turn.usage.cache_read_input_tokens.unwrap_or(0);
50
51 let (cache_write_5m, cache_write_1h) = if let Some(ref detail) = turn.usage.cache_creation {
53 (
54 detail.ephemeral_5m_input_tokens.unwrap_or(0),
55 detail.ephemeral_1h_input_tokens.unwrap_or(0),
56 )
57 } else {
58 (0, 0)
59 };
60
61 let context_size = input + cache_create + cache_read;
62 let cache_hit_rate = if context_size > 0 {
63 (cache_read as f64 / context_size as f64) * 100.0
64 } else {
65 0.0
66 };
67
68 if context_size > max_context {
70 max_context = context_size;
71 }
72
73 let is_compaction = match prev_context_size {
75 Some(prev) => prev > 0 && (context_size as f64) < (prev as f64 * 0.9),
76 None => false,
77 };
78 let context_delta = match prev_context_size {
79 Some(prev) => context_size as i64 - prev as i64,
80 None => 0,
81 };
82 prev_context_size = Some(context_size);
83
84 let pricing_cost = calc.calculate_turn_cost(&turn.model, &turn.usage);
85
86 let cost_breakdown = TurnCostBreakdown {
87 input_cost: pricing_cost.input_cost,
88 output_cost: pricing_cost.output_cost,
89 cache_write_5m_cost: pricing_cost.cache_write_5m_cost,
90 cache_write_1h_cost: pricing_cost.cache_write_1h_cost,
91 cache_read_cost: pricing_cost.cache_read_cost,
92 total: pricing_cost.total,
93 };
94
95 if let Some(ref reason) = turn.stop_reason {
97 *stop_reason_counts.entry(reason.clone()).or_insert(0) += 1;
98 if reason == "max_tokens" {
99 truncated_count += 1;
100 }
101 }
102
103 tool_error_count += turn.tool_error_count;
105 if let Some(ref tier) = turn.service_tier {
106 if !tier.is_empty() {
107 *service_tiers.entry(tier.clone()).or_insert(0) += 1;
108 }
109 }
110 if let Some(ref spd) = turn.speed {
111 if !spd.is_empty() {
112 *speeds.entry(spd.clone()).or_insert(0) += 1;
113 }
114 }
115 if let Some(ref geo) = turn.inference_geo {
116 if !geo.is_empty() {
117 *inference_geos.entry(geo.clone()).or_insert(0) += 1;
118 }
119 }
120 if let Some(ref branch) = turn.git_branch {
121 if !branch.is_empty() {
122 *git_branches.entry(branch.clone()).or_insert(0) += 1;
123 }
124 }
125
126 total_tokens.add_usage(&turn.usage);
128 total_cost += pricing_cost.total;
129
130 *model_counts.entry(&turn.model).or_insert(0) += 1;
132
133 let is_agent = turn.is_agent;
135 if is_agent {
136 agent_summary.total_agent_turns += 1;
137 agent_summary.agent_output_tokens += output;
138 agent_summary.agent_cost += pricing_cost.total;
139
140 let aid = turn.agent_id.clone().unwrap_or_default();
142 if !aid.is_empty() {
143 let entry = agent_acc.entry(aid).or_insert((0usize, 0u64, 0.0f64));
144 entry.0 += 1;
145 entry.1 += output;
146 entry.2 += pricing_cost.total;
147 }
148 }
149
150 turn_details.push(TurnDetail {
151 turn_number: i + 1,
152 timestamp: turn.timestamp,
153 model: turn.model.clone(),
154 input_tokens: input,
155 output_tokens: output,
156 cache_write_5m_tokens: cache_write_5m,
157 cache_write_1h_tokens: cache_write_1h,
158 cache_read_tokens: cache_read,
159 context_size,
160 cache_hit_rate,
161 cost: pricing_cost.total,
162 cost_breakdown,
163 stop_reason: turn.stop_reason.clone(),
164 is_agent,
165 is_compaction,
166 context_delta,
167 user_text: turn.user_text.clone(),
168 assistant_text: turn.assistant_text.clone(),
169 tool_names: turn.tool_names.clone(),
170 });
171 }
172
173 let duration_minutes = match (session.first_timestamp, session.last_timestamp) {
175 (Some(first), Some(last)) => (last - first).num_seconds() as f64 / 60.0,
176 _ => 0.0,
177 };
178
179 let compaction_count = turn_details.iter().filter(|t| t.is_compaction).count();
181
182 let total_5m = total_tokens.cache_write_5m_tokens;
184 let total_1h = total_tokens.cache_write_1h_tokens;
185 let total_cache_write = total_5m + total_1h;
186 let cache_write_5m_pct = if total_cache_write > 0 {
187 (total_5m as f64 / total_cache_write as f64) * 100.0
188 } else {
189 0.0
190 };
191 let cache_write_1h_pct = if total_cache_write > 0 {
192 (total_1h as f64 / total_cache_write as f64) * 100.0
193 } else {
194 0.0
195 };
196
197 let mut agents: Vec<AgentDetail> = agent_acc
199 .into_iter()
200 .map(|(aid, (turns, output, cost))| {
201 let meta = agent_meta.get(&aid);
202 AgentDetail {
203 agent_id: aid,
204 agent_type: meta.map_or_else(|| "unknown".into(), |m| m.agent_type.clone()),
205 description: meta.map_or_else(|| "".into(), |m| m.description.clone()),
206 turns,
207 output_tokens: output,
208 cost,
209 }
210 })
211 .collect();
212 agents.sort_by(|a, b| {
213 b.cost
214 .partial_cmp(&a.cost)
215 .unwrap_or(std::cmp::Ordering::Equal)
216 });
217 agent_summary.agents = agents;
218
219 let model = model_counts
221 .into_iter()
222 .max_by_key(|(_, count)| *count)
223 .map(|(m, _)| m.to_string())
224 .unwrap_or_default();
225
226 let total_turn_count = session.total_turn_count();
228 let user_prompt_count = session.metadata.user_prompt_count;
229 let autonomy_ratio = if user_prompt_count > 0 {
230 total_turn_count as f64 / user_prompt_count as f64
231 } else {
232 0.0
233 };
234
235 SessionResult {
236 session_id: session.session_id.clone(),
237 project: session
238 .project
239 .clone()
240 .unwrap_or_else(|| "(unknown)".to_string()),
241 turn_details,
242 agent_summary,
243 total_tokens,
244 total_cost,
245 stop_reason_counts,
246 duration_minutes,
247 max_context,
248 compaction_count,
249 cache_write_5m_pct,
250 cache_write_1h_pct,
251 model,
252 title: session.metadata.title.clone(),
254 tags: session.metadata.tags.clone(),
255 mode: session.metadata.mode.clone(),
256 pr_links: session.metadata.pr_links.clone(),
257 user_prompt_count,
259 autonomy_ratio,
260 api_error_count: session.metadata.api_error_count,
262 tool_error_count,
263 truncated_count,
264 speculation_accepts: session.metadata.speculation_accepts,
266 speculation_time_saved_ms: session.metadata.speculation_time_saved_ms,
267 service_tiers,
269 speeds,
270 inference_geos,
271 git_branches,
273 collapse_count: session.metadata.collapse_commits.len(),
275 collapse_summaries: session
276 .metadata
277 .collapse_commits
278 .iter()
279 .map(|c| c.summary.clone())
280 .collect(),
281 collapse_avg_risk: session
282 .metadata
283 .collapse_snapshot
284 .as_ref()
285 .map_or(0.0, |s| s.avg_risk),
286 collapse_max_risk: session
287 .metadata
288 .collapse_snapshot
289 .as_ref()
290 .map_or(0.0, |s| s.max_risk),
291 attribution: session.metadata.attribution.clone(),
293 subagents: build_subagent_summaries(session, calc),
295 plugins: session.plugins.clone(),
296 skills: session.skills.clone(),
297 hooks: session.hooks.clone(),
298 subagent_types: session.subagent_type_aggregates(calc),
299 workflows: build_workflow_summaries(session, calc, claude_home),
300 is_orphan: session.is_orphan,
301 }
302}
303
304fn build_subagent_summaries(
308 session: &SessionData,
309 calc: &PricingCalculator,
310) -> Vec<SubagentSummary> {
311 let mut out: Vec<SubagentSummary> = session
312 .subagents
313 .iter()
314 .map(|sa| {
315 let mut output_tokens: u64 = 0;
316 let mut cost = 0.0f64;
317 for t in &sa.turns {
318 output_tokens += t.usage.output_tokens.unwrap_or(0);
319 cost += calc.calculate_turn_cost(&t.model, &t.usage).total;
320 }
321 SubagentSummary {
322 agent_id: sa.agent_id.clone(),
323 agent_type: sa.agent_type.clone(),
324 description: sa.description.clone(),
325 turns: sa.turns.len(),
326 output_tokens,
327 cost,
328 }
329 })
330 .collect();
331 out.sort_by(|a, b| {
332 b.cost
333 .partial_cmp(&a.cost)
334 .unwrap_or(std::cmp::Ordering::Equal)
335 });
336 out
337}
338
339pub fn build_workflow_summaries(
353 session: &SessionData,
354 calc: &PricingCalculator,
355 claude_home: &Path,
356) -> Vec<WorkflowSummary> {
357 let runs =
358 match cc_session_jsonl::scanner::scan_session_workflows(&session.session_id, claude_home) {
359 Ok(r) => r,
360 Err(_) => return Vec::new(),
361 };
362 if runs.is_empty() {
363 return Vec::new();
364 }
365
366 let mut out: Vec<WorkflowSummary> = runs
367 .into_iter()
368 .map(|run| {
369 let mut parsed_agent_count = 0usize;
372 let mut parsed_turns = 0usize;
373 let mut parsed_output_tokens: u64 = 0;
374 let mut parsed_cost = 0.0f64;
375 for sa in &session.subagents {
376 if sa.workflow_run_id.as_deref() != Some(run.run_id.as_str()) {
377 continue;
378 }
379 parsed_agent_count += 1;
380 parsed_turns += sa.turns.len();
381 for t in &sa.turns {
382 parsed_output_tokens += t.usage.output_tokens.unwrap_or(0);
383 parsed_cost += calc.calculate_turn_cost(&t.model, &t.usage).total;
384 }
385 }
386
387 let snapshot = run.snapshot.as_ref();
388 let phases = snapshot
389 .and_then(|s| s.phases.as_ref())
390 .map(|ps| {
391 ps.iter()
392 .map(|p| WorkflowPhaseSummary {
393 title: p.title.clone(),
394 detail: p.detail.clone(),
395 })
396 .collect()
397 })
398 .unwrap_or_default();
399
400 WorkflowSummary {
401 run_id: run.run_id.clone(),
402 workflow_name: snapshot.and_then(|s| s.workflow_name.clone()),
403 status: snapshot.and_then(|s| s.status.clone()),
404 snapshot_duration_ms: snapshot.and_then(|s| s.duration_ms),
405 snapshot_agent_count: snapshot.and_then(|s| s.agent_count),
406 snapshot_total_tokens: snapshot.and_then(|s| s.total_tokens),
407 phases,
408 parsed_agent_count,
409 parsed_turns,
410 parsed_output_tokens,
411 parsed_cost,
412 }
413 })
414 .collect();
415
416 out.sort_by(|a, b| a.run_id.cmp(&b.run_id));
417 out
418}