Skip to main content

lean_ctx/core/context_kernel/
evidence_hook.rs

1//! Thin evidence-recording hooks for tool and proxy hot paths.
2
3/// Unified evidence totals across tool and proxy calls.
4#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)]
5pub struct EvidenceReport {
6    /// Number of recorded MCP tool calls.
7    pub tool_calls: usize,
8    /// Number of recorded proxy calls.
9    pub proxy_calls: usize,
10    /// Input tokens across all recorded calls.
11    pub total_input_tokens: usize,
12    /// Output tokens across all recorded calls.
13    pub total_output_tokens: usize,
14    /// Tokens saved across all recorded calls.
15    pub total_tokens_saved: usize,
16    /// Number of entries in the evidence receipt chain.
17    pub evidence_chain_entries: usize,
18}
19
20/// Records evidence for one MCP tool call when the kernel is enabled.
21pub fn record_tool_call(tool_name: &str, input_tokens: usize, output_tokens: usize) {
22    if !super::kernel_config::is_enabled() {
23        return;
24    }
25    let data = super::mcp_bridge::McpCallData {
26        tool_name: tool_name.to_owned(),
27        input_tokens,
28        output_tokens,
29        is_retry: false,
30        call_number: 1,
31    };
32    super::mcp_bridge::record_mcp_call(&data);
33    super::envelope_wiring::process_mcp_evidence(&data);
34}
35
36/// Records evidence for one proxy-forwarded request when the kernel is enabled.
37pub fn record_proxy_call(
38    input_tokens: usize,
39    output_tokens: usize,
40    tokens_saved: usize,
41    model: Option<&str>,
42    provider: Option<&str>,
43) {
44    if !super::kernel_config::is_enabled() {
45        return;
46    }
47    let data = super::proxy_bridge::ProxyRequestData {
48        input_tokens,
49        output_tokens,
50        tokens_saved,
51        model: model.map(str::to_owned),
52        provider: provider.map(str::to_owned),
53        request_count: 1,
54        ..Default::default()
55    };
56    let result = super::proxy_bridge::process_proxy_request(&data);
57    super::envelope_wiring::process_proxy_evidence(&data, &result);
58}
59
60/// Returns unified evidence totals for the current process.
61#[must_use]
62pub fn evidence_report() -> EvidenceReport {
63    let evidence = super::envelope_wiring::evidence_summary();
64    let mcp = super::mcp_bridge::mcp_summary();
65    let proxy = super::proxy_bridge::identity_summary();
66    let usage = super::usage_normalizer::session_usage();
67    let (total_input_tokens, total_output_tokens) =
68        usage
69            .per_model
70            .values()
71            .fold((0_usize, 0_usize), |totals, entry| {
72                (
73                    totals.0.saturating_add(entry.input_tokens),
74                    totals.1.saturating_add(entry.output_tokens),
75                )
76            });
77
78    EvidenceReport {
79        tool_calls: mcp.total_calls,
80        proxy_calls: evidence.proxy_requests,
81        total_input_tokens,
82        total_output_tokens,
83        total_tokens_saved: proxy.total_savings,
84        evidence_chain_entries: evidence.chain_entries,
85    }
86}
87
88/// Clears all tool, proxy, and evidence-pipeline state.
89pub fn reset() {
90    super::envelope_wiring::reset_evidence();
91    super::proxy_bridge::reset_state();
92    super::mcp_bridge::reset_mcp_state();
93}
94
95#[cfg(test)]
96mod tests {
97    use super::{EvidenceReport, evidence_report, record_proxy_call, record_tool_call, reset};
98
99    fn isolated() -> std::sync::MutexGuard<'static, ()> {
100        let guard = super::super::kernel_config::KERNEL_TEST_LOCK
101            .lock()
102            .unwrap_or_else(std::sync::PoisonError::into_inner);
103        super::super::kernel_config::reset_features();
104        super::super::envelope_wiring::reset_evidence();
105        super::super::proxy_bridge::reset_state();
106        super::super::mcp_bridge::reset_mcp_state();
107        guard
108    }
109
110    #[test]
111    fn tool_call_records_evidence() {
112        let _guard = isolated();
113        for _ in 0..3 {
114            record_tool_call("ctx_read", 10, 4);
115        }
116        let report = evidence_report();
117        assert!(report.tool_calls >= 3);
118    }
119
120    #[test]
121    fn proxy_call_records_evidence() {
122        let _guard = isolated();
123        for _ in 0..2 {
124            record_proxy_call(20, 5, 8, Some("gpt-test"), Some("openai"));
125        }
126        let report = evidence_report();
127        assert!(report.proxy_calls >= 2);
128    }
129
130    #[test]
131    fn disabled_kernel_no_evidence() {
132        let _guard = isolated();
133        let mut features = super::super::kernel_config::features();
134        features.enabled = false;
135        super::super::kernel_config::update_features(features);
136        record_tool_call("ctx_read", 10, 4);
137        record_proxy_call(20, 5, 8, None, None);
138        assert_eq!(evidence_report(), EvidenceReport::default());
139    }
140
141    #[test]
142    fn reset_clears_state() {
143        let _guard = isolated();
144        record_tool_call("ctx_read", 10, 4);
145        record_proxy_call(20, 5, 8, None, None);
146        reset();
147        assert_eq!(evidence_report(), EvidenceReport::default());
148    }
149}