agtrace_engine/session/
extensions.rs

1//! Extension traits for session analysis.
2//!
3//! These traits provide complex computation logic for session types
4//! defined in agtrace-types, keeping the types crate lightweight and
5//! focused on data structures while engine handles analysis logic.
6
7use super::types::{AgentSession, AgentTurn, TurnMetrics};
8
9/// Detect if context was compacted by analyzing origin and token drop.
10///
11/// Context compaction is detected when:
12/// 1. Turn origin is ContextCompaction (authoritative), OR
13/// 2. Tokens dropped by more than 50% (fallback for backward compatibility)
14fn detect_context_compaction(turn: &AgentTurn, current_tokens: u32, prev_cumulative: u32) -> bool {
15    // Primary: origin-based detection (authoritative)
16    if turn.user.origin.is_context_compaction() {
17        return true;
18    }
19
20    // Fallback: token-based detection for backward compatibility
21    // (handles data assembled before origin was added)
22    prev_cumulative > 0 && current_tokens < prev_cumulative / 2
23}
24
25/// Extension trait for `AgentSession` providing analysis and metrics computation.
26pub trait SessionAnalysisExt {
27    /// Compute presentation metrics for all turns
28    fn compute_turn_metrics(&self, max_context: Option<u32>) -> Vec<TurnMetrics>;
29}
30
31impl SessionAnalysisExt for AgentSession {
32    fn compute_turn_metrics(&self, max_context: Option<u32>) -> Vec<TurnMetrics> {
33        let mut cumulative_total = 0u32;
34        let mut metrics = Vec::new();
35        let total_turns = self.turns.len();
36
37        for (idx, turn) in self.turns.iter().enumerate() {
38            let turn_end_tokens = turn.cumulative_total_tokens(cumulative_total);
39
40            // Detect if context was compacted (reset) during this turn
41            let context_compacted =
42                detect_context_compaction(turn, turn_end_tokens, cumulative_total);
43
44            // Calculate prev_total, delta, and compaction_from based on compaction state
45            let (prev_total, delta, compaction_from) = if context_compacted {
46                // Context was reset: prev=0, delta=new baseline
47                // Store the previous cumulative to show reduction (e.g., 150k → 30k)
48                (0, turn_end_tokens, Some(cumulative_total))
49            } else {
50                // Normal case: additive
51                let delta = turn_end_tokens.saturating_sub(cumulative_total);
52                (cumulative_total, delta, None)
53            };
54
55            // Simplified: Last turn is always considered active for live watching
56            // This avoids flickering caused by per-step status checks
57            let is_active = idx == total_turns - 1;
58
59            metrics.push(TurnMetrics {
60                turn_index: idx,
61                prev_total,
62                delta,
63                is_heavy: TurnMetrics::is_delta_heavy(delta, max_context),
64                is_active,
65                context_compacted,
66                cumulative_total: turn_end_tokens,
67                compaction_from,
68            });
69
70            cumulative_total = turn_end_tokens;
71        }
72
73        metrics
74    }
75}