Skip to main content

lean_ctx/core/context_kernel/
context_broker.rs

1//! Client-adaptive selection of tools, context, and output representation.
2
3use std::cmp::Reverse;
4
5use serde::{Deserialize, Serialize};
6
7use super::client_profile::ClientEfficiencyProfile;
8use super::coverage_class::CoverageClass;
9
10/// Detail level used when supplying source context to a client.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
12pub enum ContextMode {
13    /// Supply only a manifest of available context.
14    ManifestOnly,
15    /// Supply symbol signatures and a structural map.
16    SignaturesMap,
17    /// Supply only lines relevant to the request.
18    #[default]
19    RelevantLines,
20    /// Supply complete source text.
21    FullText,
22}
23
24/// Representation used for broker output.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
26pub enum OutputFormat {
27    /// Return a compact typed result.
28    TypedResult,
29    /// Return a natural-language summary.
30    #[default]
31    Summary,
32    /// Return the complete output.
33    Full,
34}
35
36/// Tool metadata used for budget-aware selection.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ToolDescriptor {
39    /// Tool name exposed to the client.
40    pub name: String,
41    /// Tokens consumed by the tool schema.
42    pub schema_tokens: usize,
43    /// Selection priority, where larger values rank first.
44    pub priority: u8,
45}
46
47/// Token allocation computed for a client context window.
48#[derive(Debug, Clone, Copy)]
49pub struct BrokerBudget {
50    /// Tokens allocated to request context.
51    pub context_tokens: usize,
52    /// Tokens allocated to kernel instructions.
53    pub kernel_tokens: usize,
54    /// Tokens allocated to tool schemas.
55    pub schema_tokens: usize,
56}
57
58/// Selects context resources according to client efficiency constraints.
59pub struct ContextBroker {
60    profile: ClientEfficiencyProfile,
61}
62
63impl ContextBroker {
64    /// Creates a broker for a client efficiency profile.
65    pub fn new(profile: ClientEfficiencyProfile) -> Self {
66        Self { profile }
67    }
68
69    /// Selects highest-priority tools within count and schema-token limits.
70    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    /// Selects context detail from the client's context-window size.
90    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    /// Splits the context window 70/10/20 between context, kernel, and schemas.
100    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    /// Returns whether a small context window should use indirect handles.
110    pub fn should_use_handles(&self) -> bool {
111        self.profile.context_window < 32_000
112    }
113
114    /// Selects the output representation supported by the coverage class.
115    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}