lean_ctx/core/context_kernel/
evidence_hook.rs1#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)]
5pub struct EvidenceReport {
6 pub tool_calls: usize,
8 pub proxy_calls: usize,
10 pub total_input_tokens: usize,
12 pub total_output_tokens: usize,
14 pub total_tokens_saved: usize,
16 pub evidence_chain_entries: usize,
18}
19
20pub 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
36pub 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#[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
88pub 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}