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