Skip to main content

agentic_forge_core/metrics/
tokens.rs

1//! Per-call token tracking.
2
3use serde::{Deserialize, Serialize};
4use std::sync::atomic::{AtomicU64, Ordering};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum Layer {
8    Cache,
9    Index,
10    Scoped,
11    Delta,
12    Full,
13}
14
15impl Layer {
16    pub fn number(&self) -> u8 {
17        match self {
18            Self::Cache => 0,
19            Self::Index => 1,
20            Self::Scoped => 2,
21            Self::Delta => 3,
22            Self::Full => 4,
23        }
24    }
25}
26
27pub struct TokenMetrics {
28    pub total: AtomicU64,
29    pub layer0_cache: AtomicU64,
30    pub layer1_index: AtomicU64,
31    pub layer2_scoped: AtomicU64,
32    pub layer3_delta: AtomicU64,
33    pub layer4_full: AtomicU64,
34    pub cache_savings: AtomicU64,
35    pub scope_savings: AtomicU64,
36    pub delta_savings: AtomicU64,
37}
38
39impl TokenMetrics {
40    pub fn new() -> Self {
41        Self {
42            total: AtomicU64::new(0),
43            layer0_cache: AtomicU64::new(0),
44            layer1_index: AtomicU64::new(0),
45            layer2_scoped: AtomicU64::new(0),
46            layer3_delta: AtomicU64::new(0),
47            layer4_full: AtomicU64::new(0),
48            cache_savings: AtomicU64::new(0),
49            scope_savings: AtomicU64::new(0),
50            delta_savings: AtomicU64::new(0),
51        }
52    }
53
54    pub fn record(&self, layer: Layer, tokens: u64, potential: u64) {
55        self.total.fetch_add(tokens, Ordering::Relaxed);
56        match layer {
57            Layer::Cache => {
58                self.layer0_cache.fetch_add(tokens, Ordering::Relaxed);
59            }
60            Layer::Index => {
61                self.layer1_index.fetch_add(tokens, Ordering::Relaxed);
62            }
63            Layer::Scoped => {
64                self.layer2_scoped.fetch_add(tokens, Ordering::Relaxed);
65            }
66            Layer::Delta => {
67                self.layer3_delta.fetch_add(tokens, Ordering::Relaxed);
68            }
69            Layer::Full => {
70                self.layer4_full.fetch_add(tokens, Ordering::Relaxed);
71            }
72        }
73        let saved = potential.saturating_sub(tokens);
74        match layer {
75            Layer::Cache => {
76                self.cache_savings.fetch_add(saved, Ordering::Relaxed);
77            }
78            Layer::Scoped => {
79                self.scope_savings.fetch_add(saved, Ordering::Relaxed);
80            }
81            Layer::Delta => {
82                self.delta_savings.fetch_add(saved, Ordering::Relaxed);
83            }
84            _ => {}
85        }
86    }
87
88    pub fn total_tokens(&self) -> u64 {
89        self.total.load(Ordering::Relaxed)
90    }
91
92    pub fn total_savings(&self) -> u64 {
93        self.cache_savings.load(Ordering::Relaxed)
94            + self.scope_savings.load(Ordering::Relaxed)
95            + self.delta_savings.load(Ordering::Relaxed)
96    }
97
98    pub fn conservation_score(&self) -> f64 {
99        let total = self.total_tokens();
100        let saved = self.total_savings();
101        let potential = total + saved;
102        if potential == 0 {
103            return 1.0;
104        }
105        saved as f64 / potential as f64
106    }
107
108    pub fn reset(&self) {
109        self.total.store(0, Ordering::Relaxed);
110        self.layer0_cache.store(0, Ordering::Relaxed);
111        self.layer1_index.store(0, Ordering::Relaxed);
112        self.layer2_scoped.store(0, Ordering::Relaxed);
113        self.layer3_delta.store(0, Ordering::Relaxed);
114        self.layer4_full.store(0, Ordering::Relaxed);
115        self.cache_savings.store(0, Ordering::Relaxed);
116        self.scope_savings.store(0, Ordering::Relaxed);
117        self.delta_savings.store(0, Ordering::Relaxed);
118    }
119}
120
121impl Default for TokenMetrics {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct ResponseMetrics {
129    pub layer: Layer,
130    pub tokens_used: u64,
131    pub tokens_saved: u64,
132    pub cache_hit: bool,
133    pub response_size: usize,
134}
135
136impl ResponseMetrics {
137    pub fn from_cache(full_cost: u64) -> Self {
138        Self {
139            layer: Layer::Cache,
140            tokens_used: 0,
141            tokens_saved: full_cost,
142            cache_hit: true,
143            response_size: 0,
144        }
145    }
146
147    pub fn from_query(layer: Layer, tokens: u64, full_cost: u64) -> Self {
148        Self {
149            layer,
150            tokens_used: tokens,
151            tokens_saved: full_cost.saturating_sub(tokens),
152            cache_hit: false,
153            response_size: 0,
154        }
155    }
156
157    pub fn full(tokens: u64) -> Self {
158        Self {
159            layer: Layer::Full,
160            tokens_used: tokens,
161            tokens_saved: 0,
162            cache_hit: false,
163            response_size: 0,
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_token_metrics_new() {
174        let m = TokenMetrics::new();
175        assert_eq!(m.total_tokens(), 0);
176        assert_eq!(m.conservation_score(), 1.0);
177    }
178
179    #[test]
180    fn test_token_metrics_record() {
181        let m = TokenMetrics::new();
182        m.record(Layer::Full, 500, 500);
183        assert_eq!(m.total_tokens(), 500);
184
185        m.record(Layer::Cache, 0, 500);
186        assert_eq!(m.total_tokens(), 500);
187        assert_eq!(m.total_savings(), 500);
188    }
189
190    #[test]
191    fn test_conservation_score() {
192        let m = TokenMetrics::new();
193        // Full query: 500 tokens, no savings
194        m.record(Layer::Full, 500, 500);
195        assert_eq!(m.conservation_score(), 0.0);
196
197        // Cache hit: 0 tokens, saved 500
198        m.record(Layer::Cache, 0, 500);
199        // total=500, savings=500, potential=1000
200        assert!((m.conservation_score() - 0.5).abs() < 0.01);
201    }
202
203    #[test]
204    fn test_conservation_score_improves() {
205        let m = TokenMetrics::new();
206
207        // Cold: 10 full queries
208        for _ in 0..10 {
209            m.record(Layer::Full, 500, 500);
210        }
211        let cold = m.conservation_score();
212
213        // Warm: 10 cache hits
214        for _ in 0..10 {
215            m.record(Layer::Cache, 0, 500);
216        }
217        let warm = m.conservation_score();
218
219        assert!(
220            warm > cold,
221            "Conservation should improve: cold={} warm={}",
222            cold,
223            warm
224        );
225    }
226
227    #[test]
228    fn test_scoped_savings() {
229        let m = TokenMetrics::new();
230        m.record(Layer::Scoped, 50, 500);
231        assert_eq!(m.total_tokens(), 50);
232        assert_eq!(m.total_savings(), 450);
233    }
234
235    #[test]
236    fn test_delta_savings() {
237        let m = TokenMetrics::new();
238        m.record(Layer::Delta, 10, 500);
239        assert_eq!(m.total_savings(), 490);
240    }
241
242    #[test]
243    fn test_metrics_reset() {
244        let m = TokenMetrics::new();
245        m.record(Layer::Full, 1000, 1000);
246        m.reset();
247        assert_eq!(m.total_tokens(), 0);
248        assert_eq!(m.total_savings(), 0);
249    }
250
251    #[test]
252    fn test_response_metrics_cache() {
253        let rm = ResponseMetrics::from_cache(500);
254        assert!(rm.cache_hit);
255        assert_eq!(rm.tokens_used, 0);
256        assert_eq!(rm.tokens_saved, 500);
257    }
258
259    #[test]
260    fn test_response_metrics_query() {
261        let rm = ResponseMetrics::from_query(Layer::Scoped, 50, 500);
262        assert!(!rm.cache_hit);
263        assert_eq!(rm.tokens_used, 50);
264        assert_eq!(rm.tokens_saved, 450);
265    }
266
267    #[test]
268    fn test_layer_numbers() {
269        assert_eq!(Layer::Cache.number(), 0);
270        assert_eq!(Layer::Index.number(), 1);
271        assert_eq!(Layer::Scoped.number(), 2);
272        assert_eq!(Layer::Delta.number(), 3);
273        assert_eq!(Layer::Full.number(), 4);
274    }
275}