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
119#[cfg(test)]
120mod tests {
121    use serde_json::json;
122
123    use super::*;
124
125    struct FakeTool;
126
127    impl Tool for FakeTool {
128        fn name(&self) -> &str {
129            "fake"
130        }
131        fn description(&self) -> &str {
132            "A fake tool for testing"
133        }
134        fn parameters_schema(&self) -> Value {
135            json!({"type": "object", "properties": {}})
136        }
137        fn execute(
138            &self,
139            _input: Value,
140        ) -> Pin<Box<dyn Future<Output = Result<ToolOutput, ToolError>> + Send + '_>> {
141            Box::pin(async { Ok(ToolOutput::success("done")) })
142        }
143    }
144
145    #[test]
146    fn tool_output_success() {
147        let output = ToolOutput::success("hello");
148        assert_eq!(output.content, "hello");
149        assert!(!output.is_error);
150    }
151
152    #[test]
153    fn tool_output_error() {
154        let output = ToolOutput::error("file not found");
155        assert_eq!(output.content, "file not found");
156        assert!(output.is_error);
157    }
158
159    #[test]
160    fn tool_error_display() {
161        let err = ToolError::new("timeout");
162        assert_eq!(err.to_string(), "tool execution failed: timeout");
163    }
164
165    #[test]
166    fn fake_tool_implements_trait() {
167        let tool = FakeTool;
168        assert_eq!(tool.name(), "fake");
169        assert_eq!(tool.description(), "A fake tool for testing");
170        assert_eq!(
171            tool.parameters_schema(),
172            json!({"type": "object", "properties": {}})
173        );
174    }
175
176    #[tokio::test]
177    async fn fake_tool_execute() {
178        let tool = FakeTool;
179        let result = tool
180            .execute(json!({}))
181            .await
182            .expect("tool execution should succeed");
183        assert_eq!(result.content, "done");
184        assert!(!result.is_error);
185    }
186}