Skip to main content

funera_core/re_act/
tool.rs

1#![cfg(feature = "tool")]
2
3use std::{collections::HashMap, fmt::Display};
4
5use anyhow::Result;
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use serde_json::Value as JsonValue;
9use thiserror::Error;
10
11/// The type of a tool, as communicated to the LLM.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub enum ToolType {
14    /// Standard OpenAI-compatible function tool.
15    Function,
16}
17
18impl Display for ToolType {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        write!(f, "function")
21    }
22}
23
24/// A callable tool exposed to the LLM agent.
25///
26/// Implement this trait to define custom tools. The framework will expose
27/// the tool's [`schema`](Tool::schema) to the LLM and invoke
28/// [`execute`](Tool::execute) when the LLM requests it.
29#[async_trait]
30pub trait Tool: Send + Sync {
31    /// Unique name for this tool (e.g. `"read"`, `"shell"`).
32    fn name(&self) -> &str;
33
34    /// Human-readable description sent to the LLM.
35    fn description(&self) -> &str;
36
37    /// Execute the tool with the given JSON arguments.
38    ///
39    /// Returns a string result on success, or a [`ToolCallError`] on failure.
40    async fn execute(&self, args: JsonValue) -> Result<String, ToolCallError>;
41
42    /// Returns the tool type (defaults to [`ToolType::Function`]).
43    fn get_type(&self) -> ToolType {
44        ToolType::Function
45    }
46
47    /// Returns the JSON schema describing this tool's parameters.
48    ///
49    /// This is sent to the LLM so it can generate well-formed invocations.
50    fn schema(&self) -> JsonValue;
51}
52
53/// Errors that can occur during tool execution.
54#[derive(Debug, Error)]
55pub enum ToolCallError {
56    /// The arguments did not match the expected schema.
57    #[error("parameter mismatch: {0}")]
58    ParameterMismatch(JsonValue),
59
60    /// The tool encountered a runtime error during execution.
61    #[error("tool execution error: {0}")]
62    ToolExecutionError(#[from] anyhow::Error),
63
64    /// The tool exists but is currently unavailable (e.g. disabled by policy).
65    #[error("tool unavailable: {0}")]
66    ToolUnavailable(String),
67
68    /// No tool with the given name is registered.
69    #[error("tool not found: {0}")]
70    ToolNotFound(String),
71
72    /// The tool call requires user approval before proceeding.
73    #[error("approval required for {tool_name}: {reason}")]
74    ApprovalRequired {
75        call_id: String,
76        tool_name: String,
77        reason: String,
78    },
79
80    /// The tool call was rejected after an approval request was denied.
81    #[error("tool call rejected: {reason}")]
82    Rejected {
83        reason: String,
84    },
85}
86
87/// An entry in the tool registry, pairing a tool with its availability status.
88pub struct ToolRegistryEntry {
89    pub tool: Box<dyn Tool>,
90    pub available: bool,
91}
92impl ToolRegistryEntry {
93    /// Create a new registry entry with explicit availability.
94    pub fn new(tool: Box<dyn Tool>, available: bool) -> Self {
95        Self { tool, available }
96    }
97
98    /// Whether the tool is currently available for execution.
99    pub fn is_available(&self) -> bool {
100        self.available
101    }
102
103    /// Create a new registry entry with the tool available.
104    pub fn new_available(tool: Box<dyn Tool>) -> Self {
105        Self::new(tool, true)
106    }
107
108    /// Create a new registry entry with the tool unavailable.
109    pub fn new_unavailable(tool: Box<dyn Tool>) -> Self {
110        Self::new(tool, false)
111    }
112}
113
114/// Raw tool registry (no security checks).
115///
116/// When the `security` feature is enabled, [`ToolRegistry`] aliases to
117/// [`GuardedToolRegistry`](crate::security::registry::GuardedToolRegistry)
118/// instead, which wraps this registry with policy checks and audit logging.
119#[doc(hidden)]
120pub struct RawToolRegistry {
121    tools: HashMap<String, ToolRegistryEntry>,
122}
123
124impl Default for RawToolRegistry {
125    fn default() -> Self {
126        Self::new()
127    }
128}
129
130impl RawToolRegistry {
131    pub fn new() -> Self {
132        Self {
133            tools: HashMap::new(),
134        }
135    }
136
137    pub fn add_tool(&mut self, tool: Box<dyn Tool>) {
138        self.tools.insert(
139            tool.name().to_string(),
140            ToolRegistryEntry::new_available(tool),
141        );
142    }
143    pub fn get_tool(&self, name: &str) -> Option<&ToolRegistryEntry> {
144        self.tools.get(name)
145    }
146    pub fn remove_tool(&mut self, name: &str) {
147        self.tools.remove(name);
148    }
149    pub fn tool_exists(&self, name: &str) -> bool {
150        self.tools.contains_key(name)
151    }
152    pub fn tool_count(&self) -> usize {
153        self.tools.len()
154    }
155    pub fn get_all_tools(&self) -> &HashMap<String, ToolRegistryEntry> {
156        &self.tools
157    }
158    pub fn available_tools_json(&self) -> JsonValue {
159        self.tools
160            .values()
161            .filter_map(|tool| {
162                if tool.is_available() {
163                    Some(tool.tool.schema())
164                } else {
165                    None
166                }
167            })
168            .collect::<Vec<_>>()
169            .into()
170    }
171    pub async fn call_tool(&self, name: &str, args: JsonValue) -> Result<String, ToolCallError> {
172        if let Some(tool) = self.get_tool(name) {
173            if tool.is_available() {
174                tool.tool.execute(args).await
175            } else {
176                Err(ToolCallError::ToolUnavailable(name.to_string()))
177            }
178        } else {
179            Err(ToolCallError::ToolNotFound(name.to_string()))
180        }
181    }
182}
183
184/// The active tool registry type.
185///
186/// When the `security` feature is enabled, this aliases to
187/// [`GuardedToolRegistry`](crate::security::registry::GuardedToolRegistry)
188/// which enforces tool policies and logs audit events on every tool call.
189/// Without `security`, it is the raw registry with no policy checks.
190#[cfg(feature = "security")]
191pub use crate::security::registry::GuardedToolRegistry as ToolRegistry;
192
193#[cfg(not(feature = "security"))]
194pub use RawToolRegistry as ToolRegistry;
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn rejected_error_display() {
202        let e = ToolCallError::Rejected {
203            reason: "access denied".into(),
204        };
205        let msg = format!("{e}");
206        assert!(msg.contains("access denied"), "msg: {msg}");
207    }
208
209    #[test]
210    fn approval_required_error_display() {
211        let e = ToolCallError::ApprovalRequired {
212            call_id: "c1".into(),
213            tool_name: "shell".into(),
214            reason: "needs approval".into(),
215        };
216        let msg = format!("{e}");
217        assert!(msg.contains("shell"), "msg: {msg}");
218        assert!(msg.contains("approval"), "msg: {msg}");
219    }
220}