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}