agentic_forge_core/metrics/
conservation.rs1use super::audit::AuditLog;
4use super::tokens::TokenMetrics;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ConservationReport {
9 pub score: f64,
10 pub total_tokens: u64,
11 pub total_savings: u64,
12 pub cache_hit_rate: f64,
13 pub layer_breakdown: LayerBreakdown,
14 pub verdict: ConservationVerdict,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct LayerBreakdown {
19 pub cache_tokens: u64,
20 pub index_tokens: u64,
21 pub scoped_tokens: u64,
22 pub delta_tokens: u64,
23 pub full_tokens: u64,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27pub enum ConservationVerdict {
28 Excellent, Good, Fair, Poor, Wasteful, }
34
35impl ConservationVerdict {
36 pub fn from_score(score: f64) -> Self {
37 if score >= 0.8 {
38 Self::Excellent
39 } else if score >= 0.6 {
40 Self::Good
41 } else if score >= 0.4 {
42 Self::Fair
43 } else if score >= 0.2 {
44 Self::Poor
45 } else {
46 Self::Wasteful
47 }
48 }
49
50 pub fn name(&self) -> &'static str {
51 match self {
52 Self::Excellent => "excellent",
53 Self::Good => "good",
54 Self::Fair => "fair",
55 Self::Poor => "poor",
56 Self::Wasteful => "wasteful",
57 }
58 }
59}
60
61pub fn generate_report(metrics: &TokenMetrics, audit: &AuditLog) -> ConservationReport {
62 let score = metrics.conservation_score();
63 let total_tokens = metrics.total_tokens();
64 let total_savings = metrics.total_savings();
65 let cache_hit_rate = audit.cache_hit_rate();
66
67 use std::sync::atomic::Ordering;
68 let layer_breakdown = LayerBreakdown {
69 cache_tokens: metrics.layer0_cache.load(Ordering::Relaxed),
70 index_tokens: metrics.layer1_index.load(Ordering::Relaxed),
71 scoped_tokens: metrics.layer2_scoped.load(Ordering::Relaxed),
72 delta_tokens: metrics.layer3_delta.load(Ordering::Relaxed),
73 full_tokens: metrics.layer4_full.load(Ordering::Relaxed),
74 };
75
76 ConservationReport {
77 score,
78 total_tokens,
79 total_savings,
80 cache_hit_rate,
81 layer_breakdown,
82 verdict: ConservationVerdict::from_score(score),
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::super::tokens::Layer;
89 use super::*;
90
91 #[test]
92 fn test_verdict_from_score() {
93 assert_eq!(
94 ConservationVerdict::from_score(0.9),
95 ConservationVerdict::Excellent
96 );
97 assert_eq!(
98 ConservationVerdict::from_score(0.7),
99 ConservationVerdict::Good
100 );
101 assert_eq!(
102 ConservationVerdict::from_score(0.5),
103 ConservationVerdict::Fair
104 );
105 assert_eq!(
106 ConservationVerdict::from_score(0.3),
107 ConservationVerdict::Poor
108 );
109 assert_eq!(
110 ConservationVerdict::from_score(0.1),
111 ConservationVerdict::Wasteful
112 );
113 }
114
115 #[test]
116 fn test_generate_report() {
117 let metrics = TokenMetrics::new();
118 let audit = AuditLog::new(100);
119
120 metrics.record(Layer::Full, 500, 500);
122 metrics.record(Layer::Cache, 0, 500);
123 metrics.record(Layer::Scoped, 50, 500);
124
125 let report = generate_report(&metrics, &audit);
126 assert!(report.score > 0.0);
127 assert_eq!(report.total_tokens, 550);
128 assert!(report.total_savings > 0);
129 }
130
131 #[test]
132 fn test_report_with_warm_cache() {
133 let metrics = TokenMetrics::new();
134 let audit = AuditLog::new(100);
135
136 for _ in 0..5 {
138 metrics.record(Layer::Full, 500, 500);
139 }
140
141 for _ in 0..15 {
143 metrics.record(Layer::Cache, 0, 500);
144 }
145
146 let report = generate_report(&metrics, &audit);
147 assert!(
148 report.score >= 0.7,
149 "Score should be >=0.7 with 75% cache hits: {}",
150 report.score
151 );
152 assert!(
153 report.verdict == ConservationVerdict::Good
154 || report.verdict == ConservationVerdict::Excellent
155 );
156 }
157
158 #[test]
159 fn test_conservation_after_warmup() {
160 let metrics = TokenMetrics::new();
161 let audit = AuditLog::new(100);
162
163 for _ in 0..10 {
165 metrics.record(Layer::Full, 500, 500);
166 }
167 let cold_report = generate_report(&metrics, &audit);
168
169 for _ in 0..10 {
171 metrics.record(Layer::Cache, 0, 500);
172 }
173 let warm_report = generate_report(&metrics, &audit);
174
175 assert!(
176 warm_report.score > cold_report.score,
177 "Warm score ({}) should exceed cold score ({})",
178 warm_report.score,
179 cold_report.score
180 );
181 }
182
183 #[test]
184 fn test_verdict_name() {
185 assert_eq!(ConservationVerdict::Excellent.name(), "excellent");
186 assert_eq!(ConservationVerdict::Wasteful.name(), "wasteful");
187 }
188}