Skip to main content

nanny_core/tool/
mod.rs

1// The Tool contract.
2//
3// nanny-core defines the shapes.
4// Concrete tool implementations live in nanny-tools.
5//
6// The executor programs against ToolExecutor: not against any specific tool.
7// This means: add a new tool, replace a tool, sandbox a tool in WASM,
8// the executor never changes.
9
10use std::collections::HashMap;
11use thiserror::Error;
12
13// ── ToolArgs ──────────────────────────────────────────────────────────────────
14
15/// The arguments passed to a tool call.
16///
17/// A flat key-value map. Tools declare which keys they expect
18/// and validate them inside `execute()`.
19/// The executor passes whatever the agent provided: no pre-filtering.
20pub type ToolArgs = HashMap<String, String>;
21
22// ── ToolOutput ────────────────────────────────────────────────────────────────
23
24/// The result of a successful tool execution.
25///
26/// Kept intentionally simple: structured output parsing belongs
27/// to the agent layer above, not the executor.
28#[derive(Debug, Clone)]
29pub struct ToolOutput {
30    /// The raw content returned by the tool.
31    pub content: String,
32}
33
34// ── ToolError ─────────────────────────────────────────────────────────────────
35
36/// Errors a tool can produce during execution.
37///
38/// These represent tool-level failures: bad args, network errors, timeouts.
39/// They are distinct from policy denials: a tool error means the tool was
40/// permitted but failed during execution. A policy denial means the tool
41/// was never called at all.
42#[derive(Debug, Error)]
43pub enum ToolError {
44    /// A required argument was missing or had an invalid value.
45    #[error("invalid argument '{arg}': {reason}")]
46    InvalidArgument { arg: String, reason: String },
47
48    /// The tool ran but encountered an error during execution.
49    #[error("execution failed: {0}")]
50    ExecutionFailed(String),
51
52    /// The tool did not complete within its allowed time.
53    #[error("timed out after {timeout_ms}ms")]
54    Timeout { timeout_ms: u64 },
55}
56
57// ── Tool trait ────────────────────────────────────────────────────────────────
58
59/// The contract for a single tool.
60///
61/// Any type that implements this can be registered and called by the executor.
62pub trait Tool: Send + Sync {
63    /// The unique name used to identify this tool in config and agent output.
64    /// Must be stable: changing this is a breaking change.
65    fn name(&self) -> &str;
66
67    /// Execute the tool with the given arguments.
68    fn execute(&self, args: &ToolArgs) -> Result<ToolOutput, ToolError>;
69}
70
71// ── ToolCallError ─────────────────────────────────────────────────────────────
72
73/// What can go wrong when the executor calls a tool via the registry.
74///
75/// Two cases only:
76/// - The tool name is not registered (config allows it but nobody registered it)
77/// - The tool is registered but failed during execution
78///
79/// Policy denial is not represented here: that stops the executor
80/// before `call()` is ever invoked.
81#[derive(Debug, Error)]
82pub enum ToolCallError {
83    /// The tool name was not found in the registry.
84    #[error("tool '{tool_name}' is not registered")]
85    NotFound { tool_name: String },
86
87    /// The tool was found but failed during execution.
88    #[error("tool '{tool_name}' failed: {source}")]
89    Execution {
90        tool_name: String,
91        #[source]
92        source: ToolError,
93    },
94}
95
96// ── ToolExecutor trait ────────────────────────────────────────────────────────
97
98/// The contract for a collection of tools.
99///
100/// The executor programs against this: not against ToolRegistry directly.
101/// This separation means ToolRegistry can live in nanny-tools without
102/// nanny-core needing to import it.
103///
104/// Implementations: ToolRegistry in nanny-tools.
105/// In tests: inline NoOpToolExecutor or similar test doubles.
106pub trait ToolExecutor {
107    /// Execute a tool by name with the given arguments.
108    ///
109    /// Returns `Err(ToolCallError::NotFound)` if the tool is not registered.
110    /// Returns `Err(ToolCallError::Execution)` if the tool fails.
111    fn call(&self, name: &str, args: &ToolArgs) -> Result<ToolOutput, ToolCallError>;
112}