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