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