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