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