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