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}