1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4pub use cc_session_jsonl::scanner::SessionFile;
11
12#[derive(Debug, Clone, Default, Deserialize)]
16pub struct TokenUsage {
17 pub input_tokens: Option<u64>,
18 pub output_tokens: Option<u64>,
19 pub cache_creation_input_tokens: Option<u64>,
20 pub cache_read_input_tokens: Option<u64>,
21 pub cache_creation: Option<CacheCreationDetail>,
22 pub server_tool_use: Option<ServerToolUse>,
23 pub service_tier: Option<String>,
24 pub speed: Option<String>,
25 pub inference_geo: Option<String>,
26}
27
28#[derive(Debug, Clone, PartialEq, Deserialize)]
30pub struct CacheCreationDetail {
31 pub ephemeral_5m_input_tokens: Option<u64>,
32 pub ephemeral_1h_input_tokens: Option<u64>,
33}
34
35#[derive(Debug, Clone, Deserialize)]
37pub struct ServerToolUse {
38 pub web_search_requests: Option<u64>,
39 pub web_fetch_requests: Option<u64>,
40}
41
42impl From<cc_session_jsonl::types::Usage> for TokenUsage {
45 fn from(u: cc_session_jsonl::types::Usage) -> Self {
46 Self {
47 input_tokens: u.input_tokens,
48 output_tokens: u.output_tokens,
49 cache_creation_input_tokens: u.cache_creation_input_tokens,
50 cache_read_input_tokens: u.cache_read_input_tokens,
51 cache_creation: u.cache_creation.map(|c| CacheCreationDetail {
52 ephemeral_5m_input_tokens: c.ephemeral_5m_input_tokens,
53 ephemeral_1h_input_tokens: c.ephemeral_1h_input_tokens,
54 }),
55 server_tool_use: u.server_tool_use.map(|s| ServerToolUse {
56 web_search_requests: s.web_search_requests,
57 web_fetch_requests: s.web_fetch_requests,
58 }),
59 service_tier: u.service_tier,
60 inference_geo: u.inference_geo,
61 speed: u.speed,
62 }
63 }
64}
65
66#[derive(Debug, Clone)]
70pub struct ValidatedTurn {
71 pub uuid: String,
72 pub request_id: Option<String>,
73 pub timestamp: DateTime<Utc>,
74 pub model: String,
75 pub usage: TokenUsage,
76 pub stop_reason: Option<String>,
77 pub content_types: Vec<String>,
78 pub is_agent: bool,
79 pub agent_id: Option<String>,
80 pub user_text: Option<String>, pub assistant_text: Option<String>, pub tool_names: Vec<String>, pub service_tier: Option<String>,
84 pub speed: Option<String>,
85 pub inference_geo: Option<String>,
86 pub tool_error_count: usize, pub git_branch: Option<String>, pub attribution_plugin: Option<String>,
90 pub attribution_skill: Option<String>,
92}
93
94#[derive(Debug, Clone)]
101pub struct Subagent {
102 pub agent_id: String,
104 pub agent_type: Option<String>,
106 pub description: Option<String>,
108 pub turns: Vec<ValidatedTurn>,
110 pub first_timestamp: Option<DateTime<Utc>>,
111 pub last_timestamp: Option<DateTime<Utc>>,
112 pub workflow_run_id: Option<String>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct PluginUsage {
126 pub plugin: String,
127 pub turns: u64,
128 pub cost: f64,
129 pub input_tokens: u64,
130 pub output_tokens: u64,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct SkillUsage {
140 pub skill: String,
141 pub turns: u64,
142 pub cost: f64,
143 pub input_tokens: u64,
144 pub output_tokens: u64,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct HookUsage {
154 pub command: String,
155 pub invocations: u64,
156 pub total_duration_ms: u64,
157 pub error_count: u64,
158 pub prevented_continuation_count: u64,
159}
160
161#[derive(Debug, Clone)]
163pub struct SessionData {
164 pub session_id: String,
165 pub project: Option<String>,
166 pub turns: Vec<ValidatedTurn>,
167 pub subagents: Vec<Subagent>,
170 pub plugins: Vec<PluginUsage>,
173 pub skills: Vec<SkillUsage>,
176 pub hooks: Vec<HookUsage>,
179 pub first_timestamp: Option<DateTime<Utc>>,
180 pub last_timestamp: Option<DateTime<Utc>>,
181 pub version: Option<String>,
182 pub quality: DataQuality,
183 pub metadata: SessionMetadata,
184 pub is_orphan: bool,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct SubagentTypeAggregate {
204 pub agent_type: String,
205 pub count: u64,
206 pub total_turns: u64,
207 pub total_cost: f64,
208 pub total_input_tokens: u64,
209 pub total_output_tokens: u64,
210 pub descriptions: Vec<String>,
213}
214
215#[derive(Debug, Clone, serde::Serialize)]
219pub struct PrLinkInfo {
220 pub number: u64,
221 pub url: String,
222 pub repository: String,
223}
224
225#[derive(Debug, Clone)]
227pub struct CollapseCommit {
228 pub collapse_id: String,
229 pub summary: String,
230}
231
232#[derive(Debug, Clone)]
234pub struct CollapseSnapshot {
235 pub staged_count: usize,
236 pub avg_risk: f64,
237 pub max_risk: f64,
238 pub armed: bool,
239 pub last_spawn_tokens: u64,
240}
241
242#[derive(Debug, Clone, serde::Serialize)]
244pub struct AttributionData {
245 pub surface: String,
246 pub file_count: usize,
247 pub total_claude_contribution: u64,
248 pub prompt_count: Option<u64>,
249 pub escape_count: Option<u64>,
250 pub permission_prompt_count: Option<u64>,
251}
252
253#[derive(Debug, Default, Clone)]
255pub struct SessionMetadata {
256 pub title: Option<String>, pub tags: Vec<String>,
258 pub mode: Option<String>, pub pr_links: Vec<PrLinkInfo>,
260 pub speculation_accepts: usize,
261 pub speculation_time_saved_ms: f64,
262 pub queue_enqueues: usize,
263 pub queue_dequeues: usize,
264 pub api_error_count: usize, pub user_prompt_count: usize, pub collapse_commits: Vec<CollapseCommit>,
267 pub collapse_snapshot: Option<CollapseSnapshot>,
268 pub attribution: Option<AttributionData>,
269}
270
271impl SessionData {
272 pub fn all_responses(&self) -> Vec<&ValidatedTurn> {
274 let mut all: Vec<&ValidatedTurn> = self
275 .turns
276 .iter()
277 .chain(self.subagents.iter().flat_map(|s| s.turns.iter()))
278 .collect();
279 all.sort_by_key(|r| r.timestamp);
280 all
281 }
282
283 pub fn total_turn_count(&self) -> usize {
285 self.turns.len() + self.subagents.iter().map(|s| s.turns.len()).sum::<usize>()
286 }
287
288 pub fn agent_turn_count(&self) -> usize {
290 self.subagents.iter().map(|s| s.turns.len()).sum::<usize>()
291 }
292
293 pub fn subagent_type_aggregates(
304 &self,
305 calc: &crate::pricing::calculator::PricingCalculator,
306 ) -> Vec<SubagentTypeAggregate> {
307 use std::collections::BTreeMap;
308
309 let mut sorted: Vec<&Subagent> = self.subagents.iter().collect();
311 sorted.sort_by(|a, b| a.agent_id.cmp(&b.agent_id));
312
313 let mut acc: BTreeMap<String, SubagentTypeAggregate> = BTreeMap::new();
314 for sa in sorted {
315 let key = sa
316 .agent_type
317 .clone()
318 .filter(|s| !s.is_empty())
319 .unwrap_or_else(|| "unknown".to_string());
320 let mut sa_input: u64 = 0;
321 let mut sa_output: u64 = 0;
322 let mut sa_cost: f64 = 0.0;
323 for t in &sa.turns {
324 sa_input += t.usage.input_tokens.unwrap_or(0);
325 sa_output += t.usage.output_tokens.unwrap_or(0);
326 sa_cost += calc.calculate_turn_cost(&t.model, &t.usage).total;
327 }
328 let entry = acc
329 .entry(key.clone())
330 .or_insert_with(|| SubagentTypeAggregate {
331 agent_type: key,
332 count: 0,
333 total_turns: 0,
334 total_cost: 0.0,
335 total_input_tokens: 0,
336 total_output_tokens: 0,
337 descriptions: Vec::new(),
338 });
339 entry.count += 1;
340 entry.total_turns += sa.turns.len() as u64;
341 entry.total_cost += sa_cost;
342 entry.total_input_tokens += sa_input;
343 entry.total_output_tokens += sa_output;
344 if let Some(desc) = sa.description.as_ref().filter(|d| !d.is_empty()) {
345 entry.descriptions.push(desc.clone());
346 }
347 }
348 acc.into_values().collect()
349 }
350}
351
352#[derive(Debug, Default, Clone)]
354pub struct DataQuality {
355 pub total_lines: usize,
356 pub valid_turns: usize,
357 pub skipped_synthetic: usize,
358 pub skipped_sidechain: usize,
359 pub skipped_invalid: usize,
360 pub skipped_parse_error: usize,
361 pub duplicate_turns: usize,
362}
363
364#[derive(Debug, Default, Clone, Serialize)]
366pub struct GlobalDataQuality {
367 pub total_session_files: usize,
368 pub total_agent_files: usize,
369 pub orphan_agents: usize,
370 pub total_valid_turns: usize,
371 pub total_skipped: usize,
372 pub time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
373}
374
375#[cfg(test)]
378mod tests {
379 use super::*;
380
381 #[test]
382 fn test_parse_assistant_message() {
383 let json = r#"{"parentUuid":"abc","isSidechain":false,"type":"assistant","uuid":"def","timestamp":"2026-03-16T13:51:35.912Z","message":{"model":"claude-opus-4-6","role":"assistant","stop_reason":"end_turn","usage":{"input_tokens":3,"cache_creation_input_tokens":1281,"cache_read_input_tokens":15204,"cache_creation":{"ephemeral_5m_input_tokens":1281,"ephemeral_1h_input_tokens":0},"output_tokens":108,"service_tier":"standard"},"content":[{"type":"text","text":"Hello"}]},"sessionId":"abc-123","version":"2.0.77","cwd":"/tmp","gitBranch":"main","userType":"external","requestId":"req_1"}"#;
384
385 let entry: cc_session_jsonl::types::Entry = serde_json::from_str(json).unwrap();
386
387 match entry {
388 cc_session_jsonl::types::Entry::Assistant(msg) => {
389 assert_eq!(msg.uuid.as_deref(), Some("def"));
390 assert_eq!(msg.session_id.as_deref(), Some("abc-123"));
391 assert_eq!(msg.request_id.as_deref(), Some("req_1"));
392 assert_eq!(msg.parent_uuid.as_deref(), Some("abc"));
393 assert_eq!(msg.is_sidechain, Some(false));
394
395 let api = msg.message.unwrap();
396 assert_eq!(api.model.as_deref(), Some("claude-opus-4-6"));
397 assert_eq!(api.stop_reason.as_deref(), Some("end_turn"));
398
399 let usage: TokenUsage = api.usage.unwrap().into();
400 assert_eq!(usage.input_tokens, Some(3));
401 assert_eq!(usage.output_tokens, Some(108));
402 assert_eq!(usage.cache_creation_input_tokens, Some(1281));
403 assert_eq!(usage.cache_read_input_tokens, Some(15204));
404 assert_eq!(usage.service_tier.as_deref(), Some("standard"));
405
406 let cache = usage.cache_creation.unwrap();
407 assert_eq!(cache.ephemeral_5m_input_tokens, Some(1281));
408 assert_eq!(cache.ephemeral_1h_input_tokens, Some(0));
409
410 let content = api.content.unwrap();
411 assert_eq!(content.len(), 1);
412 match &content[0] {
413 cc_session_jsonl::types::ContentBlock::Text { text } => {
414 assert_eq!(text.as_deref(), Some("Hello"));
415 }
416 _ => panic!("expected Text content block"),
417 }
418 }
419 _ => panic!("expected Assistant variant"),
420 }
421 }
422
423 #[test]
424 fn test_parse_user_message() {
425 let json = r#"{"parentUuid":null,"isSidechain":false,"type":"user","message":{"role":"user","content":[{"type":"text","text":"hello"}]},"uuid":"u1","timestamp":"2026-03-16T13:51:19.053Z","sessionId":"s1","version":"2.1.80","cwd":"/tmp","gitBranch":"main","userType":"external"}"#;
426
427 let entry: cc_session_jsonl::types::Entry = serde_json::from_str(json).unwrap();
428
429 match entry {
430 cc_session_jsonl::types::Entry::User(msg) => {
431 assert_eq!(msg.uuid.as_deref(), Some("u1"));
432 assert_eq!(msg.session_id.as_deref(), Some("s1"));
433 assert_eq!(msg.version.as_deref(), Some("2.1.80"));
434 assert_eq!(msg.cwd.as_deref(), Some("/tmp"));
435 assert_eq!(msg.git_branch.as_deref(), Some("main"));
436 assert!(msg.parent_uuid.is_none());
437 }
438 _ => panic!("expected User variant"),
439 }
440 }
441
442 #[test]
443 fn test_parse_queue_operation() {
444 let json = r#"{"type":"queue-operation","operation":"dequeue","timestamp":"2026-03-16T13:51:19.041Z","sessionId":"abc"}"#;
445
446 let entry: cc_session_jsonl::types::Entry = serde_json::from_str(json).unwrap();
447
448 match entry {
449 cc_session_jsonl::types::Entry::QueueOperation(val) => {
450 assert_eq!(val.operation.as_deref(), Some("dequeue"));
451 assert_eq!(val.session_id.as_deref(), Some("abc"));
452 }
453 _ => panic!("expected QueueOperation variant"),
454 }
455 }
456
457 #[test]
458 fn test_parse_progress_entry() {
459 let json = r#"{"type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"toolUseID":"toolu_01","parentToolUseID":"toolu_01","uuid":"u1","timestamp":"2026-03-16T13:51:19.053Z","sessionId":"s1"}"#;
460 let entry: cc_session_jsonl::types::Entry = serde_json::from_str(json).unwrap();
461 match entry {
462 cc_session_jsonl::types::Entry::Progress(p) => {
463 assert_eq!(p.tool_use_id.as_deref(), Some("toolu_01"));
464 match p.data.unwrap() {
465 cc_session_jsonl::types::ProgressData::HookProgress {
466 hook_event,
467 hook_name,
468 command,
469 } => {
470 assert_eq!(hook_event.as_deref(), Some("PostToolUse"));
471 assert_eq!(hook_name.as_deref(), Some("PostToolUse:Read"));
472 assert_eq!(command.as_deref(), Some("callback"));
473 }
474 other => panic!("expected HookProgress, got {other:?}"),
475 }
476 }
477 other => panic!("expected Progress, got {other:?}"),
478 }
479 }
480
481 #[test]
482 fn test_parse_system_entry() {
483 let json = r#"{"type":"system","subtype":"turn_duration","durationMs":1234,"uuid":"u1","timestamp":"2026-03-16T13:51:19.053Z","sessionId":"s1"}"#;
484 let entry: cc_session_jsonl::types::Entry = serde_json::from_str(json).unwrap();
485 assert!(matches!(entry, cc_session_jsonl::types::Entry::System(_)));
486 }
487
488 #[test]
489 fn test_parse_unknown_entry_type() {
490 let json = r#"{"type":"some-future-type","data":"whatever","uuid":"u1","timestamp":"2026-03-16T13:51:19.053Z"}"#;
491 let entry: cc_session_jsonl::types::Entry = serde_json::from_str(json).unwrap();
492 assert!(matches!(entry, cc_session_jsonl::types::Entry::Unknown));
493 }
494
495 #[test]
496 fn test_parse_thinking_content_block() {
497 let json = r#"{"type":"assistant","uuid":"u1","timestamp":"2026-03-16T10:00:00Z","message":{"model":"claude-opus-4-6","role":"assistant","stop_reason":"end_turn","usage":{"input_tokens":3,"output_tokens":100,"cache_creation_input_tokens":500,"cache_read_input_tokens":10000},"content":[{"type":"thinking","thinking":"Let me analyze this...","signature":"abc123"},{"type":"text","text":"Here is my answer."}]},"sessionId":"s1","cwd":"/tmp","gitBranch":"","userType":"external","isSidechain":false,"parentUuid":null,"requestId":"r1"}"#;
498 let entry: cc_session_jsonl::types::Entry = serde_json::from_str(json).unwrap();
499 match entry {
500 cc_session_jsonl::types::Entry::Assistant(msg) => {
501 let content = msg.message.unwrap().content.unwrap();
502 assert_eq!(content.len(), 2);
503 assert!(
504 matches!(&content[0], cc_session_jsonl::types::ContentBlock::Thinking { thinking: Some(t), .. } if t.contains("analyze"))
505 );
506 assert!(matches!(
507 &content[1],
508 cc_session_jsonl::types::ContentBlock::Text { .. }
509 ));
510 }
511 _ => panic!("expected Assistant variant"),
512 }
513 }
514
515 #[test]
516 fn test_parse_synthetic_message() {
517 let json = r#"{"type":"assistant","uuid":"x","timestamp":"2026-03-16T00:00:00Z","message":{"model":"<synthetic>","role":"assistant","stop_reason":"stop_sequence","usage":{"input_tokens":0,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0},"content":[{"type":"text","text":"error"}]},"sessionId":"s1","cwd":"/tmp","gitBranch":"","userType":"external","isSidechain":false,"parentUuid":null}"#;
518
519 let entry: cc_session_jsonl::types::Entry = serde_json::from_str(json).unwrap();
520
521 match entry {
522 cc_session_jsonl::types::Entry::Assistant(msg) => {
523 let api = msg.message.unwrap();
524 assert_eq!(api.model.as_deref(), Some("<synthetic>"));
525 assert_eq!(api.stop_reason.as_deref(), Some("stop_sequence"));
526
527 let usage: TokenUsage = api.usage.unwrap().into();
528 assert_eq!(usage.input_tokens, Some(0));
529 assert_eq!(usage.output_tokens, Some(0));
530
531 assert!(usage.cache_creation.is_none());
533 }
534 _ => panic!("expected Assistant variant"),
535 }
536 }
537
538 #[test]
539 fn test_token_usage_from_conversion() {
540 let lib_usage = cc_session_jsonl::types::Usage {
541 input_tokens: Some(100),
542 output_tokens: Some(200),
543 cache_creation_input_tokens: Some(50),
544 cache_read_input_tokens: Some(300),
545 cache_creation: Some(cc_session_jsonl::types::CacheCreation {
546 ephemeral_5m_input_tokens: Some(30),
547 ephemeral_1h_input_tokens: Some(20),
548 }),
549 server_tool_use: Some(cc_session_jsonl::types::ServerToolUse {
550 web_search_requests: Some(2),
551 web_fetch_requests: Some(1),
552 }),
553 service_tier: Some("standard".into()),
554 inference_geo: Some("us".into()), iterations: None, speed: Some("fast".into()),
557 };
558
559 let local: TokenUsage = lib_usage.into();
560 assert_eq!(local.input_tokens, Some(100));
561 assert_eq!(local.output_tokens, Some(200));
562 assert_eq!(local.cache_creation_input_tokens, Some(50));
563 assert_eq!(local.cache_read_input_tokens, Some(300));
564 assert_eq!(local.service_tier.as_deref(), Some("standard"));
565 assert_eq!(local.speed.as_deref(), Some("fast"));
566
567 let cache = local.cache_creation.unwrap();
568 assert_eq!(cache.ephemeral_5m_input_tokens, Some(30));
569 assert_eq!(cache.ephemeral_1h_input_tokens, Some(20));
570
571 let stu = local.server_tool_use.unwrap();
572 assert_eq!(stu.web_search_requests, Some(2));
573 assert_eq!(stu.web_fetch_requests, Some(1));
574 }
575}