Skip to main content

hanzo_llm_mcp/
tools.rs

1use image::DynamicImage;
2use serde::{Deserialize, Serialize};
3use serde_json::{json, Value};
4use std::collections::HashMap;
5use std::fmt;
6use std::path::PathBuf;
7use std::sync::Arc;
8
9#[derive(Clone, Debug, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum AgentToolSource {
12    BuiltIn,
13    User,
14    Mcp,
15    External,
16}
17
18#[derive(Clone, Debug, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum AgentToolKind {
21    CodeExecution,
22    WebSearch,
23    File,
24    Custom,
25    External,
26}
27
28#[derive(Clone, Debug, Serialize, Deserialize)]
29pub struct AgentToolMetadata {
30    pub source: AgentToolSource,
31    pub kind: AgentToolKind,
32    pub label: String,
33}
34
35#[derive(Clone, Debug)]
36pub struct AgentToolApprovalRequest {
37    pub approval_id: String,
38    pub session_id: String,
39    pub round: usize,
40    pub tool: AgentToolMetadata,
41    pub arguments: Value,
42}
43
44pub type AgentToolApprovalNotifier = dyn Fn(AgentToolApprovalRequest) + Send + Sync + 'static;
45
46#[derive(Clone, Debug)]
47pub struct CodeExecutionApprovalRequest {
48    pub approval_id: String,
49    pub session_id: String,
50    pub round: usize,
51    pub tool_name: String,
52    pub code: String,
53    pub outputs: Vec<String>,
54    pub working_directory: Option<PathBuf>,
55}
56
57pub type CodeExecutionApprovalNotifier =
58    dyn Fn(CodeExecutionApprovalRequest) + Send + Sync + 'static;
59
60#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "kebab-case")]
62pub enum AgentPermission {
63    #[default]
64    Auto,
65    Ask,
66    Deny,
67}
68
69impl AgentPermission {
70    pub fn as_str(self) -> &'static str {
71        match self {
72            Self::Auto => "auto",
73            Self::Ask => "ask",
74            Self::Deny => "deny",
75        }
76    }
77
78    pub fn strictest(self, other: Self) -> Self {
79        match (self, other) {
80            (Self::Deny, _) | (_, Self::Deny) => Self::Deny,
81            (Self::Ask, _) | (_, Self::Ask) => Self::Ask,
82            (Self::Auto, Self::Auto) => Self::Auto,
83        }
84    }
85}
86
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "kebab-case")]
89pub enum CodeExecutionPermission {
90    #[default]
91    Auto,
92    Ask,
93    Deny,
94}
95
96impl CodeExecutionPermission {
97    pub fn as_str(self) -> &'static str {
98        match self {
99            Self::Auto => "auto",
100            Self::Ask => "ask",
101            Self::Deny => "deny",
102        }
103    }
104
105    pub fn strictest(self, other: Self) -> Self {
106        match (self, other) {
107            (Self::Deny, _) | (_, Self::Deny) => Self::Deny,
108            (Self::Ask, _) | (_, Self::Ask) => Self::Ask,
109            (Self::Auto, Self::Auto) => Self::Auto,
110        }
111    }
112}
113
114impl From<CodeExecutionPermission> for AgentPermission {
115    fn from(value: CodeExecutionPermission) -> Self {
116        match value {
117            CodeExecutionPermission::Auto => Self::Auto,
118            CodeExecutionPermission::Ask => Self::Ask,
119            CodeExecutionPermission::Deny => Self::Deny,
120        }
121    }
122}
123
124impl From<AgentPermission> for CodeExecutionPermission {
125    fn from(value: AgentPermission) -> Self {
126        match value {
127            AgentPermission::Auto => Self::Auto,
128            AgentPermission::Ask => Self::Ask,
129            AgentPermission::Deny => Self::Deny,
130        }
131    }
132}
133
134/// Context provided to tool callbacks by the agentic loop.
135#[derive(Clone, Default)]
136pub struct ToolCallContext {
137    /// Use to key per-session state across invocations.
138    pub session_id: Option<String>,
139    pub round: Option<usize>,
140    pub tool_name: Option<String>,
141    pub agent_permission: Option<AgentPermission>,
142    pub agent_approval_notifier: Option<Arc<AgentToolApprovalNotifier>>,
143    pub code_execution_permission: Option<CodeExecutionPermission>,
144    pub code_execution_approval_notifier: Option<Arc<CodeExecutionApprovalNotifier>>,
145}
146
147impl fmt::Debug for ToolCallContext {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        f.debug_struct("ToolCallContext")
150            .field("session_id", &self.session_id)
151            .field("round", &self.round)
152            .field("tool_name", &self.tool_name)
153            .field("agent_permission", &self.agent_permission)
154            .field(
155                "agent_approval_notifier",
156                &self.agent_approval_notifier.is_some(),
157            )
158            .field("code_execution_permission", &self.code_execution_permission)
159            .field(
160                "code_execution_approval_notifier",
161                &self.code_execution_approval_notifier.is_some(),
162            )
163            .finish()
164    }
165}
166
167/// Custom tool callback. Receives the called function and returns the tool output as a string.
168pub type ToolCallback =
169    dyn Fn(&CalledFunction, &ToolCallContext) -> anyhow::Result<String> + Send + Sync;
170
171/// Callback that can return multimodal output (text + images).
172pub type MultimodalToolCallback =
173    dyn Fn(&CalledFunction, &ToolCallContext) -> anyhow::Result<ToolOutput> + Send + Sync;
174
175/// A file produced by a tool, carried out of band from the text response. The engine converts it to a typed `File`.
176#[derive(Debug, Clone)]
177pub struct ToolFile {
178    pub name: String,
179    pub format: String,
180    pub mime_type: Option<String>,
181    /// Set for utf-8 readable files.
182    pub text: Option<String>,
183    /// Set for binary files.
184    pub data_base64: Option<String>,
185    pub size_bytes: u64,
186    /// Set when the file was requested but not produced or failed to read.
187    pub error: Option<String>,
188}
189
190impl ToolFile {
191    pub fn is_text(&self) -> bool {
192        self.text.is_some()
193    }
194    pub fn is_error(&self) -> bool {
195        self.error.is_some()
196    }
197}
198
199/// Tool output: text-only or multimodal.
200pub enum ToolOutput {
201    Text(String),
202    Multimodal {
203        text: String,
204        images: Vec<DynamicImage>,
205        /// Ordered. The caller assembles these (e.g. into a `VideoInput`).
206        video_frames: Vec<DynamicImage>,
207        /// Surfaced as typed `File`s in the chat response.
208        files: Vec<ToolFile>,
209    },
210}
211
212impl From<String> for ToolOutput {
213    fn from(s: String) -> Self {
214        ToolOutput::Text(s)
215    }
216}
217
218impl ToolOutput {
219    pub fn text(&self) -> &str {
220        match self {
221            ToolOutput::Text(s) => s,
222            ToolOutput::Multimodal { text, .. } => text,
223        }
224    }
225
226    pub fn images(&self) -> &[DynamicImage] {
227        match self {
228            ToolOutput::Text(_) => &[],
229            ToolOutput::Multimodal { images, .. } => images,
230        }
231    }
232
233    pub fn video_frames(&self) -> &[DynamicImage] {
234        match self {
235            ToolOutput::Text(_) => &[],
236            ToolOutput::Multimodal { video_frames, .. } => video_frames,
237        }
238    }
239
240    pub fn files(&self) -> &[ToolFile] {
241        match self {
242            ToolOutput::Text(_) => &[],
243            ToolOutput::Multimodal { files, .. } => files,
244        }
245    }
246
247    pub fn has_multimodal(&self) -> bool {
248        match self {
249            ToolOutput::Text(_) => false,
250            ToolOutput::Multimodal {
251                images,
252                video_frames,
253                ..
254            } => !images.is_empty() || !video_frames.is_empty(),
255        }
256    }
257}
258
259/// Wraps either a text-only or multimodal tool callback.
260pub enum ToolCallbackKind {
261    /// Legacy text-only callback returning a String.
262    Text(Arc<ToolCallback>),
263    /// Multimodal callback that may return images alongside text.
264    Multimodal(Arc<MultimodalToolCallback>),
265}
266
267impl Clone for ToolCallbackKind {
268    fn clone(&self) -> Self {
269        match self {
270            ToolCallbackKind::Text(cb) => ToolCallbackKind::Text(Arc::clone(cb)),
271            ToolCallbackKind::Multimodal(cb) => ToolCallbackKind::Multimodal(Arc::clone(cb)),
272        }
273    }
274}
275
276/// A tool callback with its associated Tool definition.
277#[derive(Clone)]
278pub struct ToolCallbackWithTool {
279    pub callback: ToolCallbackKind,
280    pub tool: Tool,
281}
282
283/// Collection of callbacks keyed by tool name.
284pub type ToolCallbacks = HashMap<String, Arc<ToolCallback>>;
285
286/// Collection of callbacks with their tool definitions keyed by tool name.
287pub type ToolCallbacksWithTools = HashMap<String, ToolCallbackWithTool>;
288
289/// Type of tool
290#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
291#[derive(Clone, Debug, Deserialize, Serialize)]
292pub enum ToolType {
293    #[serde(rename = "function")]
294    Function,
295}
296
297/// Function definition for a tool
298#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
299#[derive(Clone, Debug, Deserialize, Serialize)]
300pub struct Function {
301    pub description: Option<String>,
302    pub name: String,
303    #[serde(alias = "arguments")]
304    pub parameters: Option<HashMap<String, Value>>,
305    /// When `true`, the tool's `parameters` JSON schema is enforced on the
306    /// generated arguments via constrained decoding (llguidance).
307    #[serde(default, skip_serializing_if = "Option::is_none")]
308    pub strict: Option<bool>,
309}
310
311impl Function {
312    /// Returns the parameters as a JSON Schema [`Value`] when strict mode is
313    /// enabled.  Returns `None` when strict is absent or `false`.
314    pub fn strict_parameters_schema(&self) -> Option<Value> {
315        if self.strict != Some(true) {
316            return None;
317        }
318        match &self.parameters {
319            Some(p) => match serde_json::to_value(p) {
320                Ok(v) => Some(v),
321                Err(e) => {
322                    tracing::warn!(
323                        "Failed to serialize parameters for strict tool `{}`: {e}. \
324                         Falling back to generic object schema.",
325                        self.name,
326                    );
327                    Some(json!({"type": "object"}))
328                }
329            },
330            None => {
331                tracing::warn!(
332                    "Tool `{}` has strict: true but no parameters schema defined. \
333                     Cannot enforce strict mode; falling back to generic object schema.",
334                    self.name,
335                );
336                Some(json!({"type": "object"}))
337            }
338        }
339    }
340}
341
342/// Tool definition
343#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
344#[derive(Clone, Debug, Deserialize, Serialize)]
345pub struct Tool {
346    #[serde(rename = "type")]
347    pub tp: ToolType,
348    pub function: Function,
349}
350
351/// Called function with name and arguments
352#[cfg_attr(feature = "pyo3_macros", pyo3::pyclass)]
353#[cfg_attr(feature = "pyo3_macros", pyo3(get_all))]
354#[derive(Clone, Debug, Serialize, Deserialize)]
355pub struct CalledFunction {
356    pub name: String,
357    pub arguments: String,
358}