agentic_forge_core/metrics/
tokens.rs1use 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 m.record(Layer::Full, 500, 500);
195 assert_eq!(m.conservation_score(), 0.0);
196
197 m.record(Layer::Cache, 0, 500);
199 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 for _ in 0..10 {
209 m.record(Layer::Full, 500, 500);
210 }
211 let cold = m.conservation_score();
212
213 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}