Skip to main content

oxi_agent/tools/
tool_definition_wrapper.rs

1/// Tool definition wrapping utilities
2/// Provides adapters for converting between tool representations.
3use crate::tools::{AgentTool, AgentToolResult, ToolContext, ToolError};
4use crate::types::ToolDefinition;
5use async_trait::async_trait;
6use serde_json::Value;
7use std::sync::Arc;
8use tokio::sync::oneshot;
9
10/// A dynamically-constructed tool from a definition and execution function.
11///
12/// This is useful for wrapping external tool definitions (e.g., from extensions
13/// or plugins) into the `AgentTool` interface without requiring a dedicated struct.
14pub struct DynamicTool {
15    name: String,
16    label: String,
17    description: String,
18    parameters: Value,
19    #[allow(clippy::type_complexity)]
20    execute_fn: Arc<
21        dyn Fn(
22                &str,
23                Value,
24                Option<oneshot::Receiver<()>>,
25            ) -> std::pin::Pin<
26                Box<dyn std::future::Future<Output = Result<AgentToolResult, ToolError>> + Send>,
27            > + Send
28            + Sync,
29    >,
30}
31
32impl DynamicTool {
33    /// Create a new dynamic tool from components.
34    pub fn new(
35        name: impl Into<String>,
36        label: impl Into<String>,
37        description: impl Into<String>,
38        parameters: Value,
39        execute_fn: impl Fn(
40                &str,
41                Value,
42                Option<oneshot::Receiver<()>>,
43            ) -> std::pin::Pin<
44                Box<dyn std::future::Future<Output = Result<AgentToolResult, ToolError>> + Send>,
45            > + Send
46            + Sync
47            + 'static,
48    ) -> Self {
49        Self {
50            name: name.into(),
51            label: label.into(),
52            description: description.into(),
53            parameters,
54            execute_fn: Arc::new(execute_fn),
55        }
56    }
57
58    /// Create a `DynamicTool` from a `ToolDefinition` with a simple execution function.
59    pub fn from_definition(
60        def: ToolDefinition,
61        execute_fn: impl Fn(
62                &str,
63                Value,
64                Option<oneshot::Receiver<()>>,
65            ) -> std::pin::Pin<
66                Box<dyn std::future::Future<Output = Result<AgentToolResult, ToolError>> + Send>,
67            > + Send
68            + Sync
69            + 'static,
70    ) -> Self {
71        let name_for_label = def.name.clone();
72        let schema =
73            serde_json::to_value(&def.input_schema).unwrap_or(Value::Object(Default::default()));
74        Self {
75            name: def.name,
76            label: name_for_label, // Use name as label fallback
77            description: def.description,
78            parameters: schema,
79            execute_fn: Arc::new(execute_fn),
80        }
81    }
82}
83
84#[async_trait]
85impl AgentTool for DynamicTool {
86    fn name(&self) -> &str {
87        &self.name
88    }
89
90    fn label(&self) -> &str {
91        &self.label
92    }
93
94    fn description(&self) -> &str {
95        &self.description
96    }
97
98    fn parameters_schema(&self) -> Value {
99        self.parameters.clone()
100    }
101
102    async fn execute(
103        &self,
104        tool_call_id: &str,
105        params: Value,
106        signal: Option<oneshot::Receiver<()>>,
107        _ctx: &ToolContext,
108    ) -> Result<AgentToolResult, ToolError> {
109        (self.execute_fn)(tool_call_id, params, signal).await
110    }
111}
112
113/// Trait for tool definitions that can be wrapped into `AgentTool`.
114pub trait ToolDefinitionLike: Send + Sync {
115    /// TODO: document this function.
116    fn tool_name(&self) -> &str;
117    /// TODO: document this function.
118    fn tool_label(&self) -> &str;
119    /// TODO: document this function.
120    fn tool_description(&self) -> &str;
121    /// TODO: document this function.
122    fn tool_parameters(&self) -> Value;
123    /// TODO: document this function.
124    fn tool_execute(
125        &self,
126        tool_call_id: &str,
127        params: Value,
128        signal: Option<oneshot::Receiver<()>>,
129    ) -> std::pin::Pin<
130        Box<dyn std::future::Future<Output = Result<AgentToolResult, ToolError>> + Send>,
131    >;
132}
133
134/// Wrap a `ToolDefinitionLike` into an `Arc<dyn AgentTool>`.
135pub fn wrap_tool_definition<T: ToolDefinitionLike + 'static>(def: T) -> Arc<dyn AgentTool> {
136    Arc::new(DefinitionWrapper(def))
137}
138
139/// Internal wrapper that adapts `ToolDefinitionLike` to `AgentTool`.
140struct DefinitionWrapper<T>(T);
141
142#[async_trait]
143impl<T: ToolDefinitionLike + 'static> AgentTool for DefinitionWrapper<T> {
144    fn name(&self) -> &str {
145        self.0.tool_name()
146    }
147
148    fn label(&self) -> &str {
149        self.0.tool_label()
150    }
151
152    fn description(&self) -> &str {
153        self.0.tool_description()
154    }
155
156    fn parameters_schema(&self) -> Value {
157        self.0.tool_parameters()
158    }
159
160    async fn execute(
161        &self,
162        tool_call_id: &str,
163        params: Value,
164        signal: Option<oneshot::Receiver<()>>,
165        _ctx: &ToolContext,
166    ) -> Result<AgentToolResult, ToolError> {
167        self.0.tool_execute(tool_call_id, params, signal).await
168    }
169}
170
171/// Create a minimal `ToolDefinition` from an `AgentTool`.
172///
173/// This is useful for internal registries that operate on definitions
174/// even when tools are provided as trait objects.
175pub fn create_tool_definition_from_agent_tool(tool: &dyn AgentTool) -> ToolDefinition {
176    let schema_map: std::collections::HashMap<String, Value> = {
177        let schema = tool.parameters_schema();
178        if let Value::Object(map) = schema {
179            map.into_iter().collect()
180        } else {
181            let mut map = std::collections::HashMap::new();
182            map.insert("type".to_string(), Value::String("object".to_string()));
183            map
184        }
185    };
186
187    ToolDefinition::new(tool.name(), tool.description(), schema_map)
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_create_tool_definition_from_agent_tool() {
196        struct TestTool;
197
198        #[async_trait]
199        impl AgentTool for TestTool {
200            fn name(&self) -> &str {
201                "test_tool"
202            }
203            fn label(&self) -> &str {
204                "Test Tool"
205            }
206            fn description(&self) -> &str {
207                "A test tool"
208            }
209            fn parameters_schema(&self) -> Value {
210                serde_json::json!({
211                    "type": "object",
212                    "properties": {
213                        "input": { "type": "string" }
214                    }
215                })
216            }
217            async fn execute(
218                &self,
219                _tool_call_id: &str,
220                _params: Value,
221                _signal: Option<oneshot::Receiver<()>>,
222                _ctx: &ToolContext,
223            ) -> Result<AgentToolResult, ToolError> {
224                Ok(AgentToolResult::success("test"))
225            }
226        }
227
228        let tool = TestTool;
229        let def = create_tool_definition_from_agent_tool(&tool);
230        assert_eq!(def.name, "test_tool");
231        assert_eq!(def.description, "A test tool");
232    }
233
234    #[test]
235    fn test_dynamic_tool_creation() {
236        let tool = DynamicTool::new(
237            "dynamic_test",
238            "Dynamic Test",
239            "A dynamic test tool",
240            serde_json::json!({"type": "object"}),
241            |_id, _params, _signal| {
242                Box::pin(async { Ok(AgentToolResult::success("dynamic result")) })
243            },
244        );
245
246        assert_eq!(tool.name(), "dynamic_test");
247        assert_eq!(tool.label(), "Dynamic Test");
248        assert_eq!(tool.description(), "A dynamic test tool");
249    }
250
251    #[tokio::test]
252    async fn test_dynamic_tool_execution() {
253        let tool = DynamicTool::new(
254            "exec_test",
255            "Exec Test",
256            "Exec test tool",
257            serde_json::json!({"type": "object"}),
258            |_id, params, _signal| {
259                let result = format!("got: {}", params);
260                Box::pin(async move { Ok(AgentToolResult::success(result)) })
261            },
262        );
263
264        let result = tool
265            .execute(
266                "call_1",
267                serde_json::json!({"key": "value"}),
268                None,
269                &ToolContext::default(),
270            )
271            .await;
272        assert!(result.is_ok());
273        assert_eq!(result.unwrap().output, "got: {\"key\":\"value\"}");
274    }
275
276    struct MockDefLike;
277
278    impl ToolDefinitionLike for MockDefLike {
279        fn tool_name(&self) -> &str {
280            "mock_def"
281        }
282        fn tool_label(&self) -> &str {
283            "Mock Definition"
284        }
285        fn tool_description(&self) -> &str {
286            "Mock description"
287        }
288        fn tool_parameters(&self) -> Value {
289            serde_json::json!({"type": "object"})
290        }
291        fn tool_execute(
292            &self,
293            _tool_call_id: &str,
294            _params: Value,
295            _signal: Option<oneshot::Receiver<()>>,
296        ) -> std::pin::Pin<
297            Box<dyn std::future::Future<Output = Result<AgentToolResult, ToolError>> + Send>,
298        > {
299            Box::pin(async { Ok(AgentToolResult::success("mock result")) })
300        }
301    }
302
303    #[test]
304    fn test_wrap_tool_definition() {
305        let wrapped = wrap_tool_definition(MockDefLike);
306        assert_eq!(wrapped.name(), "mock_def");
307        assert_eq!(wrapped.label(), "Mock Definition");
308    }
309
310    #[tokio::test]
311    async fn test_wrapped_tool_execution() {
312        let wrapped = wrap_tool_definition(MockDefLike);
313        let result = wrapped
314            .execute("call_1", Value::Null, None, &ToolContext::default())
315            .await;
316        assert!(result.is_ok());
317        assert_eq!(result.unwrap().output, "mock result");
318    }
319}