Skip to main content

lean_ctx/server/
dynamic_tools.rs

1use std::collections::HashSet;
2use std::sync::{Mutex, OnceLock};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum ToolCategory {
6    Core,
7    Internal,
8    Arch,
9    Debug,
10    Memory,
11    Metrics,
12    Session,
13}
14
15impl ToolCategory {
16    pub fn parse(s: &str) -> Option<Self> {
17        match s {
18            "core" => Some(Self::Core),
19            "arch" | "architecture" => Some(Self::Arch),
20            "debug" | "profiling" => Some(Self::Debug),
21            "memory" | "semantic" => Some(Self::Memory),
22            "metrics" | "stats" => Some(Self::Metrics),
23            "session" => Some(Self::Session),
24            _ => None,
25        }
26    }
27
28    pub fn as_str(&self) -> &'static str {
29        match self {
30            Self::Core => "core",
31            Self::Internal => "internal",
32            Self::Arch => "arch",
33            Self::Debug => "debug",
34            Self::Memory => "memory",
35            Self::Metrics => "metrics",
36            Self::Session => "session",
37        }
38    }
39}
40
41#[allow(clippy::match_same_arms)]
42pub fn categorize_tool(name: &str) -> ToolCategory {
43    match name {
44        // Internal: meta/self-referential + automated mechanisms (never exposed)
45        "ctx_metrics"
46        | "ctx_cost"
47        | "ctx_gain"
48        | "ctx_radar"
49        | "ctx_heatmap"
50        | "ctx_feedback"
51        | "ctx_intent"
52        | "ctx_response"
53        | "ctx_discover"
54        | "ctx_discover_tools"
55        | "ctx_load_tools"
56        | "ctx_dedup"
57        | "ctx_preload"
58        | "ctx_prefetch"
59        | "ctx_compress_memory" => ToolCategory::Internal,
60
61        // Core: always visible
62        "ctx_read" | "ctx_search" | "ctx_shell" | "ctx_tree" | "ctx_edit" | "ctx_session"
63        | "ctx_knowledge" | "ctx_overview" | "ctx_graph" | "ctx_call" | "ctx_compress"
64        | "ctx_cache" | "ctx_retrieve" => ToolCategory::Core,
65
66        // Merged tools (redirects in registry, treated as Core for backward compat)
67        "ctx_multi_read" | "ctx_smart_read" | "ctx_delta" | "ctx_outline" | "ctx_context" => {
68            ToolCategory::Core
69        }
70
71        // Arch: on-demand architecture analysis
72        "ctx_architecture" | "ctx_impact" | "ctx_callgraph" | "ctx_refactor" | "ctx_symbol"
73        | "ctx_routes" | "ctx_smells" | "ctx_index" => ToolCategory::Arch,
74
75        // Debug/Verify: on-demand quality analysis
76        "ctx_benchmark" | "ctx_verify" | "ctx_analyze" | "ctx_profile" | "ctx_proof"
77        | "ctx_review" => ToolCategory::Debug,
78
79        // Memory: on-demand semantic + provider tools
80        "ctx_semantic_search" | "ctx_provider" | "ctx_artifacts" => ToolCategory::Memory,
81
82        // Batch: on-demand batch/PR/sandbox tools
83        "ctx_fill" | "ctx_execute" | "ctx_expand" | "ctx_pack" | "ctx_plan" | "ctx_control"
84        | "ctx_compile" => ToolCategory::Metrics,
85
86        // Multi-agent: on-demand collaboration
87        "ctx_agent" | "ctx_share" | "ctx_task" | "ctx_handoff" | "ctx_workflow" => {
88            ToolCategory::Session
89        }
90
91        _ => ToolCategory::Core,
92    }
93}
94
95#[derive(Debug)]
96pub struct DynamicToolState {
97    active_categories: HashSet<ToolCategory>,
98    supports_list_changed: bool,
99}
100
101impl Default for DynamicToolState {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107impl DynamicToolState {
108    pub fn new() -> Self {
109        let mut active = HashSet::new();
110        active.insert(ToolCategory::Core);
111        active.insert(ToolCategory::Session);
112        Self {
113            active_categories: active,
114            supports_list_changed: false,
115        }
116    }
117
118    pub fn all_enabled() -> Self {
119        let mut active = HashSet::new();
120        active.insert(ToolCategory::Core);
121        active.insert(ToolCategory::Arch);
122        active.insert(ToolCategory::Debug);
123        active.insert(ToolCategory::Memory);
124        active.insert(ToolCategory::Metrics);
125        active.insert(ToolCategory::Session);
126        Self {
127            active_categories: active,
128            supports_list_changed: false,
129        }
130    }
131
132    pub fn set_supports_list_changed(&mut self, val: bool) {
133        self.supports_list_changed = val;
134    }
135
136    pub fn supports_list_changed(&self) -> bool {
137        self.supports_list_changed
138    }
139
140    pub fn load_category(&mut self, cat: ToolCategory) -> bool {
141        self.active_categories.insert(cat)
142    }
143
144    pub fn unload_category(&mut self, cat: ToolCategory) -> bool {
145        if cat == ToolCategory::Core || cat == ToolCategory::Internal {
146            return false;
147        }
148        self.active_categories.remove(&cat)
149    }
150
151    pub fn is_tool_active(&self, name: &str) -> bool {
152        let cat = categorize_tool(name);
153        if cat == ToolCategory::Internal {
154            return false;
155        }
156        if !self.supports_list_changed {
157            return true;
158        }
159        self.active_categories.contains(&cat)
160    }
161
162    pub fn active_categories(&self) -> Vec<&'static str> {
163        let mut cats: Vec<_> = self
164            .active_categories
165            .iter()
166            .map(ToolCategory::as_str)
167            .collect();
168        cats.sort_unstable();
169        cats
170    }
171
172    pub fn all_categories() -> Vec<&'static str> {
173        vec!["core", "arch", "debug", "memory", "metrics", "session"]
174    }
175}
176
177static GLOBAL: OnceLock<Mutex<DynamicToolState>> = OnceLock::new();
178
179pub fn global() -> &'static Mutex<DynamicToolState> {
180    GLOBAL.get_or_init(|| Mutex::new(DynamicToolState::new()))
181}
182
183pub fn init_all_enabled() {
184    let _ = GLOBAL.set(Mutex::new(DynamicToolState::all_enabled()));
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn core_tools_always_active() {
193        let state = DynamicToolState::new();
194        assert!(state.is_tool_active("ctx_read"));
195        assert!(state.is_tool_active("ctx_search"));
196    }
197
198    #[test]
199    fn dynamic_tools_filtered_when_list_changed() {
200        let mut state = DynamicToolState::new();
201        state.set_supports_list_changed(true);
202        assert!(!state.is_tool_active("ctx_benchmark"));
203        assert!(!state.is_tool_active("ctx_architecture"));
204        assert!(state.is_tool_active("ctx_read"));
205    }
206
207    #[test]
208    fn load_category_enables_tools() {
209        let mut state = DynamicToolState::new();
210        state.set_supports_list_changed(true);
211        assert!(!state.is_tool_active("ctx_architecture"));
212        state.load_category(ToolCategory::Arch);
213        assert!(state.is_tool_active("ctx_architecture"));
214    }
215
216    #[test]
217    fn cannot_unload_core() {
218        let mut state = DynamicToolState::new();
219        assert!(!state.unload_category(ToolCategory::Core));
220    }
221
222    #[test]
223    fn all_tools_visible_without_list_changed() {
224        let state = DynamicToolState::new();
225        assert!(state.is_tool_active("ctx_graph"));
226        assert!(!state.is_tool_active("ctx_metrics")); // Internal tools never active
227    }
228
229    #[test]
230    fn internal_tools_never_active() {
231        let state = DynamicToolState::all_enabled();
232        assert!(!state.is_tool_active("ctx_metrics"));
233        assert!(!state.is_tool_active("ctx_cost"));
234        assert!(!state.is_tool_active("ctx_discover_tools"));
235        assert!(!state.is_tool_active("ctx_dedup"));
236    }
237
238    #[test]
239    fn categorize_known_tools() {
240        assert_eq!(categorize_tool("ctx_read"), ToolCategory::Core);
241        assert_eq!(categorize_tool("ctx_graph"), ToolCategory::Core);
242        assert_eq!(categorize_tool("ctx_benchmark"), ToolCategory::Debug);
243        assert_eq!(categorize_tool("ctx_semantic_search"), ToolCategory::Memory);
244        assert_eq!(categorize_tool("ctx_metrics"), ToolCategory::Internal);
245        assert_eq!(categorize_tool("ctx_workflow"), ToolCategory::Session);
246    }
247}