Skip to main content

lean_ctx/core/context_kernel/
response_evidence.rs

1//! Response-token evidence recorded at tool output boundaries.
2
3use std::sync::atomic::{AtomicUsize, Ordering};
4
5use super::kernel_config;
6
7static TOTAL_RESPONSES: AtomicUsize = AtomicUsize::new(0);
8static TOTAL_OUTPUT_TOKENS: AtomicUsize = AtomicUsize::new(0);
9static CACHED_RESPONSES: AtomicUsize = AtomicUsize::new(0);
10
11/// Cumulative response evidence observed by the Context Kernel.
12#[derive(Debug, Clone, Default, serde::Serialize)]
13pub struct ResponseSummary {
14    /// Number of responses recorded.
15    pub total_responses: usize,
16    /// Output tokens across all recorded responses.
17    pub total_output_tokens: usize,
18    /// Number of responses served from cache.
19    pub cached_responses: usize,
20    /// Fraction of recorded responses served from cache.
21    pub cache_hit_rate: f64,
22}
23
24/// Records output-token and cache evidence for one tool response.
25pub fn record_response(tool_name: &str, output_tokens: usize, was_cached: bool) {
26    if !kernel_config::is_enabled() {
27        return;
28    }
29    let _ = tool_name;
30    TOTAL_RESPONSES.fetch_add(1, Ordering::Relaxed);
31    TOTAL_OUTPUT_TOKENS.fetch_add(output_tokens, Ordering::Relaxed);
32    if was_cached {
33        CACHED_RESPONSES.fetch_add(1, Ordering::Relaxed);
34    }
35}
36
37/// Returns cumulative response evidence.
38#[must_use]
39pub fn response_summary() -> ResponseSummary {
40    let total_responses = TOTAL_RESPONSES.load(Ordering::Relaxed);
41    let cached_responses = CACHED_RESPONSES.load(Ordering::Relaxed);
42    ResponseSummary {
43        total_responses,
44        total_output_tokens: TOTAL_OUTPUT_TOKENS.load(Ordering::Relaxed),
45        cached_responses,
46        cache_hit_rate: cached_responses as f64 / total_responses.max(1) as f64,
47    }
48}
49
50/// Clears all response evidence counters.
51pub fn reset() {
52    TOTAL_RESPONSES.store(0, Ordering::Relaxed);
53    TOTAL_OUTPUT_TOKENS.store(0, Ordering::Relaxed);
54    CACHED_RESPONSES.store(0, Ordering::Relaxed);
55}
56
57#[cfg(test)]
58mod tests {
59    use super::{record_response, reset, response_summary};
60    use crate::core::context_kernel::kernel_config;
61
62    fn isolated() -> std::sync::MutexGuard<'static, ()> {
63        let guard = kernel_config::KERNEL_TEST_LOCK
64            .lock()
65            .unwrap_or_else(std::sync::PoisonError::into_inner);
66        kernel_config::reset_features();
67        reset();
68        guard
69    }
70
71    #[test]
72    fn records_response() {
73        let _guard = isolated();
74        for _ in 0..3 {
75            record_response("ctx_read", 1, false);
76        }
77        assert_eq!(response_summary().total_responses, 3);
78    }
79
80    #[test]
81    fn tracks_output_tokens() {
82        let _guard = isolated();
83        record_response("ctx_read", 13, false);
84        record_response("ctx_search", 21, false);
85        assert_eq!(response_summary().total_output_tokens, 34);
86    }
87
88    #[test]
89    fn cache_hit_rate() {
90        let _guard = isolated();
91        for was_cached in [true, false, true, false] {
92            record_response("ctx_read", 1, was_cached);
93        }
94        assert_eq!(response_summary().cache_hit_rate, 0.5);
95    }
96
97    #[test]
98    fn disabled_kernel_noop() {
99        let _guard = isolated();
100        let mut features = kernel_config::features();
101        features.enabled = false;
102        kernel_config::update_features(features);
103        record_response("ctx_read", 10, true);
104        let summary = response_summary();
105        assert_eq!(summary.total_responses, 0);
106        assert_eq!(summary.total_output_tokens, 0);
107        assert_eq!(summary.cached_responses, 0);
108    }
109}