Skip to main content

llm_tool/
types.rs

1//! Core types for `llm-tool`: tool output, errors, context, and definitions.
2//!
3//! Defines the types that tools produce ([`ToolOutput`], [`ToolError`]),
4//! the execution context ([`ToolContext`]) shared across tool calls, and
5//! the [`ToolDefinition`] metadata sent to models.
6
7use alloc::{
8    borrow::ToOwned,
9    boxed::Box,
10    format,
11    string::{String, ToString},
12    sync::Arc,
13};
14use core::any::{Any, TypeId};
15
16use serde::{Deserialize, Serialize};
17
18use crate::compat::{HashMap, RwLock, read_lock, write_lock};
19
20/// Context passed to Rust tools during dispatch.
21///
22/// Provides access to the current conversation ID, a shared key-value state
23/// store that persists across tool calls within the same agent turn, and an
24/// idle flag.
25///
26/// The state is backed by `Arc<RwLock<HashMap>>` so it can be cheaply cloned
27/// and shared across concurrent tool invocations. Reads acquire a shared
28/// lock; only writes take an exclusive lock.
29///
30/// # `get_state` vs `set_state` error handling
31///
32/// These two methods intentionally handle mutex poisoning differently:
33///
34/// - **[`get_state`](Self::get_state)** acquires a **read** lock and returns
35///   the caller-supplied `default` when the lock is poisoned. Reads are
36///   best-effort — a missing value is indistinguishable from a default, so
37///   returning `default` keeps the tool running without surfacing
38///   infrastructure errors to the model.
39///
40/// - **[`set_state`](Self::set_state)** acquires a **write** lock and returns
41///   `Err` when the lock is poisoned. Writes that silently vanish can cause
42///   subtle logic bugs, so callers must handle the failure explicitly.
43/// # Typed extensions
44///
45/// In addition to the string-keyed JSON state, `ToolContext` supports
46/// **typed extensions** via [`set_ext`](Self::set_ext) /
47/// [`get_ext`](Self::get_ext). These use `std::any::Any` under the hood
48/// and are keyed by `TypeId`, so callers store and retrieve strongly-typed
49/// values (typically `Arc<T>`) without serialization.
50///
51/// ```rust
52/// use std::sync::Arc;
53///
54/// use llm_tool::ToolContext;
55///
56/// struct MyState {
57///     session_dir: String,
58/// }
59///
60/// let ctx = ToolContext::new(None);
61/// ctx.set_ext(Arc::new(MyState {
62///     session_dir: "/tmp".into(),
63/// }))
64/// .unwrap();
65///
66/// let state: Arc<MyState> = ctx.get_ext::<Arc<MyState>>().unwrap();
67/// assert_eq!(state.session_dir, "/tmp");
68/// ```
69pub struct ToolContext {
70    conversation_id: Option<String>,
71    state: Arc<RwLock<HashMap<String, serde_json::Value>>>,
72    extensions: Arc<RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>>,
73    is_idle: bool,
74}
75
76impl ToolContext {
77    /// Create a new context with the given conversation ID.
78    #[must_use]
79    pub fn new(conversation_id: Option<String>) -> Self {
80        Self {
81            conversation_id,
82            state: Arc::new(RwLock::new(HashMap::new())),
83            extensions: Arc::new(RwLock::new(HashMap::new())),
84            is_idle: false,
85        }
86    }
87
88    /// Create a context that shares an externally-provided state map.
89    ///
90    /// Use this when multiple `ToolContext` instances (e.g. successive tool
91    /// calls within the same agent) must read/write the **same** state store.
92    #[must_use]
93    pub fn with_shared_state(
94        conversation_id: Option<String>,
95        state: Arc<RwLock<HashMap<String, serde_json::Value>>>,
96    ) -> Self {
97        Self {
98            conversation_id,
99            state,
100            extensions: Arc::new(RwLock::new(HashMap::new())),
101            is_idle: false,
102        }
103    }
104
105    /// Return the conversation ID, if one has been set.
106    #[must_use]
107    pub fn conversation_id(&self) -> Option<&str> {
108        self.conversation_id.as_deref()
109    }
110
111    /// Retrieve a value from the shared state, returning `default` if the key
112    /// is absent or the lock is poisoned.
113    ///
114    /// This method never fails — on a poisoned lock it logs a warning and
115    /// returns `default`. See the [struct-level docs](Self) for rationale.
116    #[must_use]
117    pub fn get_state(&self, key: &str, default: serde_json::Value) -> serde_json::Value {
118        match read_lock(&self.state) {
119            Ok(guard) => guard.get(key).cloned().unwrap_or(default),
120            Err(e) => {
121                tracing::warn!(key, error = %e, "ToolContext::get_state: lock poisoned, returning default");
122                default
123            }
124        }
125    }
126
127    /// Insert or update a value in the shared state.
128    ///
129    /// Unlike [`get_state`](Self::get_state), this method returns `Err` on a
130    /// poisoned lock because silently dropping a write can cause subtle bugs.
131    /// See the [struct-level docs](Self) for rationale.
132    ///
133    /// # Errors
134    ///
135    /// Returns [`ToolError`]
136    /// if the lock is poisoned.
137    pub fn set_state(&self, key: &str, value: serde_json::Value) -> Result<(), ToolError> {
138        match write_lock(&self.state) {
139            Ok(mut guard) => {
140                guard.insert(key.to_owned(), value);
141                Ok(())
142            }
143            Err(e) => {
144                let msg = format!("ToolContext::set_state: lock poisoned for key '{key}': {e}");
145                tracing::warn!("{msg}");
146                Err(ToolError::new(msg))
147            }
148        }
149    }
150
151    /// Whether the agent is currently idle.
152    #[must_use]
153    pub const fn is_idle(&self) -> bool {
154        self.is_idle
155    }
156
157    /// Store a typed value in the extensions map.
158    ///
159    /// Values are keyed by `TypeId`, so each concrete type can only appear
160    /// once. Typically used to store `Arc<T>` for shared, cloneable access.
161    ///
162    /// # Errors
163    ///
164    /// Returns [`ToolError`] if the lock is poisoned.
165    pub fn set_ext<T: Send + Sync + 'static>(&self, value: T) -> Result<(), ToolError> {
166        match write_lock(&self.extensions) {
167            Ok(mut exts) => {
168                exts.insert(TypeId::of::<T>(), Box::new(value));
169                Ok(())
170            }
171            Err(e) => {
172                let msg = format!(
173                    "ToolContext::set_ext: lock poisoned for type '{}': {e}",
174                    core::any::type_name::<T>()
175                );
176                tracing::warn!("{msg}");
177                Err(ToolError::new(msg))
178            }
179        }
180    }
181
182    /// Retrieve a clone of a typed value from the extensions map.
183    ///
184    /// Returns `None` if no value of type `T` has been stored via
185    /// [`set_ext`](Self::set_ext).
186    #[must_use]
187    pub fn get_ext<T: Clone + Send + Sync + 'static>(&self) -> Option<T> {
188        match read_lock(&self.extensions) {
189            Ok(exts) => exts
190                .get(&TypeId::of::<T>())
191                .and_then(|v| v.downcast_ref::<T>())
192                .cloned(),
193            Err(e) => {
194                tracing::warn!(error = %e, "ToolContext::get_ext: lock poisoned, returning None");
195                None
196            }
197        }
198    }
199}
200
201/// Re-export the `#[llm_tool]` proc macro for defining tools from plain functions.
202///
203/// # Usage
204///
205/// ```
206/// use llm_tool::{RustTool, ToolContext, ToolRegistry, llm_tool};
207///
208/// /// Adds two numbers together (with a twist).
209/// #[llm_tool]
210/// fn wonky_add(
211///     /// First number.
212///     a: i64,
213///     /// Second number.
214///     b: i64,
215/// ) -> Result<String, String> {
216///     Ok(format!("{}", a + b + 1))
217/// }
218///
219/// let mut registry = ToolRegistry::new();
220/// registry.register(WonkyAdd);
221/// assert_eq!(registry.definitions().len(), 1);
222/// ```
223pub use llm_tool_macros::{llm_prompt, llm_resource, llm_tool};
224// Re-export `JsonSchema` derive so tool authors can write `use llm_tool::JsonSchema;`
225// without adding `schemars` to their own `Cargo.toml`.
226pub use schemars::JsonSchema;
227
228/// Human-readable JSON type name for error messages.
229fn other_type_name(value: &serde_json::Value) -> &'static str {
230    match value {
231        serde_json::Value::Null => "null",
232        serde_json::Value::Bool(_) => "bool",
233        serde_json::Value::Number(_) => "number",
234        serde_json::Value::String(_) => "string",
235        serde_json::Value::Array(_) => "array",
236        serde_json::Value::Object(_) => "object",
237    }
238}
239
240/// The return value of a Rust tool execution.
241///
242/// Every tool produces a `ToolOutput` containing:
243/// - **`content`**: the text sent back to the model.
244/// - **`metadata`**: an optional structured key-value map available to hooks,
245///   policies, and logging pipelines — but **never** sent to the model.
246///
247/// # Ergonomics
248///
249/// `ToolOutput` implements `From<String>`, `From<&str>`, and `Display`, so
250/// simple tools can return plain strings without ceremony:
251///
252/// ```rust
253/// use llm_tool::ToolOutput;
254/// use serde::Serialize;
255///
256/// // From a String
257/// let out: ToolOutput = "hello".to_string().into();
258/// assert_eq!(out.content(), "hello");
259/// assert!(out.metadata().is_empty());
260///
261/// // From &str
262/// let out: ToolOutput = "world".into();
263/// assert_eq!(out.content(), "world");
264///
265/// // Structured metadata from a typed struct (preferred)
266/// #[derive(Serialize)]
267/// struct ReadMeta {
268///     bytes_read: usize,
269///     cached: bool,
270/// }
271///
272/// let out = ToolOutput::new("file contents…")
273///     .with_metadata(&ReadMeta {
274///         bytes_read: 1024,
275///         cached: true,
276///     })
277///     .unwrap();
278/// assert_eq!(out.metadata()["bytes_read"], 1024);
279/// assert_eq!(out.metadata()["cached"], true);
280///
281/// // Single ad-hoc entry
282/// let out = ToolOutput::new("done").with_meta("exit_code", serde_json::json!(0));
283/// assert_eq!(out.metadata()["exit_code"], 0);
284/// ```
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
286pub struct ToolOutput {
287    /// The text content returned to the model.
288    content: String,
289    /// Structured metadata for hooks / policies / logging.
290    /// NOT sent to the model.
291    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
292    metadata: HashMap<String, serde_json::Value>,
293}
294
295impl ToolOutput {
296    /// Create a new `ToolOutput` with the given content and no metadata.
297    pub fn new(content: impl Into<String>) -> Self {
298        Self {
299            content: content.into(),
300            metadata: HashMap::new(),
301        }
302    }
303
304    /// Serialize a value to JSON and wrap it as tool output.
305    ///
306    /// The JSON string becomes the content sent to the model, but no
307    /// metadata is attached. For the zero-redundancy path that populates
308    /// **both** content and metadata from the same struct, use
309    /// [`from_metadata`](Self::from_metadata).
310    ///
311    /// ```rust
312    /// use llm_tool::{ToolOutput, ToolError};
313    ///
314    /// let data = serde_json::json!({"temp": 72, "unit": "F"});
315    /// let output = ToolOutput::json(&data).unwrap();
316    /// assert!(output.content().contains("72"));
317    /// assert!(output.metadata().is_empty()); // no metadata attached
318    /// ```
319    ///
320    /// # Errors
321    ///
322    /// Returns `Err(ToolError)` if serialization fails.
323    pub fn json<T: serde::Serialize>(value: &T) -> Result<Self, ToolError> {
324        serde_json::to_string(value)
325            .map(Self::new)
326            .map_err(|e| ToolError::new(format!("serialization failed: {e}")))
327    }
328
329    /// Create a `ToolOutput` where **both** the content and metadata come
330    /// from the same serializable value.
331    ///
332    /// - **Content** (sent to the model): the JSON representation of `value`.
333    /// - **Metadata** (hooks / policies / logging): the flattened object fields.
334    ///
335    /// This is the zero-redundancy path: define one struct, derive
336    /// `Serialize`, and everything is populated automatically.
337    ///
338    /// # Errors
339    ///
340    /// Returns `Err(ToolError)` if `value` doesn't serialize to a JSON object.
341    ///
342    /// # Example
343    ///
344    /// ```rust
345    /// use llm_tool::ToolOutput;
346    /// use serde::Serialize;
347    ///
348    /// #[derive(Serialize)]
349    /// struct Weather {
350    ///     location: String,
351    ///     temp_f: i32,
352    ///     condition: String,
353    /// }
354    ///
355    /// let out = ToolOutput::from_metadata(&Weather {
356    ///     location: "Seattle".into(),
357    ///     temp_f: 72,
358    ///     condition: "Sunny".into(),
359    /// })
360    /// .unwrap();
361    ///
362    /// // Model sees the JSON string
363    /// assert!(out.content().contains("Seattle"));
364    /// assert!(out.content().contains("72"));
365    ///
366    /// // Hooks see typed fields
367    /// assert_eq!(out.metadata()["location"], "Seattle");
368    /// assert_eq!(out.metadata()["temp_f"], 72);
369    /// ```
370    pub fn from_metadata<T: serde::Serialize>(value: &T) -> Result<Self, ToolError> {
371        let json_value = serde_json::to_value(value)
372            .map_err(|e| ToolError::new(format!("metadata serialization failed: {e}")))?;
373        // Serialize to string *before* destructuring so we borrow the Value
374        // instead of cloning the inner Map.
375        let content = json_value.to_string();
376        match json_value {
377            serde_json::Value::Object(map) => Ok(Self {
378                content,
379                metadata: map.into_iter().collect(),
380            }),
381            other => Err(ToolError::new(format!(
382                "metadata must serialize to a JSON object, got {}",
383                other_type_name(&other),
384            ))),
385        }
386    }
387
388    /// Attach a single metadata key-value pair. Chainable.
389    ///
390    /// For attaching multiple fields at once, prefer
391    /// [`with_metadata`](Self::with_metadata) with a typed struct.
392    #[must_use]
393    pub fn with_meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
394        self.metadata.insert(key.into(), value);
395        self
396    }
397
398    /// Attach structured metadata from a serializable value.
399    ///
400    /// The value is serialized to a JSON object and its fields are **merged**
401    /// into the metadata map. This is the preferred way to attach metadata
402    /// because it avoids stringly-typed keys and data duplication.
403    ///
404    /// # Errors
405    ///
406    /// Returns `Err(ToolError)` if `value` doesn't serialize to a JSON object
407    /// (e.g. it serializes to a scalar or array).
408    ///
409    /// # Example
410    ///
411    /// ```rust
412    /// use llm_tool::ToolOutput;
413    /// use serde::Serialize;
414    ///
415    /// #[derive(Serialize)]
416    /// struct FileMeta {
417    ///     bytes_read: usize,
418    ///     source: String,
419    /// }
420    ///
421    /// let out = ToolOutput::new("file contents")
422    ///     .with_metadata(&FileMeta {
423    ///         bytes_read: 1024,
424    ///         source: "/etc/hosts".into(),
425    ///     })
426    ///     .unwrap();
427    /// assert_eq!(out.metadata()["bytes_read"], 1024);
428    /// assert_eq!(out.metadata()["source"], "/etc/hosts");
429    /// ```
430    pub fn with_metadata<T: serde::Serialize>(mut self, value: &T) -> Result<Self, ToolError> {
431        let json = serde_json::to_value(value)
432            .map_err(|e| ToolError::new(format!("metadata serialization failed: {e}")))?;
433        match json {
434            serde_json::Value::Object(map) => {
435                self.metadata.extend(map);
436                Ok(self)
437            }
438            other => Err(ToolError::new(format!(
439                "metadata must serialize to a JSON object, got {}",
440                other_type_name(&other),
441            ))),
442        }
443    }
444
445    /// The text content sent back to the model.
446    #[must_use]
447    pub fn content(&self) -> &str {
448        &self.content
449    }
450
451    /// Get the text content returned to the model (alias for `content()`).
452    #[must_use]
453    pub fn text(&self) -> &str {
454        &self.content
455    }
456
457    /// Consume self and return the owned content string.
458    #[must_use]
459    pub fn into_content(self) -> String {
460        self.content
461    }
462
463    /// The structured metadata map.
464    #[must_use]
465    pub fn metadata(&self) -> &HashMap<String, serde_json::Value> {
466        &self.metadata
467    }
468}
469
470impl core::fmt::Display for ToolOutput {
471    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
472        f.write_str(&self.content)
473    }
474}
475
476impl From<String> for ToolOutput {
477    fn from(content: String) -> Self {
478        Self::new(content)
479    }
480}
481
482impl From<&str> for ToolOutput {
483    fn from(content: &str) -> Self {
484        Self::new(content)
485    }
486}
487
488impl From<i64> for ToolOutput {
489    fn from(value: i64) -> Self {
490        Self::new(value.to_string())
491    }
492}
493
494impl From<f64> for ToolOutput {
495    fn from(value: f64) -> Self {
496        Self::new(value.to_string())
497    }
498}
499
500impl From<bool> for ToolOutput {
501    fn from(value: bool) -> Self {
502        Self::new(value.to_string())
503    }
504}
505
506impl From<serde_json::Value> for ToolOutput {
507    fn from(value: serde_json::Value) -> Self {
508        // serde_json::Value::to_string() never fails.
509        Self::new(value.to_string())
510    }
511}
512
513/// Wrapper for returning serializable values as JSON tool output.
514///
515/// Implements `From<Json<T>> for ToolOutput` so it works with the
516/// `#[llm_tool]` macro's `.into()` conversion — no `Result` wrapper needed
517/// for infallible serialization.
518///
519/// # Panics
520///
521/// The `From` conversion panics if `serde_json::to_string` fails.
522/// This only happens with broken `Serialize` implementations (e.g.,
523/// maps with non-string keys). For explicit error handling, use
524/// [`ToolOutput::json()`] instead.
525///
526/// # Example
527///
528/// ```rust
529/// use llm_tool::{Json, ToolOutput};
530/// use serde::Serialize;
531///
532/// #[derive(Serialize)]
533/// struct Weather {
534///     temp: f64,
535///     city: String,
536/// }
537///
538/// let output: ToolOutput = Json(Weather {
539///     temp: 72.0,
540///     city: "NYC".into(),
541/// })
542/// .into();
543/// assert!(output.content().contains("72"));
544/// ```
545pub struct Json<T>(pub T);
546
547impl<T: serde::Serialize> From<Json<T>> for ToolOutput {
548    fn from(json: Json<T>) -> Self {
549        let json_value = serde_json::to_value(&json.0)
550            .expect("Json<T> serialization failed — this is a bug in the Serialize impl");
551        let content = json_value.to_string();
552        match json_value {
553            serde_json::Value::Object(map) => Self {
554                content,
555                metadata: map.into_iter().collect(),
556            },
557            _ => Self::new(content),
558        }
559    }
560}
561
562/// An error returned from a tool execution.
563/// The error message is sent back to the model as the tool's error response.
564/// Structured metadata can be attached for hooks and logging — it is **not**
565/// sent to the model.
566///
567/// Implements `From<String>` and `From<&str>` for ergonomic construction.
568///
569/// # Example
570///
571/// ```
572/// use llm_tool::ToolError;
573/// use serde::Serialize;
574///
575/// let err: ToolError = "something went wrong".into();
576/// assert_eq!(err.to_string(), "something went wrong");
577///
578/// let err = ToolError::new(format!("failed to read {}", "file.txt"));
579/// assert!(err.to_string().contains("file.txt"));
580///
581/// // Structured metadata from a typed struct (preferred)
582/// #[derive(Serialize)]
583/// struct HttpErrorMeta {
584///     status_code: u16,
585///     url: String,
586/// }
587///
588/// let err = ToolError::new("HTTP request failed")
589///     .with_metadata(&HttpErrorMeta {
590///         status_code: 503,
591///         url: "https://example.com".into(),
592///     })
593///     .unwrap();
594/// assert_eq!(err.metadata()["status_code"], 503);
595///
596/// // Single ad-hoc entry
597/// let err = ToolError::new("timeout").with_meta("retry_after_secs", serde_json::json!(30));
598/// assert_eq!(err.metadata()["retry_after_secs"], 30);
599/// ```
600#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
601pub struct ToolError {
602    /// Human-readable error message sent to the model.
603    pub message: String,
604    /// Structured metadata for hooks / policies / logging.
605    /// NOT sent to the model.
606    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
607    metadata: HashMap<String, serde_json::Value>,
608}
609
610impl ToolError {
611    /// Create a new tool error with no metadata.
612    pub fn new(message: impl Into<String>) -> Self {
613        Self {
614            message: message.into(),
615            metadata: HashMap::new(),
616        }
617    }
618
619    /// Attach a single metadata key-value pair. Chainable.
620    ///
621    /// For attaching multiple fields at once, prefer
622    /// [`with_metadata`](Self::with_metadata) with a typed struct.
623    #[must_use]
624    pub fn with_meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
625        self.metadata.insert(key.into(), value);
626        self
627    }
628
629    /// Attach structured metadata from a serializable value.
630    ///
631    /// The value is serialized to a JSON object and its fields are **merged**
632    /// into the metadata map. See [`ToolOutput::with_metadata`] for details.
633    ///
634    /// # Errors
635    ///
636    /// Returns `Err(self)` if `value` doesn't serialize to a JSON object.
637    pub fn with_metadata<T: serde::Serialize>(mut self, value: &T) -> Result<Self, Self> {
638        let json = serde_json::to_value(value).map_err(|e| {
639            Self::new(format!(
640                "{} (metadata serialization also failed: {e})",
641                self.message
642            ))
643        })?;
644        match json {
645            serde_json::Value::Object(map) => {
646                self.metadata.extend(map);
647                Ok(self)
648            }
649            other => Err(Self::new(format!(
650                "{} (metadata must serialize to a JSON object, got {})",
651                self.message,
652                other_type_name(&other),
653            ))),
654        }
655    }
656
657    /// The structured metadata map.
658    #[must_use]
659    pub fn metadata(&self) -> &HashMap<String, serde_json::Value> {
660        &self.metadata
661    }
662}
663
664impl core::fmt::Display for ToolError {
665    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
666        write!(f, "{}", self.message)
667    }
668}
669
670impl core::error::Error for ToolError {}
671
672impl From<String> for ToolError {
673    fn from(message: String) -> Self {
674        Self::new(message)
675    }
676}
677
678impl From<&str> for ToolError {
679    fn from(message: &str) -> Self {
680        Self::new(message)
681    }
682}
683
684#[cfg(feature = "std")]
685impl From<std::io::Error> for ToolError {
686    fn from(e: std::io::Error) -> Self {
687        Self::new(e.to_string())
688            .with_meta("error_kind", serde_json::json!(format!("{:?}", e.kind())))
689    }
690}
691
692impl From<serde_json::Error> for ToolError {
693    fn from(e: serde_json::Error) -> Self {
694        Self::new(e.to_string())
695            .with_meta("category", serde_json::json!(format!("{:?}", e.classify())))
696    }
697}
698
699impl From<Box<dyn core::error::Error + Send + Sync>> for ToolError {
700    fn from(e: Box<dyn core::error::Error + Send + Sync>) -> Self {
701        Self::new(e.to_string())
702    }
703}
704
705impl From<core::convert::Infallible> for ToolError {
706    fn from(never: core::convert::Infallible) -> Self {
707        match never {}
708    }
709}
710
711/// Compile-time dispatch for converting tool return values into [`ToolOutput`].
712///
713/// Uses the "autoref specialization" pattern: the compiler checks inherent
714/// methods on `Wrap<T>` first (for `String`, `ToolOutput`, `Json<T>`),
715/// then falls back to the `SerializeFallback` trait blanket impl for
716/// `T: Serialize`. This eliminates all proc-macro type-name matching.
717///
718/// **Not public API** — used only by the `#[llm_tool]` proc macro.
719#[doc(hidden)]
720pub mod __private {
721    // Re-exports for generated code to work in no_std contexts.
722    #[cfg(not(feature = "std"))]
723    pub use alloc::borrow::Cow;
724    #[cfg(not(feature = "std"))]
725    use alloc::{
726        format,
727        string::{String, ToString},
728    };
729    pub use core::{convert::Into, result::Result};
730    #[cfg(feature = "std")]
731    pub use std::borrow::Cow;
732    /// Lazy initializer — [`std::sync::LazyLock`] under `std`,
733    /// [`spin::Lazy`] under `no_std`.
734    #[cfg(feature = "std")]
735    pub use std::sync::LazyLock as Lazy;
736
737    /// Lazy initializer — [`std::sync::LazyLock`] under `std`,
738    /// [`spin::LazyLock`] under `no_std`.
739    #[cfg(not(feature = "std"))]
740    pub use spin::LazyLock as Lazy;
741
742    use super::{Json, ToolError, ToolOutput};
743
744    /// Wrapper enabling compile-time method dispatch for tool output conversion.
745    pub struct Wrap<T>(pub T);
746
747    // ── Inherent methods (highest priority in method resolution) ──
748
749    impl Wrap<ToolOutput> {
750        /// `ToolOutput` → identity pass-through.
751        pub fn __convert(self) -> Result<ToolOutput, ToolError> {
752            Ok(self.0)
753        }
754    }
755
756    impl Wrap<String> {
757        /// `String` → wrap as plain text (no JSON encoding).
758        pub fn __convert(self) -> Result<ToolOutput, ToolError> {
759            Ok(ToolOutput::new(self.0))
760        }
761        pub fn __convert_prompt(self) -> Result<super::PromptOutput, ToolError> {
762            Ok(super::PromptOutput::user(self.0))
763        }
764        pub fn __convert_resource(
765            self,
766            uri: &str,
767            mime_type: Option<&str>,
768        ) -> Result<super::ResourceOutput, ToolError> {
769            Ok(super::ResourceOutput::text(uri, mime_type, self.0))
770        }
771    }
772
773    impl Wrap<&str> {
774        pub fn __convert_prompt(self) -> Result<super::PromptOutput, ToolError> {
775            Ok(super::PromptOutput::user(self.0))
776        }
777        pub fn __convert_resource(
778            self,
779            uri: &str,
780            mime_type: Option<&str>,
781        ) -> Result<super::ResourceOutput, ToolError> {
782            Ok(super::ResourceOutput::text(uri, mime_type, self.0))
783        }
784    }
785
786    impl Wrap<super::PromptOutput> {
787        pub fn __convert_prompt(self) -> Result<super::PromptOutput, ToolError> {
788            Ok(self.0)
789        }
790    }
791
792    impl Wrap<super::ResourceOutput> {
793        pub fn __convert_resource(
794            self,
795            _uri: &str,
796            _mime_type: Option<&str>,
797        ) -> Result<super::ResourceOutput, ToolError> {
798            Ok(self.0)
799        }
800    }
801
802    impl<T: serde::Serialize> Wrap<Json<T>> {
803        /// `Json<T>` → serialize to JSON string.
804        pub fn __convert(self) -> Result<ToolOutput, ToolError> {
805            Ok((self.0).into())
806        }
807    }
808
809    // ── Trait fallback (lower priority in method resolution) ──
810
811    /// Fallback conversion for any `T: Serialize` not covered by inherent methods.
812    ///
813    /// The compiler checks inherent methods first, so `String` and `ToolOutput`
814    /// use their inherent impls. Everything else falls through to this trait,
815    /// which serializes the value to JSON.
816    pub trait SerializeFallback {
817        /// Serialize `self` to JSON and wrap as [`ToolOutput`].
818        fn __convert(self) -> Result<ToolOutput, ToolError>;
819    }
820
821    impl<T: serde::Serialize> SerializeFallback for Wrap<T> {
822        fn __convert(self) -> Result<ToolOutput, ToolError> {
823            let json_value = serde_json::to_value(&self.0)
824                .map_err(|e| ToolError::new(format!("serialization failed: {e}")))?;
825            let content = json_value.to_string();
826            match json_value {
827                serde_json::Value::Object(map) => Ok(ToolOutput {
828                    content,
829                    metadata: map.into_iter().collect(),
830                }),
831                _ => Ok(ToolOutput::new(content)),
832            }
833        }
834    }
835}
836
837/// Describes a custom tool that can be registered with an agent.
838///
839/// This struct holds the metadata the SDK needs to expose the tool to the
840/// model. The actual handler function is registered separately via
841/// [`ToolRegistry::register`](super::ToolRegistry::register).
842#[derive(Debug, Clone, Serialize, Deserialize)]
843pub struct ToolDefinition {
844    /// Unique tool name (e.g. `"flash_device"`).
845    pub name: String,
846    /// Human-readable description shown to the model.
847    pub description: String,
848    /// JSON Schema describing the tool's parameters.
849    pub parameter_schema: serde_json::Value,
850}
851
852// ── Prompt types ────────────────────────────────────────────────────
853
854/// Describes a prompt template available in the registry.
855#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
856pub struct PromptDefinition {
857    /// Prompt name.
858    pub name: String,
859    /// Human-readable description.
860    #[serde(default, skip_serializing_if = "String::is_empty")]
861    pub description: String,
862    /// Arguments accepted by this prompt.
863    #[serde(default, skip_serializing_if = "alloc::vec::Vec::is_empty")]
864    pub arguments: alloc::vec::Vec<PromptArgumentDefinition>,
865}
866
867/// An argument accepted by a prompt template.
868#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
869pub struct PromptArgumentDefinition {
870    /// Argument name.
871    pub name: String,
872    /// Argument description.
873    #[serde(default, skip_serializing_if = "String::is_empty")]
874    pub description: String,
875    /// Whether this argument is required.
876    #[serde(default)]
877    pub required: bool,
878}
879
880/// A rendered message inside a prompt output.
881#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
882pub struct PromptOutputMessage {
883    /// Role (`"user"` or `"assistant"`).
884    pub role: alloc::borrow::Cow<'static, str>,
885    /// Text content.
886    pub content: String,
887}
888
889/// The output returned by rendering a prompt template.
890#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
891pub struct PromptOutput {
892    /// Rendered messages.
893    pub messages: alloc::vec::Vec<PromptOutputMessage>,
894}
895
896impl PromptOutput {
897    /// Create a new prompt output with a single user message.
898    pub fn user(content: impl Into<String>) -> Self {
899        Self {
900            messages: alloc::vec![PromptOutputMessage {
901                role: alloc::borrow::Cow::Borrowed("user"),
902                content: content.into(),
903            }],
904        }
905    }
906}
907
908impl From<String> for PromptOutput {
909    fn from(content: String) -> Self {
910        Self::user(content)
911    }
912}
913
914impl From<&str> for PromptOutput {
915    fn from(content: &str) -> Self {
916        Self::user(content)
917    }
918}
919
920// ── Resource types ──────────────────────────────────────────────────
921
922/// Describes a resource or resource template available in the registry.
923#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
924#[serde(rename_all = "camelCase")]
925pub struct ResourceDefinition {
926    /// Resource URI (e.g. `file:///path` or `config://app`) or template pattern.
927    #[serde(rename = "uriTemplate")]
928    pub uri_template: String,
929    /// Human-readable name.
930    pub name: String,
931    /// Optional description.
932    #[serde(default, skip_serializing_if = "String::is_empty")]
933    pub description: String,
934    /// Optional MIME type.
935    #[serde(skip_serializing_if = "Option::is_none")]
936    pub mime_type: Option<String>,
937}
938
939/// A content block inside a resource read output.
940#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
941#[serde(untagged)]
942pub enum ResourceOutputContent {
943    /// UTF-8 text content.
944    #[serde(rename_all = "camelCase")]
945    Text {
946        /// Resource URI.
947        uri: String,
948        /// Optional MIME type.
949        #[serde(skip_serializing_if = "Option::is_none")]
950        mime_type: Option<String>,
951        /// Text string.
952        text: String,
953    },
954    /// Base64 binary content.
955    #[serde(rename_all = "camelCase")]
956    Blob {
957        /// Resource URI.
958        uri: String,
959        /// Optional MIME type.
960        #[serde(skip_serializing_if = "Option::is_none")]
961        mime_type: Option<String>,
962        /// Base64 blob data.
963        blob: String,
964    },
965}
966
967/// The output returned by reading a resource.
968#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
969pub struct ResourceOutput {
970    /// Returned content blocks.
971    pub contents: alloc::vec::Vec<ResourceOutputContent>,
972}
973
974impl ResourceOutput {
975    /// Create a text resource output.
976    pub fn text(uri: impl Into<String>, mime_type: Option<&str>, text: impl Into<String>) -> Self {
977        Self {
978            contents: alloc::vec![ResourceOutputContent::Text {
979                uri: uri.into(),
980                mime_type: mime_type.map(ToString::to_string),
981                text: text.into(),
982            }],
983        }
984    }
985
986    /// Create a binary blob resource output.
987    pub fn blob(uri: impl Into<String>, mime_type: Option<&str>, blob: impl Into<String>) -> Self {
988        Self {
989            contents: alloc::vec![ResourceOutputContent::Blob {
990                uri: uri.into(),
991                mime_type: mime_type.map(ToString::to_string),
992                blob: blob.into(),
993            }],
994        }
995    }
996}
997
998impl From<String> for ResourceOutput {
999    fn from(text: String) -> Self {
1000        Self::text("", None, text)
1001    }
1002}
1003
1004impl From<&str> for ResourceOutput {
1005    fn from(text: &str) -> Self {
1006        Self::text("", None, text)
1007    }
1008}