Skip to main content

nanny_runtime/tools/
mod.rs

1// nanny-runtime tools: Tool registry and built-in tool implementations.
2//
3// This module owns:
4// - ToolRegistry: a collection of registered tools, implements ToolExecutor
5// - Built-in tools: http_get, and others as they are added
6//
7// The executor in nanny-core programs against ToolExecutor (the trait).
8// ToolRegistry is the concrete implementation of that trait.
9
10pub mod http_get;
11
12pub use http_get::HttpGet;
13
14use nanny_core::tool::{Tool, ToolArgs, ToolCallError, ToolExecutor, ToolOutput};
15use std::collections::HashMap;
16
17// ── ToolRegistry ──────────────────────────────────────────────────────────────
18
19/// A collection of registered tools.
20///
21/// Implements `ToolExecutor` so the executor can call tools by name
22/// without knowing anything about their implementation.
23///
24/// Tools are registered once at startup and never mutated during execution.
25/// If the tool name is not in the registry, `call()` returns `NotFound`.
26pub struct ToolRegistry {
27    /// Map from tool name to boxed implementation.
28    ///
29    /// `Box<dyn Tool>` means any type implementing `Tool` can be stored here,
30    /// regardless of its concrete type. This is Rust's runtime polymorphism.
31    tools: HashMap<String, Box<dyn Tool>>,
32}
33
34impl ToolRegistry {
35    /// Create an empty registry.
36    pub fn new() -> Self {
37        Self {
38            tools: HashMap::new(),
39        }
40    }
41
42    /// Register a tool.
43    ///
44    /// If a tool with the same name is already registered, it is replaced.
45    /// The registry takes ownership of the tool via `Box<dyn Tool>`.
46    pub fn register(&mut self, tool: Box<dyn Tool>) {
47        self.tools.insert(tool.name().to_string(), tool);
48    }
49
50    /// Return the names of all registered tools.
51    ///
52    /// Useful for debugging and `nanny init` suggestions.
53    pub fn registered_names(&self) -> Vec<&str> {
54        self.tools.keys().map(|s| s.as_str()).collect()
55    }
56}
57
58impl Default for ToolRegistry {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64/// Create a registry pre-loaded with all built-in Nanny tools.
65///
66/// Currently includes:
67/// - `http_get`: makes a single HTTP GET request
68///
69/// This is the standard starting point for most executions.
70/// Register additional tools on top of this if needed.
71pub fn default_registry() -> ToolRegistry {
72    let mut registry = ToolRegistry::new();
73    registry.register(Box::new(HttpGet::new()));
74    registry
75}
76
77impl ToolExecutor for ToolRegistry {
78    /// Call a registered tool by name.
79    ///
80    /// Returns `NotFound` if no tool with that name is registered.
81    /// Returns `Execution` if the tool was found but failed.
82    fn call(&self, name: &str, args: &ToolArgs) -> Result<ToolOutput, ToolCallError> {
83        match self.tools.get(name) {
84            None => Err(ToolCallError::NotFound {
85                tool_name: name.to_string(),
86            }),
87            Some(tool) => tool
88                .execute(args)
89                .map_err(|source| ToolCallError::Execution {
90                    tool_name: name.to_string(),
91                    source,
92                }),
93        }
94    }
95}
96
97// ── Tests ─────────────────────────────────────────────────────────────────────
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use nanny_core::tool::{ToolError, ToolOutput};
103
104    // A minimal tool for testing: always succeeds, costs 5 units.
105    struct EchoTool;
106    impl Tool for EchoTool {
107        fn name(&self) -> &str {
108            "echo"
109        }
110        fn execute(&self, args: &ToolArgs) -> Result<ToolOutput, ToolError> {
111            let message = args.get("message").cloned().unwrap_or_default();
112            Ok(ToolOutput { content: message })
113        }
114    }
115
116    // A tool that always fails.
117    struct FailingTool;
118    impl Tool for FailingTool {
119        fn name(&self) -> &str {
120            "failing"
121        }
122        fn execute(&self, _: &ToolArgs) -> Result<ToolOutput, ToolError> {
123            Err(ToolError::ExecutionFailed("always fails".to_string()))
124        }
125    }
126
127    #[test]
128    fn calls_registered_tool() {
129        let mut registry = ToolRegistry::new();
130        registry.register(Box::new(EchoTool));
131
132        let mut args = ToolArgs::new();
133        args.insert("message".to_string(), "hello".to_string());
134
135        let result = registry.call("echo", &args);
136        assert!(result.is_ok());
137        assert_eq!(result.unwrap().content, "hello");
138    }
139
140    #[test]
141    fn returns_not_found_for_unknown_tool() {
142        let registry = ToolRegistry::new();
143        let result = registry.call("unknown", &ToolArgs::new());
144
145        assert!(matches!(result, Err(ToolCallError::NotFound { .. })));
146    }
147
148    #[test]
149    fn returns_execution_error_on_tool_failure() {
150        let mut registry = ToolRegistry::new();
151        registry.register(Box::new(FailingTool));
152
153        let result = registry.call("failing", &ToolArgs::new());
154        assert!(matches!(result, Err(ToolCallError::Execution { .. })));
155    }
156
157    #[test]
158    fn registered_names_lists_all_tools() {
159        let mut registry = ToolRegistry::new();
160        registry.register(Box::new(EchoTool));
161        registry.register(Box::new(FailingTool));
162
163        let mut names = registry.registered_names();
164        names.sort();
165        assert_eq!(names, vec!["echo", "failing"]);
166    }
167}