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.
62/// Tools declare their own cost — the executor charges that amount
63/// when the tool is called successfully.
64pub trait Tool: Send + Sync {
65 /// The unique name used to identify this tool in config and agent output.
66 /// Must be stable — changing this is a breaking change.
67 fn name(&self) -> &str;
68
69 /// Cost units charged when this tool is called successfully.
70 ///
71 /// No charge on failure — the budget is only spent when work is done.
72 fn declared_cost(&self) -> u64;
73
74 /// Execute the tool with the given arguments.
75 ///
76 /// Returns `Ok(ToolOutput)` on success — cost is then charged.
77 /// Returns `Err(ToolError)` on failure — no cost is charged.
78 fn execute(&self, args: &ToolArgs) -> Result<ToolOutput, ToolError>;
79}
80
81// ── ToolCallError ─────────────────────────────────────────────────────────────
82
83/// What can go wrong when the executor calls a tool via the registry.
84///
85/// Two cases only:
86/// - The tool name is not registered (config allows it but nobody registered it)
87/// - The tool is registered but failed during execution
88///
89/// Policy denial is not represented here — that stops the executor
90/// before `call()` is ever invoked.
91#[derive(Debug, Error)]
92pub enum ToolCallError {
93 /// The tool name was not found in the registry.
94 #[error("tool '{tool_name}' is not registered")]
95 NotFound { tool_name: String },
96
97 /// The tool was found but failed during execution.
98 #[error("tool '{tool_name}' failed: {source}")]
99 Execution {
100 tool_name: String,
101 #[source]
102 source: ToolError,
103 },
104}
105
106// ── ToolExecutor trait ────────────────────────────────────────────────────────
107
108/// The contract for a collection of tools.
109///
110/// The executor programs against this — not against ToolRegistry directly.
111/// This separation means ToolRegistry can live in nanny-tools without
112/// nanny-core needing to import it.
113///
114/// Implementations: ToolRegistry in nanny-tools.
115/// In tests: inline NoOpToolExecutor or similar test doubles.
116pub trait ToolExecutor {
117 /// Execute a tool by name with the given arguments.
118 ///
119 /// Returns `Err(ToolCallError::NotFound)` if the tool is not registered.
120 /// Returns `Err(ToolCallError::Execution)` if the tool fails.
121 fn call(&self, name: &str, args: &ToolArgs) -> Result<ToolOutput, ToolCallError>;
122
123 /// Return the declared cost for a named tool, if it exists.
124 ///
125 /// Used by the executor to charge the ledger after a successful call.
126 /// Returns `None` if the tool is not registered.
127 fn declared_cost(&self, name: &str) -> Option<u64>;
128}