Skip to main content

open_agent/tools/
tool.rs

1/// A named asynchronous function with a JSON input schema.
2///
3/// Use [`tool()`] to build a tool incrementally or [`Tool::new`] to supply a schema
4/// directly. Clones share the handler through [`Arc`]. The handler is responsible
5/// for validating arguments; the schema is sent to the model as its description.
6#[derive(Clone)]
7pub struct Tool {
8    /// Name used to match model tool calls to this handler.
9    name: String,
10
11    /// Description sent to the model.
12    description: String,
13
14    /// Normalized JSON input schema sent to the model.
15    input_schema: Value,
16
17    /// Shared asynchronous handler; clones of a tool share the same closure.
18    handler: ToolHandler,
19}
20
21impl Tool {
22    /// Creates a tool and normalizes its input schema.
23    ///
24    /// Accepted schema forms:
25    ///
26    /// - A full object containing `type` and `properties` is passed through unchanged.
27    /// - A property map such as `{"query": "string"}` makes each property required.
28    /// - Extended properties may include `optional`, `required`, and `default`.
29    ///   Explicit `required: true` wins; otherwise `optional: true`, `required: false`,
30    ///   or a default makes the property optional. The two flags are removed afterward.
31    ///
32    /// Type aliases include `str`, `int`, `i32`/`i64`/`u32`/`u64`, `float`/`f32`/`f64`,
33    /// `bool`, `list`/`vec`, and `dict`/`map`. Unknown names become `string`. Non-object schema
34    /// inputs produce an empty object schema. The handler still validates arguments.
35    ///
36    /// # Example
37    ///
38    /// ```rust
39    /// use open_agent::Tool;
40    /// use serde_json::json;
41    ///
42    /// let search = Tool::new(
43    ///     "search",
44    ///     "Search documents",
45    ///     json!({
46    ///         "query": "string",
47    ///         "limit": {"type": "integer", "default": 10}
48    ///     }),
49    ///     |args| async move {
50    ///         let limit = args["limit"].as_u64().unwrap_or(10);
51    ///         Ok(json!({"query": args["query"], "limit": limit}))
52    ///     },
53    /// );
54    /// ```
55    pub fn new<F, Fut>(
56        name: impl Into<String>,
57        description: impl Into<String>,
58        input_schema: Value,
59        handler: F,
60    ) -> Self
61    where
62        F: Fn(Value) -> Fut + Send + Sync + 'static,
63        Fut: Future<Output = Result<Value>> + Send + 'static,
64    {
65        let name = name.into();
66        let description = description.into();
67
68        let input_schema = convert_schema_to_openai(input_schema);
69
70        Self {
71            name,
72            description,
73            input_schema,
74            handler: Arc::new(move |args| Box::pin(handler(args))),
75        }
76    }
77
78    /// Runs the handler with the supplied JSON arguments.
79    ///
80    /// The handler validates arguments; this method does not enforce the input schema.
81    ///
82    /// # Errors
83    ///
84    /// Propagates the handler's error unchanged. Automatic execution converts that
85    /// error to a tool-result JSON object so the model can respond to it.
86    pub async fn execute(&self, arguments: Value) -> Result<Value> {
87        (self.handler)(arguments).await
88    }
89
90    /// Returns the tool name, description, and schema in OpenAI function-calling format.
91    ///
92    /// The returned JSON contains no handler. Anthropic translation extracts the same
93    /// metadata when building its request.
94    pub fn to_openai_format(&self) -> Value {
95        serde_json::json!({
96            "type": "function",
97            "function": {
98                "name": self.name,
99                "description": self.description,
100                "parameters": self.input_schema
101            }
102        })
103    }
104
105    /// Returns the tool's name.
106    pub fn name(&self) -> &str {
107        &self.name
108    }
109
110    /// Returns the tool's description.
111    pub fn description(&self) -> &str {
112        &self.description
113    }
114
115    /// Returns a reference to the tool's input schema.
116    pub fn input_schema(&self) -> &Value {
117        &self.input_schema
118    }
119}
120
121/// Shows tool metadata and omits the handler.
122impl std::fmt::Debug for Tool {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        f.debug_struct("Tool")
125            .field("name", &self.name)
126            .field("description", &self.description)
127            .field("input_schema", &self.input_schema)
128            // Handler is intentionally omitted - it's not debuggable
129            .finish()
130    }
131}