Skip to main content

kaish_kernel/tools/
registry.rs

1//! Tool registry for looking up and managing tools.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use super::traits::{Tool, ToolSchema};
7
8/// Registry of available tools.
9#[derive(Default)]
10pub struct ToolRegistry {
11    tools: HashMap<String, Arc<dyn Tool>>,
12}
13
14impl ToolRegistry {
15    /// Create a new empty registry.
16    pub fn new() -> Self {
17        Self::default()
18    }
19
20    /// Register a tool.
21    pub fn register(&mut self, tool: impl Tool + 'static) {
22        let name = tool.name().to_string();
23        self.tools.insert(name, Arc::new(tool));
24    }
25
26    /// Register a tool that's already in an Arc.
27    pub fn register_arc(&mut self, tool: Arc<dyn Tool>) {
28        let name = tool.name().to_string();
29        self.tools.insert(name, tool);
30    }
31
32    /// Look up a tool by name.
33    pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
34        self.tools.get(name).cloned()
35    }
36
37    /// Check if a tool exists.
38    pub fn contains(&self, name: &str) -> bool {
39        self.tools.contains_key(name)
40    }
41
42    /// List all tool names.
43    pub fn names(&self) -> Vec<&str> {
44        let mut names: Vec<_> = self.tools.keys().map(|s| s.as_str()).collect();
45        names.sort();
46        names
47    }
48
49    /// List all tool schemas.
50    pub fn schemas(&self) -> Vec<ToolSchema> {
51        let mut schemas: Vec<_> = self.tools.values().map(|t| t.schema()).collect();
52        schemas.sort_by(|a, b| a.name.cmp(&b.name));
53        schemas
54    }
55
56    /// Number of registered tools.
57    pub fn len(&self) -> usize {
58        self.tools.len()
59    }
60
61    /// Check if empty.
62    pub fn is_empty(&self) -> bool {
63        self.tools.is_empty()
64    }
65}
66
67impl std::fmt::Debug for ToolRegistry {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        f.debug_struct("ToolRegistry")
70            .field("tools", &self.names())
71            .finish()
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::interpreter::ExecResult;
79    use crate::tools::{ToolArgs, ToolCtx};
80    use async_trait::async_trait;
81
82    struct DummyTool;
83
84    #[async_trait]
85    impl Tool for DummyTool {
86        fn name(&self) -> &str {
87            "dummy"
88        }
89
90        fn schema(&self) -> ToolSchema {
91            ToolSchema::new("dummy", "A test tool")
92        }
93
94        async fn execute(&self, _args: ToolArgs, _ctx: &mut dyn ToolCtx) -> ExecResult {
95            ExecResult::success("dummy output")
96        }
97    }
98
99    #[test]
100    fn test_register_and_get() {
101        let mut registry = ToolRegistry::new();
102        registry.register(DummyTool);
103
104        assert!(registry.contains("dummy"));
105        assert!(registry.get("dummy").is_some());
106        assert!(!registry.contains("nonexistent"));
107    }
108
109    /// `validate_command` (validator/walker.rs) falls back to `tool.schema()`
110    /// on a catalog miss, and `validate_against_schema` attributes every
111    /// `ValidationIssue` it raises to that schema's `.name` — not to the AST
112    /// `cmd.name` the walker actually resolved and dispatched by (`==
113    /// tool.name()`, since `register`/`register_arc` key this map on it).
114    /// Nothing at the `Tool` trait level keeps `name()` and `schema().name`
115    /// in sync; a tool whose two names disagree would silently misattribute
116    /// every issue it raises to the wrong command.
117    ///
118    /// This test is the actual enforcement of that invariant. A
119    /// `debug_assert!` at the call site would compile to nothing in a
120    /// release build — this workspace has no `[profile.release]` override,
121    /// so `debug_assertions` is off wherever kaish actually ships — while
122    /// this test runs in CI on every `cargo test` and covers every in-tree
123    /// builtin at once, not only the ones a particular script happens to
124    /// exercise.
125    ///
126    /// Only covers what `register_builtins` installs — a third-party tool
127    /// an embedder registers at runtime is not walked here and is not
128    /// covered by anything else either.
129    #[test]
130    fn every_builtin_tool_name_matches_its_own_schema_name() {
131        let mut registry = ToolRegistry::new();
132        crate::tools::register_builtins(&mut registry);
133
134        for name in registry.names() {
135            let tool = registry.get(name).expect("a name from names() must resolve via get()");
136            assert_eq!(
137                tool.name(),
138                tool.schema().name.as_str(),
139                "tool '{name}': Tool::name() == {:?} but Tool::schema().name == {:?} -- \
140                 these must agree. validate_against_schema attributes every ValidationIssue \
141                 it raises to schema.name, while the walker resolves and dispatches the \
142                 command by cmd.name (== tool.name()); a mismatch here means an embedder \
143                 reading ValidationIssue::command sees the wrong command. Fix the tool so \
144                 Tool::name() and Tool::schema().name agree -- do not relax this test.",
145                tool.name(),
146                tool.schema().name,
147            );
148        }
149    }
150
151    #[test]
152    fn test_names_sorted() {
153        let mut registry = ToolRegistry::new();
154
155        struct ToolA;
156        struct ToolZ;
157
158        #[async_trait]
159        impl Tool for ToolA {
160            fn name(&self) -> &str { "aaa" }
161            fn schema(&self) -> ToolSchema { ToolSchema::new("aaa", "") }
162            async fn execute(&self, _: ToolArgs, _: &mut dyn ToolCtx) -> ExecResult {
163                ExecResult::success("")
164            }
165        }
166
167        #[async_trait]
168        impl Tool for ToolZ {
169            fn name(&self) -> &str { "zzz" }
170            fn schema(&self) -> ToolSchema { ToolSchema::new("zzz", "") }
171            async fn execute(&self, _: ToolArgs, _: &mut dyn ToolCtx) -> ExecResult {
172                ExecResult::success("")
173            }
174        }
175
176        registry.register(ToolZ);
177        registry.register(ToolA);
178
179        let names = registry.names();
180        assert_eq!(names, vec!["aaa", "zzz"]);
181    }
182}