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 { reason: String },
83}
84
85/// An entry in the tool registry, pairing a tool with its availability status.
86pub struct ToolRegistryEntry {
87    pub tool: Box<dyn Tool>,
88    pub available: bool,
89}
90impl ToolRegistryEntry {
91    /// Create a new registry entry with explicit availability.
92    pub fn new(tool: Box<dyn Tool>, available: bool) -> Self {
93        Self { tool, available }
94    }
95
96    /// Whether the tool is currently available for execution.
97    pub fn is_available(&self) -> bool {
98        self.available
99    }
100
101    /// Create a new registry entry with the tool available.
102    pub fn new_available(tool: Box<dyn Tool>) -> Self {
103        Self::new(tool, true)
104    }
105
106    /// Create a new registry entry with the tool unavailable.
107    pub fn new_unavailable(tool: Box<dyn Tool>) -> Self {
108        Self::new(tool, false)
109    }
110}
111
112/// Raw tool registry (no security checks).
113///
114/// When the `security` feature is enabled, [`ToolRegistry`] aliases to
115/// [`GuardedToolRegistry`](crate::security::registry::GuardedToolRegistry)
116/// instead, which wraps this registry with policy checks and audit logging.
117#[doc(hidden)]
118pub struct RawToolRegistry {
119    tools: HashMap<String, ToolRegistryEntry>,
120}
121
122impl Default for RawToolRegistry {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128impl RawToolRegistry {
129    pub fn new() -> Self {
130        Self {
131            tools: HashMap::new(),
132        }
133    }
134
135    pub fn add_tool(&mut self, tool: Box<dyn Tool>) {
136        self.tools.insert(
137            tool.name().to_string(),
138            ToolRegistryEntry::new_available(tool),
139        );
140    }
141    pub fn get_tool(&self, name: &str) -> Option<&ToolRegistryEntry> {
142        self.tools.get(name)
143    }
144    pub fn remove_tool(&mut self, name: &str) {
145        self.tools.remove(name);
146    }
147    pub fn tool_exists(&self, name: &str) -> bool {
148        self.tools.contains_key(name)
149    }
150    pub fn tool_count(&self) -> usize {
151        self.tools.len()
152    }
153    pub fn get_all_tools(&self) -> &HashMap<String, ToolRegistryEntry> {
154        &self.tools
155    }
156    pub fn available_tools_json(&self) -> JsonValue {
157        self.tools
158            .values()
159            .filter_map(|tool| {
160                if tool.is_available() {
161                    Some(tool.tool.schema())
162                } else {
163                    None
164                }
165            })
166            .collect::<Vec<_>>()
167            .into()
168    }
169    pub async fn call_tool(&self, name: &str, args: JsonValue) -> Result<String, ToolCallError> {
170        if let Some(tool) = self.get_tool(name) {
171            if tool.is_available() {
172                tool.tool.execute(args).await
173            } else {
174                Err(ToolCallError::ToolUnavailable(name.to_string()))
175            }
176        } else {
177            Err(ToolCallError::ToolNotFound(name.to_string()))
178        }
179    }
180}
181
182/// The active tool registry type.
183///
184/// When the `security` feature is enabled, this aliases to
185/// [`GuardedToolRegistry`](crate::security::registry::GuardedToolRegistry)
186/// which enforces tool policies and logs audit events on every tool call.
187/// Without `security`, it is the raw registry with no policy checks.
188#[cfg(feature = "security")]
189pub use crate::security::registry::GuardedToolRegistry as ToolRegistry;
190
191#[cfg(not(feature = "security"))]
192pub use RawToolRegistry as ToolRegistry;
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn rejected_error_display() {
200        let e = ToolCallError::Rejected {
201            reason: "access denied".into(),
202        };
203        let msg = format!("{e}");
204        assert!(msg.contains("access denied"), "msg: {msg}");
205    }
206
207    #[test]
208    fn approval_required_error_display() {
209        let e = ToolCallError::ApprovalRequired {
210            call_id: "c1".into(),
211            tool_name: "shell".into(),
212            reason: "needs approval".into(),
213        };
214        let msg = format!("{e}");
215        assert!(msg.contains("shell"), "msg: {msg}");
216        assert!(msg.contains("approval"), "msg: {msg}");
217    }
218}