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