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