Skip to main content

wm_dispatch/
registry.rs

1//! Tool registry — maps Gana → tools and provides lookup.
2
3#[cfg(test)]
4use async_trait::async_trait;
5use std::collections::HashMap;
6use std::sync::Arc;
7use wm_core::{Gana, Tool};
8
9/// Inner state of the registry, wrapped in Arc for cheap cloning.
10struct RegistryInner {
11    tools: Vec<Arc<dyn Tool>>,
12    by_gana: HashMap<Gana, Vec<usize>>,
13    by_name: HashMap<String, usize>,
14}
15
16/// Registry of all available tools, organized by Gana.
17///
18/// Wraps inner state in `Arc` so the registry can be cheaply cloned
19/// and shared between the dispatch pipeline, MCP server, and meta-tools.
20pub struct ToolRegistry {
21    inner: Arc<RegistryInner>,
22}
23
24impl ToolRegistry {
25    /// Create a new empty registry.
26    #[must_use]
27    pub fn new() -> Self {
28        Self {
29            inner: Arc::new(RegistryInner {
30                tools: vec![],
31                by_gana: HashMap::new(),
32                by_name: HashMap::new(),
33            }),
34        }
35    }
36
37    /// Create a new empty registry with pre-allocated capacity.
38    #[must_use]
39    pub fn with_capacity(cap: usize) -> Self {
40        Self {
41            inner: Arc::new(RegistryInner {
42                tools: Vec::with_capacity(cap),
43                by_gana: HashMap::new(),
44                by_name: HashMap::new(),
45            }),
46        }
47    }
48
49    /// Register a tool. Returns a new registry (immutable, Arc-shared).
50    ///
51    /// Since the inner state is Arc-shared, this method is on `&self`
52    /// and returns a new `ToolRegistry` with the tool added.
53    /// For bulk registration, use `ToolRegistryBuilder` instead.
54    #[must_use]
55    pub fn register(&self, tool: Arc<dyn Tool>) -> Self {
56        let mut inner = RegistryInner {
57            tools: self.inner.tools.clone(),
58            by_gana: self.inner.by_gana.clone(),
59            by_name: self.inner.by_name.clone(),
60        };
61        let idx = inner.tools.len();
62        let gana = tool.gana();
63        let name = tool.name().to_string();
64        inner.by_gana.entry(gana).or_default().push(idx);
65        inner.by_name.insert(name, idx);
66        inner.tools.push(tool);
67        Self {
68            inner: Arc::new(inner),
69        }
70    }
71
72    /// Get a tool by name. Returns an owned `Arc<dyn Tool>` clone.
73    #[must_use]
74    pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
75        self.inner
76            .by_name
77            .get(name)
78            .map(|&i| Arc::clone(&self.inner.tools[i]))
79    }
80
81    /// Get all tools for a Gana. Returns owned `Arc<dyn Tool>` clones.
82    #[must_use]
83    pub fn by_gana(&self, gana: Gana) -> Vec<Arc<dyn Tool>> {
84        self.inner
85            .by_gana
86            .get(&gana)
87            .map(|indices| {
88                indices
89                    .iter()
90                    .map(|&i| Arc::clone(&self.inner.tools[i]))
91                    .collect()
92            })
93            .unwrap_or_default()
94    }
95
96    /// Get all registered tools. Returns cloned Arcs.
97    #[must_use]
98    pub fn all(&self) -> Vec<Arc<dyn Tool>> {
99        self.inner.tools.clone()
100    }
101
102    /// Get all registered tools as a slice reference (no clone).
103    #[must_use]
104    pub fn all_ref(&self) -> &[Arc<dyn Tool>] {
105        &self.inner.tools
106    }
107
108    /// Number of registered tools.
109    #[must_use]
110    pub fn len(&self) -> usize {
111        self.inner.tools.len()
112    }
113
114    /// Whether the registry is empty.
115    #[must_use]
116    pub fn is_empty(&self) -> bool {
117        self.inner.tools.is_empty()
118    }
119
120    /// Get all tools available in the given brain-wave state.
121    ///
122    /// Filters tools by their `EffectRow::is_available_in()` check.
123    /// In Alpha/Theta/Delta modes, write-heavy and expensive tools are excluded.
124    #[must_use]
125    pub fn available_in(&self, brain_wave: wm_core::BrainWave) -> Vec<Arc<dyn Tool>> {
126        self.inner
127            .tools
128            .iter()
129            .filter(|t| t.effects().is_available_in(brain_wave))
130            .cloned()
131            .collect()
132    }
133
134    /// Count tools available in the given brain-wave state.
135    #[must_use]
136    pub fn available_count(&self, brain_wave: wm_core::BrainWave) -> usize {
137        self.inner
138            .tools
139            .iter()
140            .filter(|t| t.effects().is_available_in(brain_wave))
141            .count()
142    }
143}
144
145impl Default for ToolRegistry {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151impl Clone for ToolRegistry {
152    fn clone(&self) -> Self {
153        Self {
154            inner: Arc::clone(&self.inner),
155        }
156    }
157}
158
159/// Builder for incrementally constructing a ToolRegistry.
160pub struct ToolRegistryBuilder {
161    tools: Vec<Arc<dyn Tool>>,
162    by_gana: HashMap<Gana, Vec<usize>>,
163    by_name: HashMap<String, usize>,
164}
165
166impl ToolRegistryBuilder {
167    /// Create a new empty builder.
168    #[must_use]
169    pub fn new() -> Self {
170        Self {
171            tools: vec![],
172            by_gana: HashMap::new(),
173            by_name: HashMap::new(),
174        }
175    }
176
177    /// Register a tool.
178    ///
179    /// If a tool with the same name is already registered, the new tool
180    /// shadows the old one. A warning is logged to alert developers of
181    /// potential unintended shadowing.
182    pub fn register(&mut self, tool: Arc<dyn Tool>) -> &mut Self {
183        let idx = self.tools.len();
184        let gana = tool.gana();
185        let name = tool.name().to_string();
186        if self.by_name.contains_key(&name) {
187            tracing::warn!(
188                tool_name = %name,
189                "Duplicate tool registration — new tool will shadow existing one"
190            );
191        }
192        self.by_gana.entry(gana).or_default().push(idx);
193        self.by_name.insert(name, idx);
194        self.tools.push(tool);
195        self
196    }
197
198    /// Build the immutable registry.
199    #[must_use]
200    pub fn build(self) -> ToolRegistry {
201        ToolRegistry {
202            inner: Arc::new(RegistryInner {
203                tools: self.tools,
204                by_gana: self.by_gana,
205                by_name: self.by_name,
206            }),
207        }
208    }
209}
210
211impl Default for ToolRegistryBuilder {
212    fn default() -> Self {
213        Self::new()
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use wm_core::{EffectRow, ToolStats};
221
222    struct MockTool {
223        name: String,
224        gana: Gana,
225        effects: EffectRow,
226        stats: ToolStats,
227    }
228
229    #[async_trait]
230    impl Tool for MockTool {
231        fn name(&self) -> &str {
232            &self.name
233        }
234        fn gana(&self) -> Gana {
235            self.gana
236        }
237        fn effects(&self) -> &EffectRow {
238            &self.effects
239        }
240        fn stats(&self) -> &ToolStats {
241            &self.stats
242        }
243        async fn call(
244            &self,
245            _ctx: &mut wm_core::Context,
246            _args: wm_core::Args,
247        ) -> wm_core::Result<wm_core::Output> {
248            Ok(serde_json::json!({"ok": true}))
249        }
250    }
251
252    fn make_tool(name: &str) -> Arc<dyn Tool> {
253        Arc::new(MockTool {
254            name: name.into(),
255            gana: Gana::Horn,
256            effects: EffectRow::default(),
257            stats: ToolStats::default(),
258        })
259    }
260
261    #[test]
262    fn duplicate_registration_shadows_and_warns() {
263        let mut builder = ToolRegistryBuilder::new();
264        builder.register(make_tool("dup"));
265        builder.register(make_tool("dup"));
266
267        let registry = builder.build();
268        let tool = registry.get("dup").unwrap();
269        assert_eq!(tool.name(), "dup");
270    }
271
272    #[test]
273    fn unique_registration_no_warning() {
274        let mut builder = ToolRegistryBuilder::new();
275        builder.register(make_tool("tool_a"));
276        builder.register(make_tool("tool_b"));
277
278        let registry = builder.build();
279        assert!(registry.get("tool_a").is_some());
280        assert!(registry.get("tool_b").is_some());
281    }
282}