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        // Provider is Core: ctx_provider is the gateway to external context
80        // (GitHub issues, Jira, Postgres, etc.) and must always be available.
81        "ctx_provider" => ToolCategory::Core,
82
83        // Memory: on-demand semantic tools
84        "ctx_semantic_search" | "ctx_artifacts" => ToolCategory::Memory,
85
86        // Batch: on-demand batch/PR/sandbox tools
87        "ctx_fill" | "ctx_execute" | "ctx_expand" | "ctx_pack" | "ctx_plan" | "ctx_control"
88        | "ctx_compile" => ToolCategory::Metrics,
89
90        // Multi-agent: on-demand collaboration
91        "ctx_agent" | "ctx_share" | "ctx_task" | "ctx_handoff" | "ctx_workflow" => {
92            ToolCategory::Session
93        }
94
95        _ => ToolCategory::Core,
96    }
97}
98
99#[derive(Debug)]
100pub struct DynamicToolState {
101    active_categories: HashSet<ToolCategory>,
102    supports_list_changed: bool,
103}
104
105impl Default for DynamicToolState {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111impl DynamicToolState {
112    pub fn new() -> Self {
113        let mut active = HashSet::new();
114        active.insert(ToolCategory::Core);
115        active.insert(ToolCategory::Session);
116        Self {
117            active_categories: active,
118            supports_list_changed: false,
119        }
120    }
121
122    pub fn all_enabled() -> Self {
123        let mut active = HashSet::new();
124        active.insert(ToolCategory::Core);
125        active.insert(ToolCategory::Arch);
126        active.insert(ToolCategory::Debug);
127        active.insert(ToolCategory::Memory);
128        active.insert(ToolCategory::Metrics);
129        active.insert(ToolCategory::Session);
130        Self {
131            active_categories: active,
132            supports_list_changed: false,
133        }
134    }
135
136    pub fn set_supports_list_changed(&mut self, val: bool) {
137        self.supports_list_changed = val;
138    }
139
140    pub fn supports_list_changed(&self) -> bool {
141        self.supports_list_changed
142    }
143
144    pub fn load_category(&mut self, cat: ToolCategory) -> bool {
145        self.active_categories.insert(cat)
146    }
147
148    pub fn unload_category(&mut self, cat: ToolCategory) -> bool {
149        if cat == ToolCategory::Core || cat == ToolCategory::Internal {
150            return false;
151        }
152        self.active_categories.remove(&cat)
153    }
154
155    pub fn is_tool_active(&self, name: &str) -> bool {
156        let cat = categorize_tool(name);
157        if cat == ToolCategory::Internal {
158            return false;
159        }
160        if !self.supports_list_changed {
161            return true;
162        }
163        self.active_categories.contains(&cat)
164    }
165
166    pub fn active_categories(&self) -> Vec<&'static str> {
167        let mut cats: Vec<_> = self
168            .active_categories
169            .iter()
170            .map(ToolCategory::as_str)
171            .collect();
172        cats.sort_unstable();
173        cats
174    }
175
176    pub fn all_categories() -> Vec<&'static str> {
177        vec!["core", "arch", "debug", "memory", "metrics", "session"]
178    }
179}
180
181static GLOBAL: OnceLock<Mutex<DynamicToolState>> = OnceLock::new();
182
183pub fn global() -> &'static Mutex<DynamicToolState> {
184    GLOBAL.get_or_init(|| Mutex::new(DynamicToolState::new()))
185}
186
187pub fn init_all_enabled() {
188    let _ = GLOBAL.set(Mutex::new(DynamicToolState::all_enabled()));
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn core_tools_always_active() {
197        let state = DynamicToolState::new();
198        assert!(state.is_tool_active("ctx_read"));
199        assert!(state.is_tool_active("ctx_search"));
200    }
201
202    #[test]
203    fn dynamic_tools_filtered_when_list_changed() {
204        let mut state = DynamicToolState::new();
205        state.set_supports_list_changed(true);
206        assert!(!state.is_tool_active("ctx_benchmark"));
207        assert!(!state.is_tool_active("ctx_architecture"));
208        assert!(state.is_tool_active("ctx_read"));
209    }
210
211    #[test]
212    fn load_category_enables_tools() {
213        let mut state = DynamicToolState::new();
214        state.set_supports_list_changed(true);
215        assert!(!state.is_tool_active("ctx_architecture"));
216        state.load_category(ToolCategory::Arch);
217        assert!(state.is_tool_active("ctx_architecture"));
218    }
219
220    #[test]
221    fn cannot_unload_core() {
222        let mut state = DynamicToolState::new();
223        assert!(!state.unload_category(ToolCategory::Core));
224    }
225
226    #[test]
227    fn all_tools_visible_without_list_changed() {
228        let state = DynamicToolState::new();
229        assert!(state.is_tool_active("ctx_graph"));
230        assert!(!state.is_tool_active("ctx_metrics")); // Internal tools never active
231    }
232
233    #[test]
234    fn internal_tools_never_active() {
235        let state = DynamicToolState::all_enabled();
236        assert!(!state.is_tool_active("ctx_metrics"));
237        assert!(!state.is_tool_active("ctx_cost"));
238        assert!(!state.is_tool_active("ctx_discover_tools"));
239        assert!(!state.is_tool_active("ctx_dedup"));
240    }
241
242    #[test]
243    fn categorize_known_tools() {
244        assert_eq!(categorize_tool("ctx_read"), ToolCategory::Core);
245        assert_eq!(categorize_tool("ctx_graph"), ToolCategory::Core);
246        assert_eq!(categorize_tool("ctx_benchmark"), ToolCategory::Debug);
247        assert_eq!(categorize_tool("ctx_semantic_search"), ToolCategory::Memory);
248        assert_eq!(categorize_tool("ctx_metrics"), ToolCategory::Internal);
249        assert_eq!(categorize_tool("ctx_workflow"), ToolCategory::Session);
250    }
251}