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    /// Token overrides from nanny.toml [tools.<name>] tokens_per_call.
34    /// When set, this value replaces the tool's own declared_cost().
35    cost_overrides: HashMap<String, u64>,
36}
37
38impl ToolRegistry {
39    /// Create an empty registry.
40    pub fn new() -> Self {
41        Self {
42            tools: HashMap::new(),
43            cost_overrides: HashMap::new(),
44        }
45    }
46
47    /// Override the declared token cost for a tool.
48    ///
49    /// Reads from nanny.toml `[tools.<name>] tokens_per_call`.
50    /// When set, `declared_cost()` returns this value instead of the
51    /// tool's own declared cost.
52    pub fn set_cost_override(&mut self, tool_name: &str, cost: u64) {
53        self.cost_overrides.insert(tool_name.to_string(), cost);
54    }
55
56    /// Register a tool.
57    ///
58    /// If a tool with the same name is already registered, it is replaced.
59    /// The registry takes ownership of the tool via `Box<dyn Tool>`.
60    pub fn register(&mut self, tool: Box<dyn Tool>) {
61        self.tools.insert(tool.name().to_string(), tool);
62    }
63
64    /// Return the names of all registered tools.
65    ///
66    /// Useful for debugging and `nanny init` suggestions.
67    pub fn registered_names(&self) -> Vec<&str> {
68        self.tools.keys().map(|s| s.as_str()).collect()
69    }
70}
71
72impl Default for ToolRegistry {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78/// Create a registry pre-loaded with all built-in Nanny tools.
79///
80/// Currently includes:
81/// - `http_get` — makes a single HTTP GET request
82///
83/// This is the standard starting point for most executions.
84/// Register additional tools on top of this if needed.
85pub fn default_registry() -> ToolRegistry {
86    let mut registry = ToolRegistry::new();
87    registry.register(Box::new(HttpGet::new()));
88    registry
89}
90
91impl ToolExecutor for ToolRegistry {
92    /// Call a registered tool by name.
93    ///
94    /// Returns `NotFound` if no tool with that name is registered.
95    /// Returns `Execution` if the tool was found but failed.
96    fn call(&self, name: &str, args: &ToolArgs) -> Result<ToolOutput, ToolCallError> {
97        match self.tools.get(name) {
98            None => Err(ToolCallError::NotFound {
99                tool_name: name.to_string(),
100            }),
101            Some(tool) => tool.execute(args).map_err(|source| ToolCallError::Execution {
102                tool_name: name.to_string(),
103                source,
104            }),
105        }
106    }
107
108    /// Return the cost of a registered tool.
109    ///
110    /// If a cost override was set via `set_cost_override`, that value is used.
111    /// Otherwise falls back to the tool's own declared cost.
112    /// Returns `None` if the tool is not registered.
113    fn declared_cost(&self, name: &str) -> Option<u64> {
114        if self.tools.contains_key(name) {
115            Some(self.cost_overrides.get(name).copied()
116                .unwrap_or_else(|| self.tools[name].declared_cost()))
117        } else {
118            None
119        }
120    }
121}
122
123// ── Tests ─────────────────────────────────────────────────────────────────────
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use nanny_core::tool::{ToolError, ToolOutput};
129
130    // A minimal tool for testing — always succeeds, costs 5 units.
131    struct EchoTool;
132    impl Tool for EchoTool {
133        fn name(&self) -> &str { "echo" }
134        fn declared_cost(&self) -> u64 { 5 }
135        fn execute(&self, args: &ToolArgs) -> Result<ToolOutput, ToolError> {
136            let message = args.get("message").cloned().unwrap_or_default();
137            Ok(ToolOutput { content: message })
138        }
139    }
140
141    // A tool that always fails.
142    struct FailingTool;
143    impl Tool for FailingTool {
144        fn name(&self) -> &str { "failing" }
145        fn declared_cost(&self) -> u64 { 1 }
146        fn execute(&self, _: &ToolArgs) -> Result<ToolOutput, ToolError> {
147            Err(ToolError::ExecutionFailed("always fails".to_string()))
148        }
149    }
150
151    #[test]
152    fn calls_registered_tool() {
153        let mut registry = ToolRegistry::new();
154        registry.register(Box::new(EchoTool));
155
156        let mut args = ToolArgs::new();
157        args.insert("message".to_string(), "hello".to_string());
158
159        let result = registry.call("echo", &args);
160        assert!(result.is_ok());
161        assert_eq!(result.unwrap().content, "hello");
162    }
163
164    #[test]
165    fn returns_not_found_for_unknown_tool() {
166        let registry = ToolRegistry::new();
167        let result = registry.call("unknown", &ToolArgs::new());
168
169        assert!(matches!(result, Err(ToolCallError::NotFound { .. })));
170    }
171
172    #[test]
173    fn returns_execution_error_on_tool_failure() {
174        let mut registry = ToolRegistry::new();
175        registry.register(Box::new(FailingTool));
176
177        let result = registry.call("failing", &ToolArgs::new());
178        assert!(matches!(result, Err(ToolCallError::Execution { .. })));
179    }
180
181    #[test]
182    fn declared_cost_returns_correct_value() {
183        let mut registry = ToolRegistry::new();
184        registry.register(Box::new(EchoTool));
185
186        assert_eq!(registry.declared_cost("echo"), Some(5));
187        assert_eq!(registry.declared_cost("unknown"), None);
188    }
189
190    #[test]
191    fn cost_override_replaces_declared_cost() {
192        let mut registry = ToolRegistry::new();
193        registry.register(Box::new(EchoTool)); // declared_cost = 5
194        registry.set_cost_override("echo", 99);
195
196        assert_eq!(registry.declared_cost("echo"), Some(99));
197    }
198
199    #[test]
200    fn cost_override_does_not_affect_unregistered_tool() {
201        let mut registry = ToolRegistry::new();
202        registry.set_cost_override("ghost", 50); // tool not registered
203        assert_eq!(registry.declared_cost("ghost"), None);
204    }
205
206    #[test]
207    fn registered_names_lists_all_tools() {
208        let mut registry = ToolRegistry::new();
209        registry.register(Box::new(EchoTool));
210        registry.register(Box::new(FailingTool));
211
212        let mut names = registry.registered_names();
213        names.sort();
214        assert_eq!(names, vec!["echo", "failing"]);
215    }
216}