lean_ctx/core/context_kernel/
context_broker.rs1use std::cmp::Reverse;
4
5use serde::{Deserialize, Serialize};
6
7use super::client_profile::ClientEfficiencyProfile;
8use super::coverage_class::CoverageClass;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
12pub enum ContextMode {
13 ManifestOnly,
15 SignaturesMap,
17 #[default]
19 RelevantLines,
20 FullText,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
26pub enum OutputFormat {
27 TypedResult,
29 #[default]
31 Summary,
32 Full,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ToolDescriptor {
39 pub name: String,
41 pub schema_tokens: usize,
43 pub priority: u8,
45}
46
47#[derive(Debug, Clone, Copy)]
49pub struct BrokerBudget {
50 pub context_tokens: usize,
52 pub kernel_tokens: usize,
54 pub schema_tokens: usize,
56}
57
58pub struct ContextBroker {
60 profile: ClientEfficiencyProfile,
61}
62
63impl ContextBroker {
64 pub fn new(profile: ClientEfficiencyProfile) -> Self {
66 Self { profile }
67 }
68
69 pub fn select_tools(&self, available: &[ToolDescriptor]) -> Vec<ToolDescriptor> {
71 let mut ranked = available.to_vec();
72 ranked.sort_unstable_by_key(|tool| Reverse(tool.priority));
73
74 let max_tools = self.profile.tool_budget.max_tools;
75 let mut selected = Vec::with_capacity(max_tools.min(ranked.len()));
76 let mut remaining_tokens = self.profile.tool_budget.max_schema_tokens;
77 for tool in ranked {
78 if selected.len() == max_tools {
79 break;
80 }
81 if tool.schema_tokens <= remaining_tokens {
82 remaining_tokens -= tool.schema_tokens;
83 selected.push(tool);
84 }
85 }
86 selected
87 }
88
89 pub fn select_context_mode(&self) -> ContextMode {
91 match self.profile.context_window {
92 128_000.. => ContextMode::FullText,
93 64_000.. => ContextMode::RelevantLines,
94 32_000.. => ContextMode::SignaturesMap,
95 _ => ContextMode::ManifestOnly,
96 }
97 }
98
99 pub fn compute_budget(&self) -> BrokerBudget {
101 let window = self.profile.context_window;
102 BrokerBudget {
103 context_tokens: window.saturating_mul(70) / 100,
104 kernel_tokens: window.saturating_mul(10) / 100,
105 schema_tokens: window.saturating_mul(20) / 100,
106 }
107 }
108
109 pub fn should_use_handles(&self) -> bool {
111 self.profile.context_window < 32_000
112 }
113
114 pub fn select_output_format(&self) -> OutputFormat {
116 match self.profile.coverage {
117 CoverageClass::FullInline => OutputFormat::TypedResult,
118 CoverageClass::ContextControlled => OutputFormat::Summary,
119 CoverageClass::ObserveOnly | CoverageClass::Unmanaged => OutputFormat::Full,
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::{ContextBroker, ContextMode, OutputFormat, ToolDescriptor};
127 use crate::core::context_kernel::client_profile::{ProfileBuilder, ToolBudget};
128 use crate::core::context_kernel::coverage_class::CoverageClass;
129
130 fn broker(
131 context_window: usize,
132 coverage: CoverageClass,
133 max_tools: usize,
134 max_schema_tokens: usize,
135 ) -> ContextBroker {
136 let profile = ProfileBuilder::new("broker-test")
137 .context_window(context_window)
138 .coverage(coverage)
139 .tool_budget(ToolBudget {
140 max_tools,
141 max_schema_tokens,
142 })
143 .build();
144 ContextBroker::new(profile)
145 }
146
147 fn tool(index: usize, priority: u8, schema_tokens: usize) -> ToolDescriptor {
148 ToolDescriptor {
149 name: format!("tool-{index}"),
150 schema_tokens,
151 priority,
152 }
153 }
154
155 #[test]
156 fn select_tools_respects_budget() {
157 let available = (0..20)
158 .map(|index| tool(index, index as u8, 10))
159 .collect::<Vec<_>>();
160 let selected = broker(128_000, CoverageClass::default(), 5, 50).select_tools(&available);
161 assert!(selected.len() <= 5);
162 assert!(
163 selected
164 .iter()
165 .map(|tool| tool.schema_tokens)
166 .sum::<usize>()
167 <= 50
168 );
169 }
170
171 #[test]
172 fn select_tools_by_priority() {
173 let available = vec![tool(0, 1, 10), tool(1, 9, 10), tool(2, 5, 10)];
174 let selected = broker(128_000, CoverageClass::default(), 2, 20).select_tools(&available);
175 assert_eq!(selected[0].priority, 9);
176 assert_eq!(selected[1].priority, 5);
177 }
178
179 #[test]
180 fn context_mode_large_window() {
181 let broker = broker(200_000, CoverageClass::default(), 1, 1);
182 assert_eq!(broker.select_context_mode(), ContextMode::FullText);
183 }
184
185 #[test]
186 fn context_mode_small_window() {
187 let broker = broker(16_000, CoverageClass::default(), 1, 1);
188 assert_eq!(broker.select_context_mode(), ContextMode::ManifestOnly);
189 }
190
191 #[test]
192 fn budget_split_proportional() {
193 let budget = broker(100_000, CoverageClass::default(), 1, 1).compute_budget();
194 assert_eq!(budget.context_tokens, 70_000);
195 assert_eq!(budget.kernel_tokens, 10_000);
196 assert_eq!(budget.schema_tokens, 20_000);
197 }
198
199 #[test]
200 fn handles_for_small_window() {
201 let broker = broker(16_000, CoverageClass::default(), 1, 1);
202 assert!(broker.should_use_handles());
203 }
204
205 #[test]
206 fn output_format_full_inline() {
207 let broker = broker(128_000, CoverageClass::FullInline, 1, 1);
208 assert_eq!(broker.select_output_format(), OutputFormat::TypedResult);
209 }
210}