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