Skip to main content

aither_core/llm/
tool.rs

1//! # LLM Tool Calling Framework
2//!
3//!
4//! Type-safe tool calling system for Large Language Models. Enables LLMs to execute external
5//! functions, access APIs, and interact with systems through well-defined interfaces.
6//!
7//! ## How a model calls a tool
8//!
9//! A [`Tool`] declares a name, a description, and an `Arguments` type whose
10//! JSON schema is sent to the provider. When the model decides to use a tool it
11//! emits a [`ToolCall`](crate::llm::ToolCall) event carrying the tool name and
12//! arguments as JSON; this crate does not execute it. Executing the call and
13//! feeding the result back is the job of a higher layer such as `aither-agent`,
14//! which keeps tool execution under the caller's control.
15//!
16//! ## Core Components
17//!
18//! - [`Tool`] - Trait for defining executable tools
19//! - [`Tools`] - Registry for managing multiple tools  
20//! - [`tool::ToolDefinition`] - Metadata and schema for LLM consumption
21//!
22//! ## Quick Start
23//!
24//! ```rust
25//! use aither_core::llm::{Tool, ToolResult};
26//! use schemars::JsonSchema;
27//! use serde::Deserialize;
28//! use std::borrow::Cow;
29//!
30//! /// Performs basic math operations.
31//! #[derive(JsonSchema, Deserialize)]
32//! struct MathArgs {
33//!     /// Operation: "add", "subtract", "multiply", "divide"
34//!     operation: String,
35//!     /// First number
36//!     a: f64,
37//!     /// Second number
38//!     b: f64,
39//! }
40//!
41//! struct Calculator;
42//!
43//! impl Tool for Calculator {
44//!     fn name(&self) -> Cow<'static, str> {
45//!         Cow::Borrowed("calculator")
46//!     }
47//!
48//!     type Arguments = MathArgs;
49//!     type Res = ToolResult;
50//!
51//!     async fn call(&self, args: Self::Arguments) -> aither_core::Result<Self::Res> {
52//!         let result = match args.operation.as_str() {
53//!             "add" => args.a + args.b,
54//!             "subtract" => args.a - args.b,
55//!             "multiply" => args.a * args.b,
56//!             "divide" if args.b != 0.0 => args.a / args.b,
57//!             "divide" => return Err(anyhow::Error::msg("Division by zero")),
58//!             _ => return Err(anyhow::Error::msg("Unknown operation")),
59//!         };
60//!         Ok(ToolResult::text(result.to_string()))
61//!     }
62//! }
63//! ```
64//!
65//! ## Schema Design Best Practices
66//!
67//! ### 1. Use Clear Documentation Comments
68//! Doc comments automatically become schema descriptions:
69//!
70//! ```rust,ignore
71//! use schemars::JsonSchema;
72//! use serde::Deserialize;
73//!
74//! #[derive(JsonSchema, Deserialize)]
75//! struct WeatherArgs {
76//!     /// City name (e.g., "London", "Tokyo", "New York")
77//!     city: String,
78//!     /// Temperature unit: "celsius" or "fahrenheit"
79//!     #[serde(default = "default_celsius")]
80//!     unit: String,
81//! }
82//!
83//! fn default_celsius() -> String { "celsius".to_string() }
84//! ```
85//!
86//! ### 2. Prefer Enums Over Strings
87//! Enums provide clear constraints for LLMs:
88//!
89//! ```rust,ignore
90//! use schemars::JsonSchema;
91//! use serde::Deserialize;
92//!
93//! #[derive(JsonSchema, Deserialize)]
94//! enum Priority { Low, Medium, High, Critical }
95//!
96//! #[derive(JsonSchema, Deserialize)]  
97//! struct TaskArgs {
98//!     /// Task description
99//!     description: String,
100//!     /// Task priority level
101//!     priority: Priority,
102//! }
103//! ```
104//!
105//! ### 3. Add Validation Constraints
106//! Use schemars attributes for validation:
107//!
108//! ```rust,ignore
109//! use schemars::JsonSchema;
110//! use serde::Deserialize;
111//!
112//! #[derive(JsonSchema, Deserialize)]
113//! struct UserArgs {
114//!     /// Valid email address
115//!     #[schemars(regex(pattern = "^[^@]+@[^@]+\\.[^@]+$"))]
116//!     email: String,
117//!     /// Age between 13 and 120
118//!     #[schemars(range(min = 13, max = 120))]
119//!     age: u8,
120//!     /// Bio text, max 500 characters
121//!     #[schemars(length(max = 500))]
122//!     bio: Option<String>,
123//! }
124//! ```
125//!
126//! ### 4. Structure Complex Data
127//! Break down complex parameters into nested types:
128//!
129//! ```rust,ignore
130//! use schemars::JsonSchema;
131//! use serde::Deserialize;
132//!
133//! #[derive(JsonSchema, Deserialize)]
134//! struct Address {
135//!     street: String,
136//!     city: String,
137//!     /// Two-letter country code (e.g., "US", "GB", "JP")
138//!     country: String,
139//! }
140//!
141//! #[derive(JsonSchema, Deserialize)]
142//! struct CreateUserArgs {
143//!     name: String,
144//!     address: Address,
145//!     /// List of user interests
146//!     #[schemars(length(max = 10))]
147//!     interests: Vec<String>,
148//! }
149//! ```
150//!
151
152// Re-export procedural macros
153#[cfg(feature = "derive")]
154pub use aither_derive::tool;
155use alloc::borrow::Cow;
156use serde_json::Value;
157
158use crate::Result;
159use alloc::format;
160use alloc::string::{String, ToString};
161use alloc::vec::Vec;
162use alloc::{boxed::Box, collections::BTreeMap};
163use core::any::Any;
164use core::fmt::{Debug, Display};
165use core::{future::Future, pin::Pin};
166pub use mime::Mime;
167use schemars::{JsonSchema, Schema, schema_for};
168use serde::{Serialize, de::DeserializeOwned};
169
170/// Final structured result from a tool execution.
171///
172/// This keeps successful content and tool-level failures in distinct variants so
173/// UIs and runtimes do not need to infer errors from free-form strings.
174///
175/// # Example
176///
177/// ```rust,ignore
178/// use aither::llm::tool::ToolResult;
179///
180/// fn search_tool() -> ToolResult {
181///     ToolResult::text("Found 3 results...")
182/// }
183///
184/// fn delete_tool() -> ToolResult {
185///     ToolResult::Done
186/// }
187/// ```
188#[derive(Debug, Clone, PartialEq, Eq)]
189#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
190#[cfg_attr(feature = "serde", serde(tag = "kind", rename_all = "snake_case"))]
191pub enum ToolResult {
192    /// Tool completed with no output to return.
193    Done,
194
195    /// UTF-8 plain text output.
196    Text {
197        /// Plain text content.
198        text: String,
199    },
200
201    /// Tab-separated tabular output.
202    Tsv {
203        /// TSV content.
204        text: String,
205    },
206
207    /// Structured JSON output.
208    Json {
209        /// JSON content.
210        value: Value,
211    },
212
213    /// Binary or media payload.
214    Binary {
215        /// MIME type of the payload (for example `image/png`).
216        mime: String,
217        /// Raw content bytes.
218        content: Vec<u8>,
219    },
220
221    /// Tool-level error. This is distinct from transport/runtime failures.
222    Error {
223        /// Tool-level error message.
224        message: String,
225    },
226}
227
228impl ToolResult {
229    /// Creates a plain text result.
230    #[must_use]
231    pub fn text(s: impl Into<String>) -> Self {
232        Self::Text { text: s.into() }
233    }
234
235    /// Creates a TSV result.
236    #[must_use]
237    pub fn tsv(s: impl Into<String>) -> Self {
238        Self::Tsv { text: s.into() }
239    }
240
241    /// Creates a JSON result from a serializable value.
242    ///
243    /// # Errors
244    ///
245    /// Returns an error if serialization fails.
246    pub fn json<T: Serialize>(value: &T) -> Result<Self> {
247        Ok(Self::Json {
248            value: serde_json::to_value(value)?,
249        })
250    }
251
252    /// Creates a JSON result from an already-materialized JSON value.
253    #[must_use]
254    pub const fn json_value(value: Value) -> Self {
255        Self::Json { value }
256    }
257
258    /// Creates an image result.
259    #[must_use]
260    pub fn image(data: Vec<u8>, media_type: &str) -> Self {
261        Self::Binary {
262            mime: parse_media_type_or_octet_stream(media_type),
263            content: data,
264        }
265    }
266
267    /// Creates a binary result.
268    #[must_use]
269    pub fn binary(data: Vec<u8>) -> Self {
270        Self::Binary {
271            mime: mime::APPLICATION_OCTET_STREAM.essence_str().to_string(),
272            content: data,
273        }
274    }
275
276    /// Creates a typed tool error result.
277    #[must_use]
278    pub fn error(message: impl Into<String>) -> Self {
279        Self::Error {
280            message: message.into(),
281        }
282    }
283
284    /// Returns `true` if this is a `Done` variant.
285    #[must_use]
286    pub const fn is_done(&self) -> bool {
287        matches!(self, Self::Done)
288    }
289
290    /// Returns `true` if this is a typed tool error.
291    #[must_use]
292    pub const fn is_error(&self) -> bool {
293        matches!(self, Self::Error { .. })
294    }
295
296    /// Returns plain textual content for text-like variants.
297    #[must_use]
298    pub fn as_text(&self) -> Option<&str> {
299        match self {
300            Self::Text { text } | Self::Tsv { text } => Some(text),
301            Self::Error { message } => Some(message),
302            Self::Done | Self::Json { .. } | Self::Binary { .. } => None,
303        }
304    }
305
306    /// Returns the error message if this is a typed tool error.
307    #[must_use]
308    pub fn error_message(&self) -> Option<&str> {
309        match self {
310            Self::Error { message } => Some(message),
311            Self::Done
312            | Self::Text { .. }
313            | Self::Tsv { .. }
314            | Self::Json { .. }
315            | Self::Binary { .. } => None,
316        }
317    }
318
319    /// Projects the result into a textual representation safe to re-inject into model context.
320    ///
321    /// # Errors
322    ///
323    /// Returns an error if JSON serialization fails.
324    pub fn render_for_model(&self) -> Result<String> {
325        match self {
326            Self::Done => Ok(String::new()),
327            Self::Text { text } | Self::Tsv { text } => Ok(text.clone()),
328            Self::Json { value } => Ok(serde_json::to_string(value)?),
329            Self::Binary { mime, content } => {
330                let mut rendered = String::new();
331                rendered.push_str("[binary tool result: ");
332                rendered.push_str(mime);
333                rendered.push_str(", ");
334                rendered.push_str(content.len().to_string().as_str());
335                rendered.push_str(" bytes]");
336                Ok(rendered)
337            }
338            Self::Error { message } => Ok(message.clone()),
339        }
340    }
341
342    /// Renders the result for CLI display.
343    ///
344    /// # Errors
345    ///
346    /// Returns an error if JSON serialization fails.
347    pub fn render_for_cli(&self) -> Result<String> {
348        match self {
349            Self::Done => Ok(String::new()),
350            Self::Text { text } | Self::Tsv { text } => Ok(text.clone()),
351            Self::Json { value } => Ok(serde_json::to_string_pretty(value)?),
352            Self::Binary { mime, content } => {
353                let mut rendered = String::new();
354                rendered.push_str("[binary tool result: ");
355                rendered.push_str(mime);
356                rendered.push_str(", ");
357                rendered.push_str(content.len().to_string().as_str());
358                rendered.push_str(" bytes]");
359                Ok(rendered)
360            }
361            Self::Error { message } => Ok(message.clone()),
362        }
363    }
364
365    /// Parses and returns the MIME type when this result carries binary content.
366    #[must_use]
367    pub fn mime(&self) -> Option<Mime> {
368        match self {
369            Self::Binary { mime, .. } => mime.parse().ok(),
370            Self::Done
371            | Self::Text { .. }
372            | Self::Tsv { .. }
373            | Self::Json { .. }
374            | Self::Error { .. } => None,
375        }
376    }
377
378    /// Returns raw bytes for binary results.
379    #[must_use]
380    pub fn content(&self) -> Option<&[u8]> {
381        match self {
382            Self::Binary { content, .. } => Some(content),
383            Self::Done
384            | Self::Text { .. }
385            | Self::Tsv { .. }
386            | Self::Json { .. }
387            | Self::Error { .. } => None,
388        }
389    }
390}
391
392/// Conversion trait for values returned by [`Tool::call`].
393///
394/// The conversion itself is fallible so tool authors can return types like
395/// `Result<T: Serialize, E: Error>` and still surface serialization failures as
396/// framework errors while preserving typed tool errors inside [`ToolResult`].
397pub trait IntoToolResult {
398    /// Converts the value into a final [`ToolResult`].
399    ///
400    /// # Errors
401    ///
402    /// Returns an error if the conversion cannot be completed.
403    fn into_tool_result(self) -> Result<ToolResult>;
404}
405
406impl IntoToolResult for ToolResult {
407    fn into_tool_result(self) -> Result<ToolResult> {
408        Ok(self)
409    }
410}
411
412impl IntoToolResult for () {
413    fn into_tool_result(self) -> Result<ToolResult> {
414        Ok(ToolResult::Done)
415    }
416}
417
418impl IntoToolResult for String {
419    fn into_tool_result(self) -> Result<ToolResult> {
420        Ok(ToolResult::text(self))
421    }
422}
423
424impl IntoToolResult for &str {
425    fn into_tool_result(self) -> Result<ToolResult> {
426        Ok(ToolResult::text(self))
427    }
428}
429
430impl IntoToolResult for Cow<'_, str> {
431    fn into_tool_result(self) -> Result<ToolResult> {
432        Ok(ToolResult::text(self.into_owned()))
433    }
434}
435
436impl IntoToolResult for Value {
437    fn into_tool_result(self) -> Result<ToolResult> {
438        Ok(ToolResult::json_value(self))
439    }
440}
441
442impl<T> IntoToolResult for Option<T>
443where
444    T: IntoToolResult,
445{
446    fn into_tool_result(self) -> Result<ToolResult> {
447        self.map_or_else(|| Ok(ToolResult::Done), IntoToolResult::into_tool_result)
448    }
449}
450
451impl<T, E> IntoToolResult for core::result::Result<T, E>
452where
453    T: Serialize,
454    E: Display,
455{
456    fn into_tool_result(self) -> Result<ToolResult> {
457        match self {
458            Ok(value) => serialize_success_value(&value),
459            Err(error) => Ok(ToolResult::error(error.to_string())),
460        }
461    }
462}
463
464fn parse_media_type_or_octet_stream(media_type: &str) -> String {
465    media_type
466        .parse::<Mime>()
467        .unwrap_or(mime::APPLICATION_OCTET_STREAM)
468        .essence_str()
469        .to_string()
470}
471
472fn serialize_success_value<T: Serialize>(value: &T) -> Result<ToolResult> {
473    let value = serde_json::to_value(value)?;
474    if let Some(tsv) = json_value_to_tsv(&value) {
475        return Ok(ToolResult::tsv(tsv));
476    }
477
478    match value {
479        Value::String(text) => Ok(ToolResult::text(text)),
480        other => Ok(ToolResult::json_value(other)),
481    }
482}
483
484/// Converts a JSON value into TSV when it represents an object or non-empty array.
485#[must_use]
486pub fn json_value_to_tsv(value: &Value) -> Option<String> {
487    let rows = match value {
488        Value::Array(arr) if !arr.is_empty() => arr
489            .iter()
490            .map(|value| flatten_json_value(value, ""))
491            .collect::<Vec<_>>(),
492        Value::Object(_) => alloc::vec![flatten_json_value(value, "")],
493        Value::Array(_) | Value::String(_) | Value::Number(_) | Value::Bool(_) | Value::Null => {
494            return None;
495        }
496    };
497
498    if rows.is_empty() {
499        return None;
500    }
501
502    let mut columns: Vec<String> = Vec::new();
503    let mut seen: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
504    for row in &rows {
505        for (key, _) in row {
506            if seen.insert(key.clone()) {
507                columns.push(key.clone());
508            }
509        }
510    }
511
512    if columns.is_empty() {
513        return None;
514    }
515
516    let mut tsv = String::new();
517    for (index, column) in columns.iter().enumerate() {
518        if index > 0 {
519            tsv.push('\t');
520        }
521        tsv.push_str(&escape_tsv_field(column));
522    }
523    tsv.push('\n');
524
525    for row in &rows {
526        let row_map: alloc::collections::BTreeMap<&str, &str> = row
527            .iter()
528            .map(|(key, value)| (key.as_str(), value.as_str()))
529            .collect::<alloc::collections::BTreeMap<&str, &str>>();
530        for (index, column) in columns.iter().enumerate() {
531            if index > 0 {
532                tsv.push('\t');
533            }
534            if let Some(value) = row_map.get(column.as_str()) {
535                tsv.push_str(&escape_tsv_field(value));
536            }
537        }
538        tsv.push('\n');
539    }
540
541    Some(tsv)
542}
543
544fn flatten_json_value(value: &Value, prefix: &str) -> Vec<(String, String)> {
545    let mut flattened = Vec::new();
546    match value {
547        Value::Object(map) => {
548            for (key, child) in map {
549                let full_key = if prefix.is_empty() {
550                    key.clone()
551                } else {
552                    format!("{prefix}.{key}")
553                };
554                flattened.extend(flatten_json_value(child, &full_key));
555            }
556        }
557        Value::Array(_) => {
558            let serialized = serde_json::to_string(value).unwrap_or_default();
559            flattened.push((prefix.to_string(), serialized));
560        }
561        Value::String(text) => {
562            flattened.push((prefix.to_string(), text.clone()));
563        }
564        Value::Number(number) => {
565            flattened.push((prefix.to_string(), number.to_string()));
566        }
567        Value::Bool(boolean) => {
568            flattened.push((prefix.to_string(), boolean.to_string()));
569        }
570        Value::Null => {
571            flattened.push((prefix.to_string(), String::new()));
572        }
573    }
574    flattened
575}
576
577fn escape_tsv_field(value: &str) -> String {
578    value.replace(['\t', '\n', '\r'], " ")
579}
580
581/// Tools that can be called by language models.
582///
583/// # Example
584///
585/// ```rust,ignore
586/// use aither::llm::{Tool, ToolResult};
587/// use schemars::JsonSchema;
588/// use serde::Deserialize;
589///
590/// #[derive(JsonSchema, Deserialize)]
591/// struct CalculatorArgs {
592///     operation: String,
593///     a: f64,
594///     b: f64,
595/// }
596///
597/// struct Calculator;
598///
599/// impl Tool for Calculator {
600///     type Arguments = CalculatorArgs;
601///     type Res = ToolResult;
602///
603///     async fn call(&mut self, args: Self::Arguments) -> aither::Result<Self::Res> {
604///         match args.operation.as_str() {
605///             "add" => Ok(ToolResult::text((args.a + args.b).to_string())),
606///             "subtract" => Ok(ToolResult::text((args.a - args.b).to_string())),
607///             "multiply" => Ok(ToolResult::text((args.a * args.b).to_string())),
608///             "divide" => {
609///                 if args.b != 0.0 {
610///                     Ok(ToolResult::text((args.a / args.b).to_string()))
611///                 } else {
612///                     Err(anyhow::Error::msg("Division by zero"))
613///                 }
614///             }
615///             _ => Err(anyhow::Error::msg("Unknown operation")),
616///         }
617///     }
618/// }
619/// ```
620pub trait Tool: Send + Sync {
621    /// Tool name. Must be unique.
622    fn name(&self) -> Cow<'static, str>;
623
624    /// What the tool does, as shown to the model.
625    ///
626    /// This is the single most important thing a model uses to decide whether
627    /// to call a tool, so it must not be empty — [`Tools::register`] rejects a
628    /// tool whose description is blank.
629    ///
630    /// The default implementation reads the rustdoc comment on
631    /// [`Self::Arguments`], which `schemars` records in the generated schema.
632    /// Override it to supply the description directly.
633    fn description(&self) -> Cow<'static, str> {
634        description_from_schema::<Self::Arguments>().unwrap_or_default()
635    }
636
637    /// Tool arguments type. Must implement [`schemars::JsonSchema`] and [`serde::de::DeserializeOwned`].
638    /// Its rustdoc becomes the default tool description.
639    type Arguments: Send + JsonSchema + DeserializeOwned;
640
641    /// Raw return type from the tool implementation.
642    type Res: IntoToolResult + Send;
643
644    /// Executes the tool with the provided arguments.
645    ///
646    /// Returns a value that can be converted into a final [`ToolResult`].
647    ///
648    /// Tools that need mutable state should use interior mutability (e.g., `Mutex`).
649    fn call(&self, arguments: Self::Arguments) -> impl Future<Output = Result<Self::Res>> + Send;
650}
651
652/// Utility to convert a serializable value to a pretty-printed JSON string.
653///
654/// # Example
655/// ```rust,ignore
656/// use aither::llm::tool::json;
657/// use serde::Serialize;
658/// #[derive(Serialize)]
659/// struct Data {
660///     name: String,
661///     value: u32,
662/// }
663/// let data = Data {
664///     name: "example".to_string(),
665///     value: 42,
666/// };
667/// let json_str = json(&data);
668/// println!("{}", json_str);
669/// ```
670///
671/// # Errors
672///
673/// Returns an error if the value cannot be serialized to JSON, which happens
674/// for types such as maps with non-string keys.
675pub fn json<T: Serialize>(value: &T) -> Result<String> {
676    let value = serde_json::to_value(value)?;
677
678    Ok(value
679        .as_str()
680        .map_or_else(|| format!("{value:#}"), ToString::to_string))
681}
682
683trait ToolImpl: Send + Sync + Any {
684    fn call(&self, args: &str) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + '_>>;
685
686    /// The cached definition. Borrowed, so registering a tool does not have to
687    /// clone its argument schema.
688    fn definition(&self) -> &ToolDefinition;
689
690    /// Upcast to [`Any`] so [`Tools::get`] can recover the concrete tool.
691    ///
692    /// Casting the `Box<dyn ToolImpl>` itself would downcast the box rather
693    /// than the tool inside it, and so never match.
694    fn as_any(&self) -> &dyn Any;
695
696    /// Mutable counterpart of [`Self::as_any`].
697    fn as_any_mut(&mut self) -> &mut dyn Any;
698}
699
700/// Dynamic tool implementation for type-erased tools.
701struct DynToolImpl<F>
702where
703    F: Fn(&str) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send>> + Send + Sync,
704{
705    definition: ToolDefinition,
706    handler: F,
707}
708
709impl<F> ToolImpl for DynToolImpl<F>
710where
711    F: Fn(&str) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send>> + Send + Sync + 'static,
712{
713    fn call(&self, args: &str) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + '_>> {
714        (self.handler)(args)
715    }
716
717    fn definition(&self) -> &ToolDefinition {
718        &self.definition
719    }
720
721    fn as_any(&self) -> &dyn Any {
722        self
723    }
724
725    fn as_any_mut(&mut self) -> &mut dyn Any {
726        self
727    }
728}
729
730/// Whether a schema describes a JSON object, and so can be sent to a provider
731/// as tool arguments without being wrapped in [`ToolArgument`].
732fn schema_is_object(value: &Value) -> bool {
733    matches!(value.get("type").and_then(Value::as_str), Some("object"))
734        || value.get("properties").is_some()
735        || value.get("oneOf").is_some()
736        || value.get("anyOf").is_some()
737        || value.get("$defs").is_some()
738}
739
740fn is_object<T: JsonSchema>() -> bool {
741    schema_is_object(&schema_for!(T).to_value())
742}
743
744/// Builds the argument schema for a tool, wrapping scalars so the root is
745/// always an object as providers require.
746fn arguments_schema<T: JsonSchema>() -> Schema {
747    if is_object::<T>() {
748        schema_for!(T)
749    } else {
750        schema_for!(ToolArgument<T>)
751    }
752}
753
754/// Reads the `description` a `JsonSchema` derive records from a type's rustdoc.
755fn description_from_schema<T: JsonSchema>() -> Option<Cow<'static, str>> {
756    schema_for!(T)
757        .to_value()
758        .get("description")
759        .and_then(Value::as_str)
760        .filter(|text| !text.trim().is_empty())
761        .map(|text| Cow::Owned(text.to_string()))
762}
763
764/// A registered tool together with everything derived from its type.
765///
766/// The argument schema and the "are these arguments an object?" decision are
767/// properties of `T::Arguments` alone, so they are computed once here rather
768/// than rebuilt on every invocation.
769struct RegisteredTool<T: Tool> {
770    tool: T,
771    definition: ToolDefinition,
772    args_are_object: bool,
773}
774
775impl<T: Tool> RegisteredTool<T> {
776    fn new(tool: T) -> Self {
777        let definition = ToolDefinition::new(&tool);
778        let args_are_object = is_object::<T::Arguments>();
779        Self {
780            tool,
781            definition,
782            args_are_object,
783        }
784    }
785}
786
787impl<T: Tool + 'static> ToolImpl for RegisteredTool<T> {
788    fn call(&self, args: &str) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + '_>> {
789        let result = if self.args_are_object {
790            serde_json::from_str::<T::Arguments>(args)
791        } else {
792            serde_json::from_str::<ToolArgument<T::Arguments>>(args).map(|wrapper| wrapper.value)
793        };
794
795        let Ok(arguments) = result else {
796            // Cold path: spelling the schema out for the model is worth the
797            // allocation only when it has actually got the call wrong.
798            let name = self.definition.name().to_string();
799            let schema_str =
800                serde_json::to_string_pretty(&self.definition.arguments_openai_schema())
801                    .unwrap_or_else(|_| "{}".to_string());
802            return Box::pin(async move {
803                Err(anyhow::Error::msg(format!(
804                    "Invalid arguments for tool '{name}'. Expected schema:\n{schema_str}"
805                )))
806            });
807        };
808
809        Box::pin(async move { Tool::call(&self.tool, arguments).await?.into_tool_result() })
810    }
811
812    fn definition(&self) -> &ToolDefinition {
813        &self.definition
814    }
815
816    fn as_any(&self) -> &dyn Any {
817        &self.tool
818    }
819
820    fn as_any_mut(&mut self) -> &mut dyn Any {
821        &mut self.tool
822    }
823}
824
825/// A tool definition carried something that is not a JSON schema.
826///
827/// JSON Schema allows an object or a bare boolean; anything else — a string, an
828/// array, a number — describes nothing a model could fill in.
829#[derive(Debug, Clone, PartialEq, Eq)]
830pub struct InvalidSchema {
831    /// The tool whose schema was rejected.
832    name: Cow<'static, str>,
833}
834
835impl InvalidSchema {
836    /// The name of the tool whose schema was rejected.
837    #[must_use]
838    pub fn name(&self) -> &str {
839        &self.name
840    }
841}
842
843impl Display for InvalidSchema {
844    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
845        write!(
846            f,
847            "tool '{}' has an argument schema that is neither an object nor a boolean",
848            self.name
849        )
850    }
851}
852
853impl core::error::Error for InvalidSchema {}
854
855/// Why a tool could not be added to a [`Tools`] registry.
856#[derive(Debug, Clone, PartialEq, Eq)]
857pub enum RegisterError {
858    /// A tool with this name is already registered.
859    ///
860    /// Names address tools in a model's tool-call, so they must be unique.
861    DuplicateName(Cow<'static, str>),
862
863    /// The tool's description is empty.
864    ///
865    /// A description is what a model uses to decide whether to call a tool, so
866    /// an empty one makes the tool unusable rather than merely undocumented.
867    EmptyDescription(Cow<'static, str>),
868}
869
870impl Display for RegisterError {
871    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
872        match self {
873            Self::DuplicateName(name) => {
874                write!(f, "a tool named '{name}' is already registered")
875            }
876            Self::EmptyDescription(name) => write!(
877                f,
878                "tool '{name}' has an empty description; add a rustdoc comment to its \
879                 Arguments type or implement Tool::description"
880            ),
881        }
882    }
883}
884
885impl core::error::Error for RegisterError {}
886
887/// Tool registry for managing and calling tools by name.
888///
889///
890/// # Example
891///
892/// ```rust,ignore
893/// use aither::llm::tool::Tools;
894///
895/// let mut tools = Tools::new();
896/// // tools.register(Calculator);
897/// let definitions = tools.definitions();
898/// // let result = tools.call("calculator", r#"{"operation": "add", "a": 5, "b": 3}"#).await;
899/// ```
900pub struct Tools {
901    tools: BTreeMap<Cow<'static, str>, Box<dyn ToolImpl>>,
902}
903
904impl Debug for Tools {
905    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
906        f.debug_struct("Tools")
907            .field("tools", &self.tools.keys().collect::<Vec<_>>())
908            .finish()
909    }
910}
911
912/// Tool definition including schema for language models.
913///
914/// Used to provide language models with information about available [`Tool`]s.
915#[derive(Debug, Clone, PartialEq)]
916#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
917pub struct ToolDefinition {
918    /// Tool name.
919    name: Cow<'static, str>,
920    /// Tool description.
921    description: Cow<'static, str>,
922    /// JSON schema for tool arguments.
923    arguments: Schema,
924}
925
926impl ToolDefinition {
927    /// Creates a tool definition for a given tool type.
928    ///
929    /// The description comes from [`Tool::description`], which by default reads
930    /// the rustdoc on the tool's `Arguments` type.
931    #[must_use]
932    pub fn new<T: Tool>(tool: &T) -> Self {
933        Self {
934            name: tool.name(),
935            description: tool.description(),
936            arguments: arguments_schema::<T::Arguments>(),
937        }
938    }
939
940    /// Creates a tool definition from raw parts.
941    ///
942    /// This is useful for creating definitions from external sources like MCP servers.
943    ///
944    /// # Errors
945    ///
946    /// Returns [`InvalidSchema`] if the value is not a JSON schema — that is,
947    /// anything other than an object or a boolean. The schema often comes from
948    /// a remote server, so this is a rejection to report, not a bug to panic
949    /// on.
950    pub fn from_parts(
951        name: Cow<'static, str>,
952        description: Cow<'static, str>,
953        schema: Value,
954    ) -> core::result::Result<Self, InvalidSchema> {
955        let arguments: Schema = schema
956            .try_into()
957            .map_err(|_| InvalidSchema { name: name.clone() })?;
958
959        Ok(Self {
960            name,
961            description,
962            arguments,
963        })
964    }
965
966    /// Returns the tool's name.
967    #[must_use]
968    pub fn name(&self) -> &str {
969        &self.name
970    }
971
972    /// Returns the tool's description.
973    #[must_use]
974    pub fn description(&self) -> &str {
975        &self.description
976    }
977
978    /// Return an OpenAI-compatible JSON schema for the tool's arguments.
979    ///
980    /// This schema would have an object type at the root, as required by `OpenAI`.
981    #[must_use]
982    pub fn arguments_openai_schema(&self) -> serde_json::Value {
983        let mut inner = self.arguments.clone().to_value();
984        clean_schema(&mut inner);
985
986        inner
987    }
988}
989
990#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
991struct ToolArgument<T> {
992    value: T,
993}
994
995fn clean_schema(value: &mut Value) {
996    // First pass: extract $defs for reference resolution
997    let defs = extract_defs(value);
998
999    // Second pass: resolve refs and clean
1000    resolve_and_clean(value, &defs);
1001
1002    // Clean up root-level schema
1003    if let Value::Object(map) = value {
1004        // Remove root-level description - it's already used as tool description
1005        // Keeping it duplicates the description in the API request
1006        map.remove("description");
1007
1008        // Ensure root has type: object (required by OpenAI function calling)
1009        if map.contains_key("properties") && !map.contains_key("type") {
1010            map.insert("type".to_string(), Value::String("object".to_string()));
1011        }
1012    }
1013}
1014
1015/// Extracts `$defs` or `definitions` from the root schema.
1016fn extract_defs(value: &Value) -> serde_json::Map<String, Value> {
1017    if let Value::Object(map) = value
1018        && let Some(Value::Object(defs)) = map.get("$defs").or_else(|| map.get("definitions"))
1019    {
1020        return defs.clone();
1021    }
1022    serde_json::Map::new()
1023}
1024
1025/// Resolves `$ref` and cleans the schema recursively.
1026#[allow(clippy::too_many_lines)]
1027fn resolve_and_clean(value: &mut Value, defs: &serde_json::Map<String, Value>) {
1028    resolve_and_clean_inner(value, defs, false);
1029}
1030
1031/// Inner recursive function with flag to track if we're inside a properties object.
1032#[allow(clippy::too_many_lines)]
1033fn resolve_and_clean_inner(
1034    value: &mut Value,
1035    defs: &serde_json::Map<String, Value>,
1036    inside_properties: bool,
1037) {
1038    match value {
1039        Value::Object(map) => {
1040            // Handle $ref - inline the referenced definition, preserving sibling properties
1041            if let Some(Value::String(ref_path)) = map.remove("$ref")
1042                && let Some(Value::Object(resolved_map)) = resolve_ref(&ref_path, defs)
1043            {
1044                // Merge resolved definition with any existing properties (like description)
1045                // Resolved definition takes precedence for conflicts except description
1046                let existing_description = map.remove("description");
1047                for (k, v) in resolved_map {
1048                    map.entry(k).or_insert(v);
1049                }
1050                // Preserve the field-level description if it exists
1051                if let Some(desc) = existing_description {
1052                    map.insert("description".to_string(), desc);
1053                }
1054            }
1055
1056            // Convert "const" to "enum" with single value (before filtering)
1057            if let Some(const_val) = map.remove("const") {
1058                map.insert("enum".to_string(), Value::Array(alloc::vec![const_val]));
1059            }
1060
1061            // Flatten oneOf/anyOf variants (before filtering, since oneOf is not in allowed list)
1062            if let Some(Value::Array(variants)) =
1063                map.remove("oneOf").or_else(|| map.remove("anyOf"))
1064            {
1065                // Check if this is a simple string enum (variants have const/type but no properties)
1066                let is_simple_enum = variants.iter().all(|v| {
1067                    if let Value::Object(vm) = v {
1068                        (vm.contains_key("const") || vm.contains_key("enum"))
1069                            && !vm.contains_key("properties")
1070                    } else {
1071                        false
1072                    }
1073                });
1074
1075                if is_simple_enum {
1076                    // Collect all const/enum values into a single enum array
1077                    let mut enum_values: alloc::vec::Vec<Value> = alloc::vec::Vec::new();
1078                    let mut variant_type: Option<String> = None;
1079
1080                    for variant in &variants {
1081                        if let Value::Object(vm) = variant {
1082                            if let Some(const_val) = vm.get("const")
1083                                && !enum_values.contains(const_val)
1084                            {
1085                                enum_values.push(const_val.clone());
1086                            }
1087                            if let Some(Value::Array(arr)) = vm.get("enum") {
1088                                for val in arr {
1089                                    if !enum_values.contains(val) {
1090                                        enum_values.push(val.clone());
1091                                    }
1092                                }
1093                            }
1094                            if variant_type.is_none()
1095                                && let Some(Value::String(t)) = vm.get("type")
1096                            {
1097                                variant_type = Some(t.clone());
1098                            }
1099                        }
1100                    }
1101
1102                    if !enum_values.is_empty() {
1103                        map.insert("enum".to_string(), Value::Array(enum_values));
1104                        if let Some(t) = variant_type {
1105                            map.insert("type".to_string(), Value::String(t));
1106                        }
1107                    }
1108                } else {
1109                    // Complex variants with properties - merge them
1110                    let mut all_properties = serde_json::Map::new();
1111
1112                    for variant in variants {
1113                        if let Value::Object(variant_map) = variant
1114                            && let Some(Value::Object(props)) = variant_map.get("properties")
1115                        {
1116                            for (key, val) in props {
1117                                // Extract enum value - handle both "enum" and "const"
1118                                let new_values: Option<alloc::vec::Vec<Value>> =
1119                                    if let Value::Object(val_obj) = val {
1120                                        if let Some(Value::Array(arr)) = val_obj.get("enum") {
1121                                            Some(arr.clone())
1122                                        } else {
1123                                            val_obj
1124                                                .get("const")
1125                                                .map(|const_val| alloc::vec![const_val.clone()])
1126                                        }
1127                                    } else {
1128                                        None
1129                                    };
1130
1131                                if all_properties.contains_key(key) {
1132                                    // Merge enum/const values into existing
1133                                    if let Some(values) = new_values
1134                                        && let Some(Value::Object(existing_obj)) =
1135                                            all_properties.get_mut(key)
1136                                        && let Some(Value::Array(existing_enum)) =
1137                                            existing_obj.get_mut("enum")
1138                                    {
1139                                        for e in values {
1140                                            if !existing_enum.contains(&e) {
1141                                                existing_enum.push(e);
1142                                            }
1143                                        }
1144                                    }
1145                                } else {
1146                                    // First time seeing this property - convert const to enum
1147                                    let mut val_clone = val.clone();
1148                                    if let Value::Object(obj) = &mut val_clone
1149                                        && let Some(const_val) = obj.remove("const")
1150                                    {
1151                                        obj.insert(
1152                                            "enum".to_string(),
1153                                            Value::Array(alloc::vec![const_val]),
1154                                        );
1155                                    }
1156                                    all_properties.insert(key.clone(), val_clone);
1157                                }
1158                            }
1159                        }
1160                    }
1161
1162                    // Set type as object if we have properties
1163                    if !all_properties.is_empty() {
1164                        map.insert("type".to_string(), Value::String("object".to_string()));
1165                        map.insert("properties".to_string(), Value::Object(all_properties));
1166                    }
1167                }
1168            }
1169
1170            // Only filter schema keywords, not property names inside "properties"
1171            // OpenAPI schema subset supported by most LLM providers
1172            if !inside_properties {
1173                let allowed = [
1174                    "type",
1175                    "description",
1176                    "properties",
1177                    "required",
1178                    "items",
1179                    "enum",
1180                    "nullable",
1181                ];
1182                map.retain(|k, _| allowed.contains(&k.as_str()));
1183            }
1184
1185            // Simplify "type" arrays like ["string", "null"] to single type
1186            if let Some(Value::Array(types)) = map.get("type") {
1187                // Filter out "null" and take the first non-null type
1188                let non_null: Vec<&Value> = types
1189                    .iter()
1190                    .filter(|t| !matches!(t, Value::String(s) if s == "null"))
1191                    .collect();
1192                if non_null.len() == 1 {
1193                    map.insert("type".to_string(), non_null[0].clone());
1194                }
1195            }
1196
1197            // Recursively clean all values
1198            for (key, v) in map.iter_mut() {
1199                // When entering "properties", its children are property definitions
1200                let child_inside_props = key == "properties";
1201                resolve_and_clean_inner(v, defs, child_inside_props);
1202            }
1203        }
1204        Value::Array(arr) => {
1205            for v in arr {
1206                resolve_and_clean_inner(v, defs, false);
1207            }
1208        }
1209        _ => {}
1210    }
1211}
1212
1213/// Resolves a `$ref` path like `#/$defs/FsOperation` to its definition.
1214fn resolve_ref(ref_path: &str, defs: &serde_json::Map<String, Value>) -> Option<Value> {
1215    // Handle common patterns: #/$defs/Name or #/definitions/Name
1216    let name = ref_path
1217        .strip_prefix("#/$defs/")
1218        .or_else(|| ref_path.strip_prefix("#/definitions/"))?;
1219
1220    defs.get(name).cloned()
1221}
1222
1223impl Default for Tools {
1224    fn default() -> Self {
1225        Self::new()
1226    }
1227}
1228
1229impl Tools {
1230    /// Creates a new empty tools registry.
1231    #[must_use]
1232    pub const fn new() -> Self {
1233        Self {
1234            tools: BTreeMap::new(),
1235        }
1236    }
1237
1238    /// Retrieves a tool by type.
1239    ///
1240    /// Returns `None` if the tool is not found.
1241    #[must_use]
1242    pub fn get<T>(&self) -> Option<&T>
1243    where
1244        T: Tool + 'static,
1245    {
1246        self.tools
1247            .values()
1248            .find_map(|tool| tool.as_any().downcast_ref::<T>())
1249    }
1250
1251    /// Retrieves a mutable reference to a tool by type.
1252    ///
1253    /// Returns `None` if the tool is not found.
1254    #[must_use]
1255    pub fn get_mut<T>(&mut self) -> Option<&mut T>
1256    where
1257        T: Tool + 'static,
1258    {
1259        self.tools
1260            .values_mut()
1261            .find_map(|tool| tool.as_any_mut().downcast_mut::<T>())
1262    }
1263
1264    /// Returns definitions of all registered tools.
1265    #[must_use]
1266    pub fn definitions(&self) -> Vec<ToolDefinition> {
1267        self.tools
1268            .values()
1269            .map(|tool| tool.definition().clone())
1270            .collect()
1271    }
1272
1273    /// Registers a new tool.
1274    ///
1275    /// The tool must implement [`Tool`] and be `'static`.
1276    ///
1277    /// # Errors
1278    ///
1279    /// Returns [`RegisterError::DuplicateName`] if a tool of that name is
1280    /// already registered, or [`RegisterError::EmptyDescription`] if the tool
1281    /// has no description — a model cannot use a tool it cannot read about, so
1282    /// this is rejected rather than silently passed on.
1283    pub fn register<T: Tool + 'static>(
1284        &mut self,
1285        tool: T,
1286    ) -> core::result::Result<(), RegisterError> {
1287        self.insert(Box::new(RegisteredTool::new(tool)))
1288    }
1289
1290    /// Registers a dynamic tool with a pre-made definition and handler.
1291    ///
1292    /// This is useful for type-erased tools (e.g., child terminal tools for subagents)
1293    /// where the concrete type isn't known at compile time.
1294    ///
1295    /// # Errors
1296    ///
1297    /// Same conditions as [`Self::register`].
1298    pub fn register_dyn<F>(
1299        &mut self,
1300        definition: ToolDefinition,
1301        handler: F,
1302    ) -> core::result::Result<(), RegisterError>
1303    where
1304        F: Fn(&str) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send>>
1305            + Send
1306            + Sync
1307            + 'static,
1308    {
1309        self.insert(Box::new(DynToolImpl {
1310            definition,
1311            handler,
1312        }))
1313    }
1314
1315    fn insert(&mut self, tool: Box<dyn ToolImpl>) -> core::result::Result<(), RegisterError> {
1316        let name = tool.definition().name.clone();
1317        if self.tools.contains_key(&name) {
1318            return Err(RegisterError::DuplicateName(name));
1319        }
1320        if tool.definition().description().trim().is_empty() {
1321            return Err(RegisterError::EmptyDescription(name));
1322        }
1323        self.tools.insert(name, tool);
1324        Ok(())
1325    }
1326
1327    /// Removes a tool from the registry.
1328    pub fn unregister(&mut self, name: &str) {
1329        self.tools.remove(name);
1330    }
1331
1332    /// Calls a tool by name with JSON arguments.
1333    ///
1334    /// # Errors
1335    ///
1336    /// Returns an error if the tool is not found, arguments cannot be parsed,
1337    /// or tool execution fails.
1338    pub async fn call(&self, name: &str, args: &str) -> Result<ToolResult> {
1339        if let Some(tool) = self.tools.get(name) {
1340            tool.call(args).await
1341        } else {
1342            Err(anyhow::Error::msg(format!("Tool '{name}' not found")))
1343        }
1344    }
1345}
1346
1347#[cfg(test)]
1348mod tests {
1349    use super::*;
1350    use alloc::{format, string::ToString, vec};
1351    use schemars::JsonSchema;
1352    use serde::{Deserialize, Serialize};
1353
1354    /// Performs basic mathematical operations.
1355    #[derive(JsonSchema, Deserialize, Debug, PartialEq)]
1356    struct CalculatorArgs {
1357        operation: String,
1358        a: f64,
1359        b: f64,
1360    }
1361
1362    struct Calculator;
1363
1364    impl Tool for Calculator {
1365        fn name(&self) -> Cow<'static, str> {
1366            "calculator".into()
1367        }
1368        type Arguments = CalculatorArgs;
1369        type Res = ToolResult;
1370
1371        fn call(&self, args: Self::Arguments) -> impl Future<Output = Result<Self::Res>> + Send {
1372            core::future::ready(match args.operation.as_str() {
1373                "add" => Ok(ToolResult::text((args.a + args.b).to_string())),
1374                "subtract" => Ok(ToolResult::text((args.a - args.b).to_string())),
1375                "multiply" => Ok(ToolResult::text((args.a * args.b).to_string())),
1376                "divide" => {
1377                    if args.b == 0.0 {
1378                        Err(anyhow::Error::msg("Division by zero"))
1379                    } else {
1380                        Ok(ToolResult::text((args.a / args.b).to_string()))
1381                    }
1382                }
1383                _ => Err(anyhow::Error::msg(format!(
1384                    "Unknown operation: {}",
1385                    args.operation
1386                ))),
1387            })
1388        }
1389    }
1390
1391    /// Greets a person by name.
1392    #[derive(JsonSchema, Deserialize)]
1393    struct GreetArgs {
1394        name: String,
1395    }
1396
1397    struct Greeter;
1398
1399    impl Tool for Greeter {
1400        fn name(&self) -> Cow<'static, str> {
1401            "greeter".into()
1402        }
1403        type Arguments = GreetArgs;
1404        type Res = ToolResult;
1405
1406        fn call(&self, args: Self::Arguments) -> impl Future<Output = Result<Self::Res>> + Send {
1407            core::future::ready(Ok(ToolResult::text(format!("Hello, {}!", args.name))))
1408        }
1409    }
1410
1411    #[test]
1412    fn from_parts_accepts_object_and_boolean_schemas() {
1413        // JSON Schema allows a bare boolean as well as an object.
1414        for schema in [
1415            serde_json::json!({"type": "object"}),
1416            serde_json::json!(true),
1417        ] {
1418            assert!(
1419                ToolDefinition::from_parts("t".into(), "does a thing".into(), schema.clone())
1420                    .is_ok(),
1421                "{schema} should be accepted"
1422            );
1423        }
1424    }
1425
1426    #[test]
1427    fn from_parts_rejects_non_schema_values() {
1428        // A server that sends any of these describes nothing a model could
1429        // fill in. Rejecting must not panic: the value came off the wire.
1430        for schema in [
1431            serde_json::json!("a string"),
1432            serde_json::json!([1, 2, 3]),
1433            serde_json::json!(7),
1434            serde_json::json!(null),
1435        ] {
1436            let result = ToolDefinition::from_parts("weird".into(), "d".into(), schema.clone());
1437            let Err(err) = result else {
1438                panic!("{schema} should be rejected");
1439            };
1440            assert_eq!(err.name(), "weird");
1441        }
1442    }
1443
1444    #[test]
1445    fn json_utility() {
1446        let value = serde_json::json!({
1447            "name": "test",
1448            "value": 42
1449        });
1450
1451        let json_str = json(&value).expect("a JSON value always serializes");
1452        assert!(json_str.contains("\"name\": \"test\""));
1453        assert!(json_str.contains("\"value\": 42"));
1454    }
1455
1456    #[test]
1457    fn tool_definition_creation() {
1458        let calculator = Calculator;
1459        let definition = ToolDefinition::new(&calculator);
1460
1461        assert_eq!(definition.name, "calculator");
1462        assert_eq!(
1463            definition.description,
1464            "Performs basic mathematical operations."
1465        );
1466        // Schema should be present - just check it exists
1467        // The exact structure of schemars::Schema is implementation detail
1468    }
1469
1470    #[test]
1471    fn tools_creation() {
1472        let tools = Tools::new();
1473        assert_eq!(tools.definitions().len(), 0);
1474    }
1475
1476    #[test]
1477    fn tools_default() {
1478        let tools = Tools::default();
1479        assert_eq!(tools.definitions().len(), 0);
1480    }
1481
1482    #[tokio::test]
1483    async fn tools_register_and_call() {
1484        let mut tools = Tools::new();
1485        tools.register(Calculator).expect("calculator registers");
1486
1487        let definitions = tools.definitions();
1488        assert_eq!(definitions.len(), 1);
1489        assert_eq!(definitions[0].name, "calculator");
1490
1491        let result = tools
1492            .call("calculator", r#"{"operation": "add", "a": 5, "b": 3}"#)
1493            .await;
1494        assert!(result.is_ok());
1495        assert_eq!(result.unwrap().as_text(), Some("8"));
1496    }
1497
1498    #[tokio::test]
1499    async fn calculator_operations() {
1500        let mut tools = Tools::new();
1501        tools.register(Calculator).expect("calculator registers");
1502
1503        // Test addition
1504        let result = tools
1505            .call("calculator", r#"{"operation": "add", "a": 10, "b": 5}"#)
1506            .await;
1507        assert_eq!(result.unwrap().as_text(), Some("15"));
1508
1509        // Test subtraction
1510        let result = tools
1511            .call(
1512                "calculator",
1513                r#"{"operation": "subtract", "a": 10, "b": 3}"#,
1514            )
1515            .await;
1516        assert_eq!(result.unwrap().as_text(), Some("7"));
1517
1518        // Test multiplication
1519        let result = tools
1520            .call("calculator", r#"{"operation": "multiply", "a": 4, "b": 3}"#)
1521            .await;
1522        assert_eq!(result.unwrap().as_text(), Some("12"));
1523
1524        // Test division
1525        let result = tools
1526            .call("calculator", r#"{"operation": "divide", "a": 15, "b": 3}"#)
1527            .await;
1528        assert_eq!(result.unwrap().as_text(), Some("5"));
1529    }
1530
1531    #[tokio::test]
1532    async fn calculator_division_by_zero() {
1533        let mut tools = Tools::new();
1534        tools.register(Calculator).expect("calculator registers");
1535
1536        let result = tools
1537            .call("calculator", r#"{"operation": "divide", "a": 10, "b": 0}"#)
1538            .await;
1539        assert!(result.is_err());
1540        assert!(result.unwrap_err().to_string().contains("Division by zero"));
1541    }
1542
1543    #[tokio::test]
1544    async fn calculator_unknown_operation() {
1545        let mut tools = Tools::new();
1546        tools.register(Calculator).expect("calculator registers");
1547
1548        let result = tools
1549            .call("calculator", r#"{"operation": "modulo", "a": 10, "b": 3}"#)
1550            .await;
1551        assert!(result.is_err());
1552        assert!(
1553            result
1554                .unwrap_err()
1555                .to_string()
1556                .contains("Unknown operation")
1557        );
1558    }
1559
1560    #[tokio::test]
1561    async fn multiple_tools() {
1562        let mut tools = Tools::new();
1563        tools.register(Calculator).expect("calculator registers");
1564        tools.register(Greeter).expect("greeter registers");
1565
1566        let definitions = tools.definitions();
1567        assert_eq!(definitions.len(), 2);
1568
1569        // Find calculator and greeter in definitions
1570        let calc_def = definitions.iter().find(|d| d.name == "calculator").unwrap();
1571        let greet_def = definitions.iter().find(|d| d.name == "greeter").unwrap();
1572
1573        assert_eq!(
1574            calc_def.description,
1575            "Performs basic mathematical operations."
1576        );
1577        assert_eq!(greet_def.description, "Greets a person by name.");
1578
1579        // Test both tools
1580        let calc_result = tools
1581            .call("calculator", r#"{"operation": "add", "a": 2, "b": 3}"#)
1582            .await;
1583        assert_eq!(calc_result.unwrap().as_text(), Some("5"));
1584
1585        let greet_result = tools.call("greeter", r#"{"name": "Alice"}"#).await;
1586        assert_eq!(greet_result.unwrap().as_text(), Some("Hello, Alice!"));
1587    }
1588
1589    #[derive(Debug, Serialize)]
1590    struct TableRow {
1591        name: &'static str,
1592        count: u32,
1593    }
1594
1595    #[derive(Debug, Serialize)]
1596    struct NestedTableRow {
1597        user: TableRow,
1598        ok: bool,
1599    }
1600
1601    #[derive(Debug)]
1602    struct ToolFailure(&'static str);
1603
1604    impl core::fmt::Display for ToolFailure {
1605        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1606            f.write_str(self.0)
1607        }
1608    }
1609
1610    impl core::error::Error for ToolFailure {}
1611
1612    #[test]
1613    fn into_tool_result_string_is_plain_text() {
1614        assert_eq!(
1615            String::from("hello").into_tool_result().unwrap(),
1616            ToolResult::text("hello")
1617        );
1618    }
1619
1620    #[test]
1621    fn into_tool_result_str_is_plain_text() {
1622        assert_eq!(
1623            "hello".into_tool_result().unwrap(),
1624            ToolResult::text("hello")
1625        );
1626    }
1627
1628    #[test]
1629    fn into_tool_result_option_none_is_done() {
1630        let result = Option::<String>::None.into_tool_result().unwrap();
1631        assert_eq!(result, ToolResult::Done);
1632    }
1633
1634    #[test]
1635    fn into_tool_result_option_some_delegates() {
1636        let result = Some("hello").into_tool_result().unwrap();
1637        assert_eq!(result, ToolResult::text("hello"));
1638    }
1639
1640    #[test]
1641    fn into_tool_result_result_ok_string_is_text() {
1642        let result = core::result::Result::<String, ToolFailure>::Ok(String::from("hello"))
1643            .into_tool_result()
1644            .unwrap();
1645        assert_eq!(result, ToolResult::text("hello"));
1646    }
1647
1648    #[test]
1649    fn into_tool_result_result_ok_object_is_tsv() {
1650        let result = core::result::Result::<TableRow, ToolFailure>::Ok(TableRow {
1651            name: "alpha",
1652            count: 3,
1653        })
1654        .into_tool_result()
1655        .unwrap();
1656
1657        assert_eq!(result, ToolResult::tsv("count\tname\n3\talpha\n"));
1658    }
1659
1660    #[test]
1661    fn into_tool_result_result_ok_array_of_objects_is_tsv() {
1662        let result = core::result::Result::<Vec<TableRow>, ToolFailure>::Ok(vec![
1663            TableRow {
1664                name: "alpha",
1665                count: 3,
1666            },
1667            TableRow {
1668                name: "beta",
1669                count: 5,
1670            },
1671        ])
1672        .into_tool_result()
1673        .unwrap();
1674
1675        assert_eq!(result, ToolResult::tsv("count\tname\n3\talpha\n5\tbeta\n"));
1676    }
1677
1678    #[test]
1679    fn into_tool_result_result_ok_scalar_is_json() {
1680        let result = core::result::Result::<bool, ToolFailure>::Ok(true)
1681            .into_tool_result()
1682            .unwrap();
1683        assert_eq!(result, ToolResult::json_value(Value::Bool(true)));
1684    }
1685
1686    #[test]
1687    fn into_tool_result_result_err_is_typed_error() {
1688        let result = core::result::Result::<TableRow, ToolFailure>::Err(ToolFailure("boom"))
1689            .into_tool_result()
1690            .unwrap();
1691        assert_eq!(result, ToolResult::error("boom"));
1692        assert!(result.is_error());
1693        assert_eq!(result.error_message(), Some("boom"));
1694    }
1695
1696    #[test]
1697    fn json_value_to_tsv_flattens_nested_objects() {
1698        let value = serde_json::to_value(NestedTableRow {
1699            user: TableRow {
1700                name: "alpha",
1701                count: 3,
1702            },
1703            ok: true,
1704        })
1705        .unwrap();
1706
1707        assert_eq!(
1708            json_value_to_tsv(&value),
1709            Some("ok\tuser.count\tuser.name\ntrue\t3\talpha\n".to_string())
1710        );
1711    }
1712
1713    #[tokio::test]
1714    async fn tool_not_found() {
1715        let tools = Tools::new();
1716
1717        let result = tools.call("nonexistent", "{}").await;
1718        assert!(result.is_err());
1719        assert!(
1720            result
1721                .unwrap_err()
1722                .to_string()
1723                .contains("Tool 'nonexistent' not found")
1724        );
1725    }
1726
1727    #[tokio::test]
1728    async fn invalid_json() {
1729        let mut tools = Tools::new();
1730        tools.register(Calculator).expect("calculator registers");
1731
1732        let result = tools.call("calculator", "invalid json").await;
1733        assert!(result.is_err());
1734    }
1735
1736    #[test]
1737    fn tools_unregister() {
1738        let mut tools = Tools::new();
1739        tools.register(Calculator).expect("calculator registers");
1740        tools.register(Greeter).expect("greeter registers");
1741
1742        assert_eq!(tools.definitions().len(), 2);
1743
1744        tools.unregister("calculator");
1745        assert_eq!(tools.definitions().len(), 1);
1746
1747        let remaining = &tools.definitions()[0];
1748        assert_eq!(remaining.name, "greeter");
1749
1750        tools.unregister("greeter");
1751        assert_eq!(tools.definitions().len(), 0);
1752    }
1753
1754    #[test]
1755    fn tools_debug() {
1756        let mut tools = Tools::new();
1757        tools.register(Calculator).expect("calculator registers");
1758        tools.register(Greeter).expect("greeter registers");
1759
1760        let debug_str = format!("{tools:?}");
1761        assert!(debug_str.contains("Tools"));
1762        assert!(debug_str.contains("calculator"));
1763        assert!(debug_str.contains("greeter"));
1764    }
1765
1766    #[test]
1767    fn tool_definition_debug() {
1768        let calculator = Calculator;
1769        let definition = ToolDefinition::new(&calculator);
1770        let debug_str = format!("{definition:?}");
1771
1772        assert!(debug_str.contains("ToolDefinition"));
1773        assert!(debug_str.contains("calculator"));
1774        assert!(debug_str.contains("Performs basic mathematical operations"));
1775    }
1776
1777    #[test]
1778    fn tool_definition_clone() {
1779        let calculator = Calculator;
1780        let original = ToolDefinition::new(&calculator);
1781        let cloned = original.clone();
1782
1783        assert_eq!(original.name, cloned.name);
1784        assert_eq!(original.description, cloned.description);
1785    }
1786
1787    #[test]
1788    fn schema_preserves_enum() {
1789        #[derive(JsonSchema, Deserialize)]
1790        #[serde(rename_all = "snake_case")]
1791        enum Status {
1792            Pending,
1793            InProgress,
1794            Completed,
1795        }
1796
1797        #[allow(dead_code)]
1798        #[derive(JsonSchema, Deserialize)]
1799        struct Item {
1800            status: Status,
1801        }
1802
1803        #[allow(dead_code)]
1804        #[derive(JsonSchema, Deserialize)]
1805        struct Args {
1806            items: Vec<Item>,
1807        }
1808
1809        struct TestTool;
1810
1811        impl Tool for TestTool {
1812            fn name(&self) -> Cow<'static, str> {
1813                "test".into()
1814            }
1815            type Arguments = Args;
1816            type Res = ToolResult;
1817
1818            fn call(
1819                &self,
1820                _args: Self::Arguments,
1821            ) -> impl Future<Output = Result<Self::Res>> + Send {
1822                core::future::ready(Ok(ToolResult::text("ok")))
1823            }
1824        }
1825
1826        let tool = TestTool;
1827        let def = ToolDefinition::new(&tool);
1828        let schema = def.arguments_openai_schema();
1829
1830        // Check that status has enum values
1831        let schema_obj = schema.as_object().expect("schema should be object");
1832        let properties = schema_obj
1833            .get("properties")
1834            .expect("should have properties")
1835            .as_object()
1836            .unwrap();
1837        let items = properties
1838            .get("items")
1839            .expect("should have items")
1840            .as_object()
1841            .unwrap();
1842        let item_props = items
1843            .get("items")
1844            .expect("items should have items schema")
1845            .as_object()
1846            .unwrap();
1847        let item_properties = item_props
1848            .get("properties")
1849            .expect("item should have properties")
1850            .as_object()
1851            .unwrap();
1852        let status = item_properties
1853            .get("status")
1854            .expect("should have status")
1855            .as_object()
1856            .unwrap();
1857
1858        // Status should have enum
1859        assert!(
1860            status.contains_key("enum"),
1861            "Status should have enum field. Full schema: {}",
1862            serde_json::to_string_pretty(&schema).unwrap()
1863        );
1864    }
1865
1866    #[test]
1867    fn schema_ref_resolution() {
1868        // Test that $ref schemas get resolved and enum values preserved
1869        let raw_schema = serde_json::json!({
1870            "type": "object",
1871            "properties": {
1872                "status": {
1873                    "$ref": "#/$defs/Status"
1874                }
1875            },
1876            "$defs": {
1877                "Status": {
1878                    "type": "string",
1879                    "enum": ["pending", "in_progress", "completed"]
1880                }
1881            }
1882        });
1883
1884        let mut schema = raw_schema;
1885        clean_schema(&mut schema);
1886
1887        let props = schema.get("properties").unwrap().as_object().unwrap();
1888        let status = props.get("status").unwrap().as_object().unwrap();
1889
1890        assert!(
1891            status.contains_key("enum"),
1892            "Status should have enum after ref resolution. Got: {}",
1893            serde_json::to_string_pretty(&schema).unwrap()
1894        );
1895    }
1896
1897    #[test]
1898    fn schema_nested_ref_in_array() {
1899        // Test nested $ref inside array items (like TodoWriteArgs)
1900        let raw_schema = serde_json::json!({
1901            "type": "object",
1902            "properties": {
1903                "todos": {
1904                    "type": "array",
1905                    "items": {
1906                        "$ref": "#/$defs/TodoItem"
1907                    }
1908                }
1909            },
1910            "$defs": {
1911                "TodoItem": {
1912                    "type": "object",
1913                    "properties": {
1914                        "content": { "type": "string" },
1915                        "status": { "$ref": "#/$defs/TodoStatus" }
1916                    },
1917                    "required": ["content", "status"]
1918                },
1919                "TodoStatus": {
1920                    "type": "string",
1921                    "enum": ["pending", "in_progress", "completed"]
1922                }
1923            }
1924        });
1925
1926        let mut schema = raw_schema;
1927        clean_schema(&mut schema);
1928
1929        // Navigate to status
1930        let props = schema.get("properties").unwrap().as_object().unwrap();
1931        let todos = props.get("todos").unwrap().as_object().unwrap();
1932        let items = todos.get("items").unwrap().as_object().unwrap();
1933        let item_props = items.get("properties").unwrap().as_object().unwrap();
1934        let status = item_props.get("status").unwrap().as_object().unwrap();
1935
1936        assert!(
1937            status.contains_key("enum"),
1938            "Nested status should have enum. Full schema: {}",
1939            serde_json::to_string_pretty(&schema).unwrap()
1940        );
1941    }
1942}