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}