Skip to main content

agentic_evolve_core/metrics/
conservation.rs

1//! Conservation report — aggregates metrics into a verdict on token efficiency.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use super::audit::AuditLog;
7use super::tokens::TokenMetrics;
8
9/// Overall verdict on conservation effectiveness.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub enum ConservationVerdict {
12    /// Conservation score >= 0.9 — outstanding efficiency.
13    Excellent,
14    /// Conservation score >= 0.7 — target met.
15    Good,
16    /// Conservation score >= 0.5 — acceptable but improvable.
17    Fair,
18    /// Conservation score >= 0.3 — significant room for improvement.
19    Poor,
20    /// Conservation score < 0.3 — conservation is not working.
21    Wasteful,
22}
23
24impl ConservationVerdict {
25    /// Derive a verdict from a conservation score in `[0.0, 1.0]`.
26    pub fn from_score(score: f64) -> Self {
27        if score >= 0.9 {
28            Self::Excellent
29        } else if score >= 0.7 {
30            Self::Good
31        } else if score >= 0.5 {
32            Self::Fair
33        } else if score >= 0.3 {
34            Self::Poor
35        } else {
36            Self::Wasteful
37        }
38    }
39
40    /// Whether this verdict meets the 0.7 conservation target.
41    pub fn meets_target(&self) -> bool {
42        matches!(self, Self::Excellent | Self::Good)
43    }
44}
45
46/// A comprehensive conservation report.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ConservationReport {
49    /// When this report was generated.
50    pub generated_at: DateTime<Utc>,
51    /// Overall conservation score `[0.0, 1.0]`.
52    pub conservation_score: f64,
53    /// The verdict.
54    pub verdict: ConservationVerdict,
55    /// Total tokens used.
56    pub total_tokens_used: u64,
57    /// Total tokens saved.
58    pub total_tokens_saved: u64,
59    /// Cache hit rate.
60    pub cache_hit_rate: f64,
61    /// Number of queries analyzed.
62    pub query_count: usize,
63    /// Whether the 0.7 target is met.
64    pub target_met: bool,
65}
66
67/// Generate a conservation report from the current metrics and audit log.
68pub fn generate_report(metrics: &TokenMetrics, audit: &AuditLog) -> ConservationReport {
69    let score = metrics.conservation_score();
70    let verdict = ConservationVerdict::from_score(score);
71
72    ConservationReport {
73        generated_at: Utc::now(),
74        conservation_score: score,
75        verdict,
76        total_tokens_used: metrics.total_used(),
77        total_tokens_saved: metrics.total_saved(),
78        cache_hit_rate: audit.cache_hit_rate(),
79        query_count: audit.len(),
80        target_met: verdict.meets_target(),
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::metrics::tokens::Layer;
88
89    #[test]
90    fn verdict_from_score_excellent() {
91        assert_eq!(
92            ConservationVerdict::from_score(0.95),
93            ConservationVerdict::Excellent
94        );
95    }
96
97    #[test]
98    fn verdict_from_score_good() {
99        assert_eq!(
100            ConservationVerdict::from_score(0.75),
101            ConservationVerdict::Good
102        );
103    }
104
105    #[test]
106    fn verdict_from_score_fair() {
107        assert_eq!(
108            ConservationVerdict::from_score(0.55),
109            ConservationVerdict::Fair
110        );
111    }
112
113    #[test]
114    fn verdict_from_score_poor() {
115        assert_eq!(
116            ConservationVerdict::from_score(0.35),
117            ConservationVerdict::Poor
118        );
119    }
120
121    #[test]
122    fn verdict_from_score_wasteful() {
123        assert_eq!(
124            ConservationVerdict::from_score(0.1),
125            ConservationVerdict::Wasteful
126        );
127    }
128
129    #[test]
130    fn meets_target_only_good_and_excellent() {
131        assert!(ConservationVerdict::Excellent.meets_target());
132        assert!(ConservationVerdict::Good.meets_target());
133        assert!(!ConservationVerdict::Fair.meets_target());
134        assert!(!ConservationVerdict::Poor.meets_target());
135        assert!(!ConservationVerdict::Wasteful.meets_target());
136    }
137
138    #[test]
139    fn generate_report_empty() {
140        let metrics = TokenMetrics::new();
141        let audit = AuditLog::new();
142        let report = generate_report(&metrics, &audit);
143        assert_eq!(report.conservation_score, 0.0);
144        assert_eq!(report.verdict, ConservationVerdict::Wasteful);
145        assert_eq!(report.query_count, 0);
146        assert!(!report.target_met);
147    }
148
149    #[test]
150    fn generate_report_with_savings() {
151        let metrics = TokenMetrics::new();
152        // 3 cache hits saving 100 each, 1 full costing 100
153        metrics.record(Layer::Cache, 0, 100);
154        metrics.record(Layer::Cache, 0, 100);
155        metrics.record(Layer::Cache, 0, 100);
156        metrics.record(Layer::Full, 100, 100);
157
158        let audit = AuditLog::new();
159        let report = generate_report(&metrics, &audit);
160        // saved 300, used 100, potential 400 => score = 0.75
161        assert!((report.conservation_score - 0.75).abs() < f64::EPSILON);
162        assert_eq!(report.verdict, ConservationVerdict::Good);
163        assert!(report.target_met);
164    }
165}