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    Arch,
8    Debug,
9    Memory,
10    Metrics,
11    Session,
12}
13
14impl ToolCategory {
15    pub fn parse(s: &str) -> Option<Self> {
16        match s {
17            "core" => Some(Self::Core),
18            "arch" | "architecture" => Some(Self::Arch),
19            "debug" | "profiling" => Some(Self::Debug),
20            "memory" | "semantic" => Some(Self::Memory),
21            "metrics" | "stats" => Some(Self::Metrics),
22            "session" => Some(Self::Session),
23            _ => None,
24        }
25    }
26
27    pub fn as_str(&self) -> &'static str {
28        match self {
29            Self::Core => "core",
30            Self::Arch => "arch",
31            Self::Debug => "debug",
32            Self::Memory => "memory",
33            Self::Metrics => "metrics",
34            Self::Session => "session",
35        }
36    }
37}
38
39#[allow(clippy::match_same_arms)]
40pub fn categorize_tool(name: &str) -> ToolCategory {
41    match name {
42        "ctx_read" | "ctx_search" | "ctx_shell" | "ctx_tree" | "ctx_edit" | "ctx_plan"
43        | "ctx_control" | "ctx_compress" | "ctx_session" | "ctx_knowledge" | "ctx_agent"
44        | "ctx_overview" | "ctx_preload" | "ctx_dedup" | "ctx_expand" | "ctx_multi_read"
45        | "ctx_smart_read" | "ctx_delta" | "ctx_prefetch" | "ctx_compile" | "ctx_fill"
46        | "ctx_execute" | "ctx_context" | "ctx_cache" | "ctx_retrieve" | "ctx_discover_tools"
47        | "ctx_pack" | "ctx_feedback" => ToolCategory::Core,
48
49        "ctx_graph" | "ctx_architecture" | "ctx_impact" | "ctx_callers" | "ctx_callees"
50        | "ctx_callgraph" | "ctx_symbol" | "ctx_graph_diagram" | "ctx_routes" | "ctx_smells"
51        | "ctx_index" => ToolCategory::Arch,
52
53        "ctx_benchmark" | "ctx_heatmap" | "ctx_verify" | "ctx_analyze" | "ctx_profile"
54        | "ctx_proof" | "ctx_review" => ToolCategory::Debug,
55
56        "ctx_semantic_search"
57        | "ctx_compress_memory"
58        | "ctx_discover"
59        | "ctx_provider"
60        | "ctx_artifacts" => ToolCategory::Memory,
61
62        "ctx_metrics" | "ctx_cost" | "ctx_gain" | "ctx_intent" | "ctx_response" | "ctx_wrapped"
63        | "ctx_outline" | "ctx_radar" => ToolCategory::Metrics,
64
65        "ctx_share" | "ctx_task" | "ctx_handoff" | "ctx_workflow" => ToolCategory::Session,
66
67        _ => ToolCategory::Core,
68    }
69}
70
71#[derive(Debug)]
72pub struct DynamicToolState {
73    active_categories: HashSet<ToolCategory>,
74    supports_list_changed: bool,
75}
76
77impl Default for DynamicToolState {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl DynamicToolState {
84    pub fn new() -> Self {
85        let mut active = HashSet::new();
86        active.insert(ToolCategory::Core);
87        active.insert(ToolCategory::Session);
88        Self {
89            active_categories: active,
90            supports_list_changed: false,
91        }
92    }
93
94    pub fn all_enabled() -> Self {
95        let mut active = HashSet::new();
96        active.insert(ToolCategory::Core);
97        active.insert(ToolCategory::Arch);
98        active.insert(ToolCategory::Debug);
99        active.insert(ToolCategory::Memory);
100        active.insert(ToolCategory::Metrics);
101        active.insert(ToolCategory::Session);
102        Self {
103            active_categories: active,
104            supports_list_changed: false,
105        }
106    }
107
108    pub fn set_supports_list_changed(&mut self, val: bool) {
109        self.supports_list_changed = val;
110    }
111
112    pub fn supports_list_changed(&self) -> bool {
113        self.supports_list_changed
114    }
115
116    pub fn load_category(&mut self, cat: ToolCategory) -> bool {
117        self.active_categories.insert(cat)
118    }
119
120    pub fn unload_category(&mut self, cat: ToolCategory) -> bool {
121        if cat == ToolCategory::Core {
122            return false;
123        }
124        self.active_categories.remove(&cat)
125    }
126
127    pub fn is_tool_active(&self, name: &str) -> bool {
128        if !self.supports_list_changed {
129            return true;
130        }
131        let cat = categorize_tool(name);
132        self.active_categories.contains(&cat)
133    }
134
135    pub fn active_categories(&self) -> Vec<&'static str> {
136        let mut cats: Vec<_> = self
137            .active_categories
138            .iter()
139            .map(ToolCategory::as_str)
140            .collect();
141        cats.sort_unstable();
142        cats
143    }
144
145    pub fn all_categories() -> Vec<&'static str> {
146        vec!["core", "arch", "debug", "memory", "metrics", "session"]
147    }
148}
149
150static GLOBAL: OnceLock<Mutex<DynamicToolState>> = OnceLock::new();
151
152pub fn global() -> &'static Mutex<DynamicToolState> {
153    GLOBAL.get_or_init(|| Mutex::new(DynamicToolState::new()))
154}
155
156pub fn init_all_enabled() {
157    let _ = GLOBAL.set(Mutex::new(DynamicToolState::all_enabled()));
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn core_tools_always_active() {
166        let state = DynamicToolState::new();
167        assert!(state.is_tool_active("ctx_read"));
168        assert!(state.is_tool_active("ctx_search"));
169    }
170
171    #[test]
172    fn dynamic_tools_filtered_when_list_changed() {
173        let mut state = DynamicToolState::new();
174        state.set_supports_list_changed(true);
175        assert!(!state.is_tool_active("ctx_graph"));
176        assert!(!state.is_tool_active("ctx_benchmark"));
177        assert!(state.is_tool_active("ctx_read"));
178    }
179
180    #[test]
181    fn load_category_enables_tools() {
182        let mut state = DynamicToolState::new();
183        state.set_supports_list_changed(true);
184        assert!(!state.is_tool_active("ctx_graph"));
185        state.load_category(ToolCategory::Arch);
186        assert!(state.is_tool_active("ctx_graph"));
187    }
188
189    #[test]
190    fn cannot_unload_core() {
191        let mut state = DynamicToolState::new();
192        assert!(!state.unload_category(ToolCategory::Core));
193    }
194
195    #[test]
196    fn all_tools_visible_without_list_changed() {
197        let state = DynamicToolState::new();
198        assert!(state.is_tool_active("ctx_graph"));
199        assert!(state.is_tool_active("ctx_metrics"));
200    }
201
202    #[test]
203    fn categorize_known_tools() {
204        assert_eq!(categorize_tool("ctx_read"), ToolCategory::Core);
205        assert_eq!(categorize_tool("ctx_graph"), ToolCategory::Arch);
206        assert_eq!(categorize_tool("ctx_benchmark"), ToolCategory::Debug);
207        assert_eq!(categorize_tool("ctx_semantic_search"), ToolCategory::Memory);
208        assert_eq!(categorize_tool("ctx_metrics"), ToolCategory::Metrics);
209        assert_eq!(categorize_tool("ctx_workflow"), ToolCategory::Session);
210    }
211}