Skip to main content

llm_tool/
types.rs

1//! Custom tool registration for the Antigravity SDK bridge.
2//!
3//! Defines Rust-side tool metadata and a registry that tracks tools by name.
4//! The actual Python wrapping (converting Rust async fns into Python callables)
5//! requires the Python runtime and is gated behind integration tests.
6
7use std::{
8    any::{Any, TypeId},
9    collections::HashMap,
10    sync::{Arc, RwLock},
11};
12
13use serde::{Deserialize, Serialize};
14
15/// Context passed to Rust tools during dispatch, mirroring the Python SDK's `ToolContext`.
16///
17/// Provides access to the current conversation ID, a shared key-value state
18/// store that persists across tool calls within the same agent turn, and an
19/// idle flag.
20///
21/// The state is backed by `Arc<RwLock<HashMap>>` so it can be cheaply cloned
22/// and shared across concurrent tool invocations. Reads acquire a shared
23/// lock; only writes take an exclusive lock.
24///
25/// # `get_state` vs `set_state` error handling
26///
27/// These two methods intentionally handle mutex poisoning differently:
28///
29/// - **[`get_state`](Self::get_state)** acquires a **read** lock and returns
30///   the caller-supplied `default` when the lock is poisoned. Reads are
31///   best-effort — a missing value is indistinguishable from a default, so
32///   returning `default` keeps the tool running without surfacing
33///   infrastructure errors to the model.
34///
35/// - **[`set_state`](Self::set_state)** acquires a **write** lock and returns
36///   `Err` when the lock is poisoned. Writes that silently vanish can cause
37///   subtle logic bugs, so callers must handle the failure explicitly.
38/// # Typed extensions
39///
40/// In addition to the string-keyed JSON state, `ToolContext` supports
41/// **typed extensions** via [`set_ext`](Self::set_ext) /
42/// [`get_ext`](Self::get_ext). These use `std::any::Any` under the hood
43/// and are keyed by `TypeId`, so callers store and retrieve strongly-typed
44/// values (typically `Arc<T>`) without serialization.
45///
46/// ```rust
47/// use std::sync::Arc;
48///
49/// use llm_tool::ToolContext;
50///
51/// struct MyState {
52///     session_dir: String,
53/// }
54///
55/// let ctx = ToolContext::new(None);
56/// ctx.set_ext(Arc::new(MyState {
57///     session_dir: "/tmp".into(),
58/// }));
59///
60/// let state: Arc<MyState> = ctx.get_ext::<Arc<MyState>>().unwrap();
61/// assert_eq!(state.session_dir, "/tmp");
62/// ```
63pub struct ToolContext {
64    conversation_id: Option<String>,
65    state: Arc<RwLock<HashMap<String, serde_json::Value>>>,
66    extensions: Arc<RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>>,
67    is_idle: bool,
68}
69
70impl ToolContext {
71    /// Create a new context with the given conversation ID.
72    #[must_use]
73    pub fn new(conversation_id: Option<String>) -> Self {
74        Self {
75            conversation_id,
76            state: Arc::new(RwLock::new(HashMap::new())),
77            extensions: Arc::new(RwLock::new(HashMap::new())),
78            is_idle: false,
79        }
80    }
81
82    /// Create a context that shares an externally-provided state map.
83    ///
84    /// Use this when multiple `ToolContext` instances (e.g. successive tool
85    /// calls within the same agent) must read/write the **same** state store.
86    #[must_use]
87    pub fn with_shared_state(
88        conversation_id: Option<String>,
89        state: Arc<RwLock<HashMap<String, serde_json::Value>>>,
90    ) -> Self {
91        Self {
92            conversation_id,
93            state,
94            extensions: Arc::new(RwLock::new(HashMap::new())),
95            is_idle: false,
96        }
97    }
98
99    /// Return the conversation ID, if one has been set.
100    #[must_use]
101    pub fn conversation_id(&self) -> Option<&str> {
102        self.conversation_id.as_deref()
103    }
104
105    /// Retrieve a value from the shared state, returning `default` if the key
106    /// is absent or the lock is poisoned.
107    ///
108    /// This method never fails — on a poisoned lock it logs a warning and
109    /// returns `default`. See the [struct-level docs](Self) for rationale.
110    #[must_use]
111    pub fn get_state(&self, key: &str, default: serde_json::Value) -> serde_json::Value {
112        match self.state.read() {
113            Ok(guard) => guard.get(key).cloned().unwrap_or(default),
114            Err(e) => {
115                tracing::warn!(key, error = %e, "ToolContext::get_state: lock poisoned, returning default");
116                default
117            }
118        }
119    }
120
121    /// Insert or update a value in the shared state.
122    ///
123    /// Unlike [`get_state`](Self::get_state), this method returns `Err` on a
124    /// poisoned lock because silently dropping a write can cause subtle bugs.
125    /// See the [struct-level docs](Self) for rationale.
126    ///
127    /// # Errors
128    ///
129    /// Returns [`ToolError`]
130    /// if the lock is poisoned.
131    pub fn set_state(&self, key: &str, value: serde_json::Value) -> Result<(), ToolError> {
132        match self.state.write() {
133            Ok(mut guard) => {
134                guard.insert(key.to_owned(), value);
135                Ok(())
136            }
137            Err(e) => {
138                let msg = format!("ToolContext::set_state: lock poisoned for key '{key}': {e}");
139                tracing::warn!("{msg}");
140                Err(ToolError::new(msg))
141            }
142        }
143    }
144
145    /// Whether the agent is currently idle.
146    #[must_use]
147    pub const fn is_idle(&self) -> bool {
148        self.is_idle
149    }
150
151    /// Store a typed value in the extensions map.
152    ///
153    /// Values are keyed by `TypeId`, so each concrete type can only appear
154    /// once. Typically used to store `Arc<T>` for shared, cloneable access.
155    ///
156    /// # Panics
157    ///
158    /// Panics if the extensions `RwLock` is poisoned (indicates a prior panic).
159    pub fn set_ext<T: Send + Sync + 'static>(&self, value: T) {
160        let mut exts = self
161            .extensions
162            .write()
163            .expect("ToolContext extensions lock poisoned");
164        exts.insert(TypeId::of::<T>(), Box::new(value));
165    }
166
167    /// Retrieve a clone of a typed value from the extensions map.
168    ///
169    /// Returns `None` if no value of type `T` has been stored via
170    /// [`set_ext`](Self::set_ext).
171    ///
172    /// # Panics
173    ///
174    /// Panics if the extensions `RwLock` is poisoned (indicates a prior panic).
175    #[must_use]
176    pub fn get_ext<T: Clone + Send + Sync + 'static>(&self) -> Option<T> {
177        let exts = self
178            .extensions
179            .read()
180            .expect("ToolContext extensions lock poisoned");
181        exts.get(&TypeId::of::<T>())
182            .and_then(|v| v.downcast_ref::<T>())
183            .cloned()
184    }
185}
186
187/// Re-export the `#[llm_tool]` proc macro for defining tools from plain functions.
188///
189/// # Usage
190///
191/// ```
192/// use llm_tool::{RustTool, ToolContext, ToolRegistry, llm_tool};
193///
194/// /// Adds two numbers together (with a twist).
195/// #[llm_tool]
196/// fn wonky_add(
197///     /// First number.
198///     a: i64,
199///     /// Second number.
200///     b: i64,
201/// ) -> Result<String, String> {
202///     Ok(format!("{}", a + b + 1))
203/// }
204///
205/// let mut registry = ToolRegistry::new();
206/// registry.register(WonkyAdd);
207/// assert_eq!(registry.definitions().len(), 1);
208/// ```
209pub use llm_tool_macros::llm_tool;
210// Re-export `JsonSchema` derive so tool authors can write `use llm_tool::JsonSchema;`
211// without adding `schemars` to their own `Cargo.toml`.
212pub use schemars::JsonSchema;
213
214/// Human-readable JSON type name for error messages.
215fn other_type_name(value: &serde_json::Value) -> &'static str {
216    match value {
217        serde_json::Value::Null => "null",
218        serde_json::Value::Bool(_) => "bool",
219        serde_json::Value::Number(_) => "number",
220        serde_json::Value::String(_) => "string",
221        serde_json::Value::Array(_) => "array",
222        serde_json::Value::Object(_) => "object",
223    }
224}
225
226/// The return value of a Rust tool execution.
227///
228/// Every tool produces a `ToolOutput` containing:
229/// - **`content`**: the text sent back to the model.
230/// - **`metadata`**: an optional structured key-value map available to hooks,
231///   policies, and logging pipelines — but **never** sent to the model.
232///
233/// # Ergonomics
234///
235/// `ToolOutput` implements `From<String>`, `From<&str>`, and `Display`, so
236/// simple tools can return plain strings without ceremony:
237///
238/// ```rust
239/// use llm_tool::ToolOutput;
240/// use serde::Serialize;
241///
242/// // From a String
243/// let out: ToolOutput = "hello".to_string().into();
244/// assert_eq!(out.content(), "hello");
245/// assert!(out.metadata().is_empty());
246///
247/// // From &str
248/// let out: ToolOutput = "world".into();
249/// assert_eq!(out.content(), "world");
250///
251/// // Structured metadata from a typed struct (preferred)
252/// #[derive(Serialize)]
253/// struct ReadMeta {
254///     bytes_read: usize,
255///     cached: bool,
256/// }
257///
258/// let out = ToolOutput::new("file contents…")
259///     .with_metadata(&ReadMeta {
260///         bytes_read: 1024,
261///         cached: true,
262///     })
263///     .unwrap();
264/// assert_eq!(out.metadata()["bytes_read"], 1024);
265/// assert_eq!(out.metadata()["cached"], true);
266///
267/// // Single ad-hoc entry
268/// let out = ToolOutput::new("done").with_meta("exit_code", serde_json::json!(0));
269/// assert_eq!(out.metadata()["exit_code"], 0);
270/// ```
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct ToolOutput {
273    /// The text content returned to the model.
274    content: String,
275    /// Structured metadata for hooks / policies / logging.
276    /// NOT sent to the model.
277    metadata: std::collections::HashMap<String, serde_json::Value>,
278}
279
280impl ToolOutput {
281    /// Create a new `ToolOutput` with the given content and no metadata.
282    pub fn new(content: impl Into<String>) -> Self {
283        Self {
284            content: content.into(),
285            metadata: std::collections::HashMap::new(),
286        }
287    }
288
289    /// Serialize a value to JSON and wrap it as tool output.
290    ///
291    /// The JSON string becomes the content sent to the model, but no
292    /// metadata is attached. For the zero-redundancy path that populates
293    /// **both** content and metadata from the same struct, use
294    /// [`from_metadata`](Self::from_metadata).
295    ///
296    /// ```rust
297    /// use llm_tool::{ToolOutput, ToolError};
298    ///
299    /// let data = serde_json::json!({"temp": 72, "unit": "F"});
300    /// let output = ToolOutput::json(&data).unwrap();
301    /// assert!(output.content().contains("72"));
302    /// assert!(output.metadata().is_empty()); // no metadata attached
303    /// ```
304    ///
305    /// # Errors
306    ///
307    /// Returns `Err(ToolError)` if serialization fails.
308    pub fn json<T: serde::Serialize>(value: &T) -> Result<Self, ToolError> {
309        serde_json::to_string(value)
310            .map(Self::new)
311            .map_err(|e| ToolError::new(format!("serialization failed: {e}")))
312    }
313
314    /// Create a `ToolOutput` where **both** the content and metadata come
315    /// from the same serializable value.
316    ///
317    /// - **Content** (sent to the model): the JSON representation of `value`.
318    /// - **Metadata** (hooks / policies / logging): the flattened object fields.
319    ///
320    /// This is the zero-redundancy path: define one struct, derive
321    /// `Serialize`, and everything is populated automatically.
322    ///
323    /// # Errors
324    ///
325    /// Returns `Err(ToolError)` if `value` doesn't serialize to a JSON object.
326    ///
327    /// # Example
328    ///
329    /// ```rust
330    /// use llm_tool::ToolOutput;
331    /// use serde::Serialize;
332    ///
333    /// #[derive(Serialize)]
334    /// struct Weather {
335    ///     location: String,
336    ///     temp_f: i32,
337    ///     condition: String,
338    /// }
339    ///
340    /// let out = ToolOutput::from_metadata(&Weather {
341    ///     location: "Seattle".into(),
342    ///     temp_f: 72,
343    ///     condition: "Sunny".into(),
344    /// })
345    /// .unwrap();
346    ///
347    /// // Model sees the JSON string
348    /// assert!(out.content().contains("Seattle"));
349    /// assert!(out.content().contains("72"));
350    ///
351    /// // Hooks see typed fields
352    /// assert_eq!(out.metadata()["location"], "Seattle");
353    /// assert_eq!(out.metadata()["temp_f"], 72);
354    /// ```
355    pub fn from_metadata<T: serde::Serialize>(value: &T) -> Result<Self, ToolError> {
356        let json_value = serde_json::to_value(value)
357            .map_err(|e| ToolError::new(format!("metadata serialization failed: {e}")))?;
358        // Serialize to string *before* destructuring so we borrow the Value
359        // instead of cloning the inner Map.
360        let content = json_value.to_string();
361        match json_value {
362            serde_json::Value::Object(map) => Ok(Self {
363                content,
364                metadata: map.into_iter().collect(),
365            }),
366            other => Err(ToolError::new(format!(
367                "metadata must serialize to a JSON object, got {}",
368                other_type_name(&other),
369            ))),
370        }
371    }
372
373    /// Attach a single metadata key-value pair. Chainable.
374    ///
375    /// For attaching multiple fields at once, prefer
376    /// [`with_metadata`](Self::with_metadata) with a typed struct.
377    #[must_use]
378    pub fn with_meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
379        self.metadata.insert(key.into(), value);
380        self
381    }
382
383    /// Attach structured metadata from a serializable value.
384    ///
385    /// The value is serialized to a JSON object and its fields are **merged**
386    /// into the metadata map. This is the preferred way to attach metadata
387    /// because it avoids stringly-typed keys and data duplication.
388    ///
389    /// # Errors
390    ///
391    /// Returns `Err(ToolError)` if `value` doesn't serialize to a JSON object
392    /// (e.g. it serializes to a scalar or array).
393    ///
394    /// # Example
395    ///
396    /// ```rust
397    /// use llm_tool::ToolOutput;
398    /// use serde::Serialize;
399    ///
400    /// #[derive(Serialize)]
401    /// struct FileMeta {
402    ///     bytes_read: usize,
403    ///     source: String,
404    /// }
405    ///
406    /// let out = ToolOutput::new("file contents")
407    ///     .with_metadata(&FileMeta {
408    ///         bytes_read: 1024,
409    ///         source: "/etc/hosts".into(),
410    ///     })
411    ///     .unwrap();
412    /// assert_eq!(out.metadata()["bytes_read"], 1024);
413    /// assert_eq!(out.metadata()["source"], "/etc/hosts");
414    /// ```
415    pub fn with_metadata<T: serde::Serialize>(mut self, value: &T) -> Result<Self, ToolError> {
416        let json = serde_json::to_value(value)
417            .map_err(|e| ToolError::new(format!("metadata serialization failed: {e}")))?;
418        match json {
419            serde_json::Value::Object(map) => {
420                self.metadata.extend(map);
421                Ok(self)
422            }
423            other => Err(ToolError::new(format!(
424                "metadata must serialize to a JSON object, got {}",
425                other_type_name(&other),
426            ))),
427        }
428    }
429
430    /// The text content sent back to the model.
431    #[must_use]
432    pub fn content(&self) -> &str {
433        &self.content
434    }
435
436    /// Consume self and return the owned content string.
437    #[must_use]
438    pub fn into_content(self) -> String {
439        self.content
440    }
441
442    /// The structured metadata map.
443    #[must_use]
444    pub fn metadata(&self) -> &std::collections::HashMap<String, serde_json::Value> {
445        &self.metadata
446    }
447}
448
449impl std::fmt::Display for ToolOutput {
450    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451        f.write_str(&self.content)
452    }
453}
454
455impl From<String> for ToolOutput {
456    fn from(content: String) -> Self {
457        Self::new(content)
458    }
459}
460
461impl From<&str> for ToolOutput {
462    fn from(content: &str) -> Self {
463        Self::new(content)
464    }
465}
466
467impl From<i64> for ToolOutput {
468    fn from(value: i64) -> Self {
469        Self::new(value.to_string())
470    }
471}
472
473impl From<f64> for ToolOutput {
474    fn from(value: f64) -> Self {
475        Self::new(value.to_string())
476    }
477}
478
479impl From<bool> for ToolOutput {
480    fn from(value: bool) -> Self {
481        Self::new(value.to_string())
482    }
483}
484
485impl From<serde_json::Value> for ToolOutput {
486    fn from(value: serde_json::Value) -> Self {
487        // serde_json::Value::to_string() never fails.
488        Self::new(value.to_string())
489    }
490}
491
492/// Wrapper for returning serializable values as JSON tool output.
493///
494/// Implements `From<Json<T>> for ToolOutput` so it works with the
495/// `#[llm_tool]` macro's `.into()` conversion — no `Result` wrapper needed
496/// for infallible serialization.
497///
498/// # Panics
499///
500/// The `From` conversion panics if `serde_json::to_string` fails.
501/// This only happens with broken `Serialize` implementations (e.g.,
502/// maps with non-string keys). For explicit error handling, use
503/// [`ToolOutput::json()`] instead.
504///
505/// # Example
506///
507/// ```rust
508/// use llm_tool::{Json, ToolOutput};
509/// use serde::Serialize;
510///
511/// #[derive(Serialize)]
512/// struct Weather {
513///     temp: f64,
514///     city: String,
515/// }
516///
517/// let output: ToolOutput = Json(Weather {
518///     temp: 72.0,
519///     city: "NYC".into(),
520/// })
521/// .into();
522/// assert!(output.content().contains("72"));
523/// ```
524pub struct Json<T>(pub T);
525
526impl<T: serde::Serialize> From<Json<T>> for ToolOutput {
527    fn from(json: Json<T>) -> Self {
528        Self::new(
529            serde_json::to_string(&json.0)
530                .expect("Json<T> serialization failed — this is a bug in the Serialize impl"),
531        )
532    }
533}
534
535/// An error returned from a tool execution.
536/// The error message is sent back to the model as the tool's error response.
537/// Structured metadata can be attached for hooks and logging — it is **not**
538/// sent to the model.
539///
540/// Implements `From<String>` and `From<&str>` for ergonomic construction.
541///
542/// # Example
543///
544/// ```
545/// use llm_tool::ToolError;
546/// use serde::Serialize;
547///
548/// let err: ToolError = "something went wrong".into();
549/// assert_eq!(err.to_string(), "something went wrong");
550///
551/// let err = ToolError::new(format!("failed to read {}", "file.txt"));
552/// assert!(err.to_string().contains("file.txt"));
553///
554/// // Structured metadata from a typed struct (preferred)
555/// #[derive(Serialize)]
556/// struct HttpErrorMeta {
557///     status_code: u16,
558///     url: String,
559/// }
560///
561/// let err = ToolError::new("HTTP request failed")
562///     .with_metadata(&HttpErrorMeta {
563///         status_code: 503,
564///         url: "https://example.com".into(),
565///     })
566///     .unwrap();
567/// assert_eq!(err.metadata()["status_code"], 503);
568///
569/// // Single ad-hoc entry
570/// let err = ToolError::new("timeout").with_meta("retry_after_secs", serde_json::json!(30));
571/// assert_eq!(err.metadata()["retry_after_secs"], 30);
572/// ```
573#[derive(Debug, Clone, PartialEq, Eq)]
574pub struct ToolError {
575    /// Human-readable error message sent to the model.
576    pub message: String,
577    /// Structured metadata for hooks / policies / logging.
578    /// NOT sent to the model.
579    metadata: std::collections::HashMap<String, serde_json::Value>,
580}
581
582impl ToolError {
583    /// Create a new tool error with no metadata.
584    pub fn new(message: impl Into<String>) -> Self {
585        Self {
586            message: message.into(),
587            metadata: std::collections::HashMap::new(),
588        }
589    }
590
591    /// Attach a single metadata key-value pair. Chainable.
592    ///
593    /// For attaching multiple fields at once, prefer
594    /// [`with_metadata`](Self::with_metadata) with a typed struct.
595    #[must_use]
596    pub fn with_meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
597        self.metadata.insert(key.into(), value);
598        self
599    }
600
601    /// Attach structured metadata from a serializable value.
602    ///
603    /// The value is serialized to a JSON object and its fields are **merged**
604    /// into the metadata map. See [`ToolOutput::with_metadata`] for details.
605    ///
606    /// # Errors
607    ///
608    /// Returns `Err(self)` if `value` doesn't serialize to a JSON object.
609    pub fn with_metadata<T: serde::Serialize>(mut self, value: &T) -> Result<Self, Self> {
610        let json = serde_json::to_value(value).map_err(|e| {
611            Self::new(format!(
612                "{} (metadata serialization also failed: {e})",
613                self.message
614            ))
615        })?;
616        match json {
617            serde_json::Value::Object(map) => {
618                self.metadata.extend(map);
619                Ok(self)
620            }
621            other => Err(Self::new(format!(
622                "{} (metadata must serialize to a JSON object, got {})",
623                self.message,
624                other_type_name(&other),
625            ))),
626        }
627    }
628
629    /// The structured metadata map.
630    #[must_use]
631    pub fn metadata(&self) -> &std::collections::HashMap<String, serde_json::Value> {
632        &self.metadata
633    }
634}
635
636impl std::fmt::Display for ToolError {
637    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
638        write!(f, "{}", self.message)
639    }
640}
641
642impl std::error::Error for ToolError {}
643
644impl From<String> for ToolError {
645    fn from(message: String) -> Self {
646        Self::new(message)
647    }
648}
649
650impl From<&str> for ToolError {
651    fn from(message: &str) -> Self {
652        Self::new(message)
653    }
654}
655
656impl From<std::io::Error> for ToolError {
657    fn from(e: std::io::Error) -> Self {
658        Self::new(e.to_string())
659            .with_meta("error_kind", serde_json::json!(format!("{:?}", e.kind())))
660    }
661}
662
663impl From<serde_json::Error> for ToolError {
664    fn from(e: serde_json::Error) -> Self {
665        Self::new(e.to_string())
666            .with_meta("category", serde_json::json!(format!("{:?}", e.classify())))
667    }
668}
669
670impl From<Box<dyn std::error::Error + Send + Sync>> for ToolError {
671    fn from(e: Box<dyn std::error::Error + Send + Sync>) -> Self {
672        Self::new(e.to_string())
673    }
674}
675
676impl From<std::convert::Infallible> for ToolError {
677    fn from(never: std::convert::Infallible) -> Self {
678        match never {}
679    }
680}
681
682/// Serialize a tool's return value to a JSON string.
683///
684/// **Deprecated**: Use [`ToolOutput::json()`] instead.
685#[doc(hidden)]
686#[deprecated(since = "0.2.0", note = "Use ToolOutput::json() instead")]
687pub fn __serialize_tool_result<T: serde::Serialize>(value: &T) -> Result<ToolOutput, ToolError> {
688    ToolOutput::json(value)
689}
690
691/// Compile-time dispatch for converting tool return values into [`ToolOutput`].
692///
693/// Uses the "autoref specialization" pattern: the compiler checks inherent
694/// methods on `Wrap<T>` first (for `String`, `ToolOutput`, `Json<T>`),
695/// then falls back to the `SerializeFallback` trait blanket impl for
696/// `T: Serialize`. This eliminates all proc-macro type-name matching.
697///
698/// **Not public API** — used only by the `#[llm_tool]` proc macro.
699#[doc(hidden)]
700pub mod __private {
701    use super::{Json, ToolError, ToolOutput};
702
703    /// Wrapper enabling compile-time method dispatch for tool output conversion.
704    pub struct Wrap<T>(pub T);
705
706    // ── Inherent methods (highest priority in method resolution) ──
707
708    impl Wrap<ToolOutput> {
709        /// `ToolOutput` → identity pass-through.
710        pub fn __convert(self) -> Result<ToolOutput, ToolError> {
711            Ok(self.0)
712        }
713    }
714
715    impl Wrap<String> {
716        /// `String` → wrap as plain text (no JSON encoding).
717        pub fn __convert(self) -> Result<ToolOutput, ToolError> {
718            Ok(ToolOutput::new(self.0))
719        }
720    }
721
722    impl<T: serde::Serialize> Wrap<Json<T>> {
723        /// `Json<T>` → serialize to JSON string.
724        pub fn __convert(self) -> Result<ToolOutput, ToolError> {
725            Ok((self.0).into())
726        }
727    }
728
729    // ── Trait fallback (lower priority in method resolution) ──
730
731    /// Fallback conversion for any `T: Serialize` not covered by inherent methods.
732    ///
733    /// The compiler checks inherent methods first, so `String` and `ToolOutput`
734    /// use their inherent impls. Everything else falls through to this trait,
735    /// which serializes the value to JSON.
736    pub trait SerializeFallback {
737        /// Serialize `self` to JSON and wrap as [`ToolOutput`].
738        fn __convert(self) -> Result<ToolOutput, ToolError>;
739    }
740
741    impl<T: serde::Serialize> SerializeFallback for Wrap<T> {
742        fn __convert(self) -> Result<ToolOutput, ToolError> {
743            ToolOutput::json(&self.0)
744        }
745    }
746}
747
748/// Describes a custom tool that can be registered with an agent.
749///
750/// This struct holds the metadata the SDK needs to expose the tool to the
751/// model. The actual handler function is registered separately via
752/// [`ToolRegistry::register`](super::ToolRegistry::register).
753#[derive(Debug, Clone, Serialize, Deserialize)]
754pub struct ToolDefinition {
755    /// Unique tool name (e.g. `"flash_device"`).
756    pub name: String,
757    /// Human-readable description shown to the model.
758    pub description: String,
759    /// JSON Schema describing the tool's parameters.
760    pub parameter_schema: serde_json::Value,
761}