lean_ctx/core/context_kernel/
schema_wiring.rs1use std::sync::{Mutex, MutexGuard, OnceLock};
4
5use super::coverage_class;
6use super::kernel_config;
7use super::mcp_coverage;
8use super::mcp_schema_opt::{self, SchemaEntry};
9
10const ESTIMATED_TOKENS_PER_PARAMETER: usize = 8;
11
12#[derive(Debug, Clone)]
14pub struct OptimizedToolList {
15 pub tools: Vec<(String, String, usize)>,
17 pub tokens_before: usize,
19 pub tokens_after: usize,
21 pub dropped_count: usize,
23 pub compressed_count: usize,
25 pub optimized: bool,
27}
28
29#[derive(Debug, Clone, Copy, Default)]
31pub struct SchemaSavings {
32 pub optimizations_applied: usize,
34 pub total_tokens_saved: usize,
36 pub avg_compression_ratio: f64,
38}
39
40#[derive(Debug, Default)]
41struct SavingsState {
42 optimizations_applied: usize,
43 total_tokens_before: usize,
44 total_tokens_saved: usize,
45}
46
47static SAVINGS: OnceLock<Mutex<SavingsState>> = OnceLock::new();
48
49fn savings_guard() -> MutexGuard<'static, SavingsState> {
50 SAVINGS
51 .get_or_init(|| Mutex::new(SavingsState::default()))
52 .lock()
53 .unwrap_or_else(std::sync::PoisonError::into_inner)
54}
55
56fn estimated_schema_tokens(name: &str, description: &str, param_count: usize) -> usize {
57 mcp_schema_opt::estimate_tokens(name)
58 .saturating_add(mcp_schema_opt::estimate_tokens(description))
59 .saturating_add(param_count.saturating_mul(ESTIMATED_TOKENS_PER_PARAMETER))
60}
61
62fn unchanged(tools: &[(String, String, usize)]) -> OptimizedToolList {
63 let tokens = tools.iter().fold(0usize, |total, (name, desc, count)| {
64 total.saturating_add(estimated_schema_tokens(name, desc, *count))
65 });
66 OptimizedToolList {
67 tools: tools.to_vec(),
68 tokens_before: tokens,
69 tokens_after: tokens,
70 dropped_count: 0,
71 compressed_count: 0,
72 optimized: false,
73 }
74}
75
76fn optimize_tool_list_with_feature(
77 tools: &[(String, String, usize)],
78 client_name: &str,
79 enabled: bool,
80) -> OptimizedToolList {
81 if !enabled {
82 return unchanged(tools);
83 }
84
85 let coverage = mcp_coverage::detect_mcp_coverage(client_name, false, false);
86 let budget = mcp_schema_opt::budget_for_coverage(coverage);
87 let entries = tools
88 .iter()
89 .map(|(name, description, param_count)| SchemaEntry {
90 name: name.clone(),
91 description: description.clone(),
92 param_count: *param_count,
93 estimated_tokens: estimated_schema_tokens(name, description, *param_count),
94 essential: false,
95 })
96 .collect::<Vec<_>>();
97 let result = mcp_schema_opt::optimize_schemas(&entries, &budget);
98 let tokens_saved = result.tokens_before.saturating_sub(result.tokens_after);
99
100 {
101 let mut savings = savings_guard();
102 savings.optimizations_applied = savings.optimizations_applied.saturating_add(1);
103 savings.total_tokens_before = savings
104 .total_tokens_before
105 .saturating_add(result.tokens_before);
106 savings.total_tokens_saved = savings.total_tokens_saved.saturating_add(tokens_saved);
107 }
108
109 OptimizedToolList {
110 tools: result
111 .entries
112 .into_iter()
113 .map(|entry| (entry.name, entry.description, entry.param_count))
114 .collect(),
115 tokens_before: result.tokens_before,
116 tokens_after: result.tokens_after,
117 dropped_count: result.dropped_count,
118 compressed_count: result.compressed_count,
119 optimized: true,
120 }
121}
122
123#[must_use]
125pub fn optimize_tool_list(
126 tools: &[(String, String, usize)],
127 client_name: &str,
128) -> OptimizedToolList {
129 optimize_tool_list_with_feature(
130 tools,
131 client_name,
132 kernel_config::features().schema_optimization,
133 )
134}
135
136#[must_use]
138pub fn schema_savings() -> SchemaSavings {
139 let savings = savings_guard();
140 let avg_compression_ratio = if savings.total_tokens_before == 0 {
141 0.0
142 } else {
143 savings.total_tokens_saved as f64 / savings.total_tokens_before as f64
144 };
145 SchemaSavings {
146 optimizations_applied: savings.optimizations_applied,
147 total_tokens_saved: savings.total_tokens_saved,
148 avg_compression_ratio,
149 }
150}
151
152fn should_optimize_with_feature(client_name: &str, enabled: bool) -> bool {
153 let coverage = mcp_coverage::detect_mcp_coverage(client_name, false, false);
154 enabled && coverage_class::is_addressable(coverage)
155}
156
157#[must_use]
159pub fn should_optimize(client_name: &str) -> bool {
160 should_optimize_with_feature(client_name, kernel_config::features().schema_optimization)
161}
162
163pub fn reset_schema_state() {
165 *savings_guard() = SavingsState::default();
166}
167
168#[cfg(test)]
169mod tests {
170 use std::sync::MutexGuard;
171
172 use super::{
173 optimize_tool_list_with_feature, reset_schema_state, schema_savings,
174 should_optimize_with_feature,
175 };
176
177 fn setup() -> MutexGuard<'static, ()> {
178 let guard = crate::core::context_kernel::kernel_config::KERNEL_TEST_LOCK
179 .lock()
180 .unwrap_or_else(std::sync::PoisonError::into_inner);
181 reset_schema_state();
182 guard
183 }
184
185 fn tools(description_len: usize) -> Vec<(String, String, usize)> {
186 (0..20)
187 .map(|index| (format!("tool_{index}"), "x".repeat(description_len), 3))
188 .collect()
189 }
190
191 #[test]
192 fn optimize_reduces_tokens() {
193 let _guard = setup();
194 let optimized = optimize_tool_list_with_feature(&tools(4_000), "cursor", true);
195 assert!(optimized.tokens_after < optimized.tokens_before);
196 assert!(optimized.compressed_count > 0);
197 }
198
199 #[test]
200 fn optimize_disabled_returns_unchanged() {
201 let _guard = setup();
202 let original = tools(100);
203 let optimized = optimize_tool_list_with_feature(&original, "cursor", false);
204 assert_eq!(optimized.tools, original);
205 assert!(!optimized.optimized);
206 }
207
208 #[test]
209 fn cursor_gets_full_budget() {
210 let _guard = setup();
211 let optimized = optimize_tool_list_with_feature(&tools(1_000), "cursor", true);
212 assert_eq!(optimized.tools.len(), 20);
213 }
214
215 #[test]
216 fn unknown_client_gets_small_budget() {
217 let _guard = setup();
218 let optimized = optimize_tool_list_with_feature(&tools(1_000), "random", true);
219 assert!(optimized.tools.len() < 20);
220 }
221
222 #[test]
223 fn should_optimize_respects_config() {
224 let _guard = setup();
225 assert!(!should_optimize_with_feature("cursor", false));
226 assert!(should_optimize_with_feature("cursor", true));
227 assert!(!should_optimize_with_feature("random", true));
228 }
229
230 #[test]
231 fn savings_accumulate() {
232 let _guard = setup();
233 let _ = optimize_tool_list_with_feature(&tools(4_000), "cursor", true);
234 let savings = schema_savings();
235 assert_eq!(savings.optimizations_applied, 1);
236 assert!(savings.total_tokens_saved > 0);
237 assert!(savings.avg_compression_ratio > 0.0);
238 }
239}