Skip to main content

ironflow_core/providers/http/tools/
tool_trait.rs

1//! The [`Tool`] trait and associated types for client-side tool execution.
2
3use std::fmt;
4use std::future::Future;
5use std::pin::Pin;
6
7use serde_json::Value;
8
9/// Output of a successful tool execution.
10#[derive(Debug, Clone)]
11pub struct ToolOutput {
12    /// Content returned to the model (typically text or JSON).
13    pub content: String,
14    /// Whether this result represents an error that should be reported to the model.
15    pub is_error: bool,
16}
17
18impl ToolOutput {
19    /// Create a successful tool output.
20    pub fn success(content: impl Into<String>) -> Self {
21        Self {
22            content: content.into(),
23            is_error: false,
24        }
25    }
26
27    /// Create an error output that will be reported to the model.
28    pub fn error(content: impl Into<String>) -> Self {
29        Self {
30            content: content.into(),
31            is_error: true,
32        }
33    }
34}
35
36/// Error returned when a tool cannot execute at all (infrastructure failure).
37///
38/// Distinguished from [`ToolOutput::is_error`] which reports tool-level errors
39/// back to the model. A `ToolError` aborts the agentic loop entirely.
40#[derive(Debug)]
41pub struct ToolError {
42    /// Human-readable error message.
43    pub message: String,
44}
45
46impl fmt::Display for ToolError {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        write!(f, "tool execution failed: {}", self.message)
49    }
50}
51
52impl std::error::Error for ToolError {}
53
54impl ToolError {
55    /// Create a new tool error.
56    pub fn new(message: impl Into<String>) -> Self {
57        Self {
58            message: message.into(),
59        }
60    }
61}
62
63/// A client-side tool that can be executed by the HTTP agent provider.
64///
65/// Implement this trait to add custom tools to the agentic loop. The tool's
66/// JSON schema is sent to the model in the OpenAI `tools` format, and when
67/// the model calls the tool, [`execute`](Tool::execute) is invoked with the
68/// parsed arguments.
69///
70/// # Examples
71///
72/// ```no_run
73/// use std::pin::Pin;
74/// use std::future::Future;
75/// use serde_json::{Value, json};
76/// use ironflow_core::providers::http::tools::{Tool, ToolOutput, ToolError};
77///
78/// struct EchoTool;
79///
80/// impl Tool for EchoTool {
81///     fn name(&self) -> &str { "echo" }
82///     fn description(&self) -> &str { "Echoes the input back" }
83///     fn parameters_schema(&self) -> Value {
84///         json!({
85///             "type": "object",
86///             "properties": {
87///                 "message": { "type": "string", "description": "The message to echo" }
88///             },
89///             "required": ["message"]
90///         })
91///     }
92///     fn execute(&self, input: Value) -> Pin<Box<dyn Future<Output = Result<ToolOutput, ToolError>> + Send + '_>> {
93///         Box::pin(async move {
94///             let msg = input.get("message")
95///                 .and_then(|v| v.as_str())
96///                 .unwrap_or("");
97///             Ok(ToolOutput::success(msg))
98///         })
99///     }
100/// }
101/// ```
102pub trait Tool: Send + Sync {
103    /// Unique name of this tool (used in tool_calls routing).
104    fn name(&self) -> &str;
105
106    /// Short description shown to the model.
107    fn description(&self) -> &str;
108
109    /// JSON Schema for the tool's input parameters.
110    fn parameters_schema(&self) -> Value;
111
112    /// Execute the tool with the given input arguments.
113    fn execute(
114        &self,
115        input: Value,
116    ) -> Pin<Box<dyn Future<Output = Result<ToolOutput, ToolError>> + Send + '_>>;
117
118    /// Whether this tool only reads state and can safely run in parallel with
119    /// other read-only tool calls of the same turn.
120    ///
121    /// Defaults to `false` (write/unknown by default, safe default).
122    ///
123    /// # Examples
124    ///
125    /// ```no_run
126    /// use std::pin::Pin;
127    /// use std::future::Future;
128    /// use serde_json::{Value, json};
129    /// use ironflow_core::providers::http::tools::{Tool, ToolOutput, ToolError};
130    ///
131    /// struct LookupTool;
132    ///
133    /// impl Tool for LookupTool {
134    ///     fn name(&self) -> &str { "lookup" }
135    ///     fn description(&self) -> &str { "Looks up a value without side effects" }
136    ///     fn parameters_schema(&self) -> Value { json!({"type": "object"}) }
137    ///     fn execute(&self, _input: Value) -> Pin<Box<dyn Future<Output = Result<ToolOutput, ToolError>> + Send + '_>> {
138    ///         Box::pin(async { Ok(ToolOutput::success("value")) })
139    ///     }
140    ///     fn read_only(&self) -> bool { true }
141    /// }
142    ///
143    /// assert!(LookupTool.read_only());
144    /// ```
145    fn read_only(&self) -> bool {
146        false
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use serde_json::json;
153
154    use super::*;
155
156    struct FakeTool;
157
158    impl Tool for FakeTool {
159        fn name(&self) -> &str {
160            "fake"
161        }
162        fn description(&self) -> &str {
163            "A fake tool for testing"
164        }
165        fn parameters_schema(&self) -> Value {
166            json!({"type": "object", "properties": {}})
167        }
168        fn execute(
169            &self,
170            _input: Value,
171        ) -> Pin<Box<dyn Future<Output = Result<ToolOutput, ToolError>> + Send + '_>> {
172            Box::pin(async { Ok(ToolOutput::success("done")) })
173        }
174    }
175
176    #[test]
177    fn tool_output_success() {
178        let output = ToolOutput::success("hello");
179        assert_eq!(output.content, "hello");
180        assert!(!output.is_error);
181    }
182
183    #[test]
184    fn tool_output_error() {
185        let output = ToolOutput::error("file not found");
186        assert_eq!(output.content, "file not found");
187        assert!(output.is_error);
188    }
189
190    #[test]
191    fn tool_error_display() {
192        let err = ToolError::new("timeout");
193        assert_eq!(err.to_string(), "tool execution failed: timeout");
194    }
195
196    #[test]
197    fn fake_tool_implements_trait() {
198        let tool = FakeTool;
199        assert_eq!(tool.name(), "fake");
200        assert_eq!(tool.description(), "A fake tool for testing");
201        assert_eq!(
202            tool.parameters_schema(),
203            json!({"type": "object", "properties": {}})
204        );
205    }
206
207    #[test]
208    fn default_read_only_is_false() {
209        assert!(!FakeTool.read_only());
210    }
211
212    #[tokio::test]
213    async fn fake_tool_execute() {
214        let tool = FakeTool;
215        let result = tool
216            .execute(json!({}))
217            .await
218            .expect("tool execution should succeed");
219        assert_eq!(result.content, "done");
220        assert!(!result.is_error);
221    }
222}