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    boxed::Box,
9    format,
10    string::{String, ToString},
11};
12
13/// Re-export the `#[llm_tool]` proc macro for defining tools from plain functions.
14///
15/// # Usage
16///
17/// ```
18/// use llm_tool::{RustTool, ToolContext, ToolRegistry, llm_tool};
19///
20/// /// Adds two numbers together (with a twist).
21/// #[llm_tool]
22/// fn wonky_add(
23///     /// First number.
24///     a: i64,
25///     /// Second number.
26///     b: i64,
27/// ) -> Result<String, String> {
28///     Ok(format!("{}", a + b + 1))
29/// }
30///
31/// let mut registry = ToolRegistry::new();
32/// registry.register(WonkyAdd);
33/// assert_eq!(registry.definitions().len(), 1);
34/// ```
35pub use llm_tool_macros::{llm_prompt, llm_resource, llm_tool};
36// Re-export `JsonSchema` derive so tool authors can write `use llm_tool::JsonSchema;`
37// without adding `schemars` to their own `Cargo.toml`.
38pub use schemars::JsonSchema;
39use serde::{Deserialize, Serialize};
40
41use crate::compat::HashMap;
42
43/// Human-readable JSON type name for error messages.
44const fn other_type_name(value: &serde_json::Value) -> &'static str {
45    match value {
46        serde_json::Value::Null => "null",
47        serde_json::Value::Bool(_) => "bool",
48        serde_json::Value::Number(_) => "number",
49        serde_json::Value::String(_) => "string",
50        serde_json::Value::Array(_) => "array",
51        serde_json::Value::Object(_) => "object",
52    }
53}
54
55/// The return value of a Rust tool execution.
56///
57/// Every tool produces a `ToolOutput` containing:
58/// - **`content`**: the text sent back to the model.
59/// - **`metadata`**: an optional structured key-value map available to hooks,
60///   policies, and logging pipelines — but **never** sent to the model.
61///
62/// # Ergonomics
63///
64/// `ToolOutput` implements `From<String>`, `From<&str>`, and `Display`, so
65/// simple tools can return plain strings without ceremony:
66///
67/// ```rust
68/// use llm_tool::ToolOutput;
69/// use serde::Serialize;
70///
71/// // From a String
72/// let out: ToolOutput = "hello".to_string().into();
73/// assert_eq!(out.content(), "hello");
74/// assert!(out.metadata().is_empty());
75///
76/// // From &str
77/// let out: ToolOutput = "world".into();
78/// assert_eq!(out.content(), "world");
79///
80/// // Structured metadata from a typed struct (preferred)
81/// #[derive(Serialize)]
82/// struct ReadMeta {
83///     bytes_read: usize,
84///     cached: bool,
85/// }
86///
87/// let out = ToolOutput::new("file contents…")
88///     .with_metadata(&ReadMeta {
89///         bytes_read: 1024,
90///         cached: true,
91///     })
92///     .unwrap();
93/// assert_eq!(out.metadata()["bytes_read"], 1024);
94/// assert_eq!(out.metadata()["cached"], true);
95///
96/// // Single ad-hoc entry
97/// let out = ToolOutput::new("done").with_meta("exit_code", serde_json::json!(0));
98/// assert_eq!(out.metadata()["exit_code"], 0);
99/// ```
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub struct ToolOutput {
102    /// Text content sent to the LLM.
103    pub(crate) content: String,
104    /// Structured metadata attached to the output.
105    /// NOT sent to the LLM.
106    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
107    pub(crate) metadata: HashMap<String, serde_json::Value>,
108}
109
110impl ToolOutput {
111    /// Create a new `ToolOutput` with the given content and no metadata.
112    pub fn new(content: impl Into<String>) -> Self {
113        Self {
114            content: content.into(),
115            metadata: HashMap::new(),
116        }
117    }
118
119    /// Serialize a value to JSON and wrap it as tool output.
120    ///
121    /// The JSON string becomes the content sent to the model, but no
122    /// metadata is attached. For the zero-redundancy path that populates
123    /// **both** content and metadata from the same struct, use
124    /// [`from_metadata`](Self::from_metadata).
125    ///
126    /// ```rust
127    /// use llm_tool::{ToolOutput, ToolError};
128    ///
129    /// let data = serde_json::json!({"temp": 72, "unit": "F"});
130    /// let output = ToolOutput::json(&data).unwrap();
131    /// assert!(output.content().contains("72"));
132    /// assert!(output.metadata().is_empty()); // no metadata attached
133    /// ```
134    ///
135    /// # Errors
136    ///
137    /// Returns `Err(ToolError)` if serialization fails.
138    pub fn json<T: serde::Serialize>(value: &T) -> Result<Self, ToolError> {
139        serde_json::to_string(value)
140            .map(Self::new)
141            .map_err(|e| ToolError::new(format!("serialization failed: {e}")))
142    }
143
144    /// Create a `ToolOutput` where **both** the content and metadata come
145    /// from the same serializable value.
146    ///
147    /// - **Content** (sent to the model): the JSON representation of `value`.
148    /// - **Metadata** (hooks / policies / logging): the flattened object fields.
149    ///
150    /// This is the zero-redundancy path: define one struct, derive
151    /// `Serialize`, and everything is populated automatically.
152    ///
153    /// # Errors
154    ///
155    /// Returns `Err(ToolError)` if `value` doesn't serialize to a JSON object.
156    ///
157    /// # Example
158    ///
159    /// ```rust
160    /// use llm_tool::ToolOutput;
161    /// use serde::Serialize;
162    ///
163    /// #[derive(Serialize)]
164    /// struct Weather {
165    ///     location: String,
166    ///     temp_f: i32,
167    ///     condition: String,
168    /// }
169    ///
170    /// let out = ToolOutput::from_metadata(&Weather {
171    ///     location: "Seattle".into(),
172    ///     temp_f: 72,
173    ///     condition: "Sunny".into(),
174    /// })
175    /// .unwrap();
176    ///
177    /// // Model sees the JSON string
178    /// assert!(out.content().contains("Seattle"));
179    /// assert!(out.content().contains("72"));
180    ///
181    /// // Hooks see typed fields
182    /// assert_eq!(out.metadata()["location"], "Seattle");
183    /// assert_eq!(out.metadata()["temp_f"], 72);
184    /// ```
185    pub fn from_metadata<T: serde::Serialize>(value: &T) -> Result<Self, ToolError> {
186        let json_value = serde_json::to_value(value)
187            .map_err(|e| ToolError::new(format!("metadata serialization failed: {e}")))?;
188        // Serialize to string *before* destructuring so we borrow the Value
189        // instead of cloning the inner Map.
190        let content = json_value.to_string();
191        match json_value {
192            serde_json::Value::Object(map) => Ok(Self {
193                content,
194                metadata: map.into_iter().collect(),
195            }),
196            other => Err(ToolError::new(format!(
197                "metadata must serialize to a JSON object, got {}",
198                other_type_name(&other),
199            ))),
200        }
201    }
202
203    /// Attach a single metadata key-value pair. Chainable.
204    ///
205    /// For attaching multiple fields at once, prefer
206    /// [`with_metadata`](Self::with_metadata) with a typed struct.
207    #[must_use]
208    pub fn with_meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
209        self.metadata.insert(key.into(), value);
210        self
211    }
212
213    /// Attach structured metadata from a serializable value.
214    ///
215    /// The value is serialized to a JSON object and its fields are **merged**
216    /// into the metadata map. This is the preferred way to attach metadata
217    /// because it avoids stringly-typed keys and data duplication.
218    ///
219    /// # Errors
220    ///
221    /// Returns `Err(ToolError)` if `value` doesn't serialize to a JSON object
222    /// (e.g. it serializes to a scalar or array).
223    ///
224    /// # Example
225    ///
226    /// ```rust
227    /// use llm_tool::ToolOutput;
228    /// use serde::Serialize;
229    ///
230    /// #[derive(Serialize)]
231    /// struct FileMeta {
232    ///     bytes_read: usize,
233    ///     source: String,
234    /// }
235    ///
236    /// let out = ToolOutput::new("file contents")
237    ///     .with_metadata(&FileMeta {
238    ///         bytes_read: 1024,
239    ///         source: "/etc/hosts".into(),
240    ///     })
241    ///     .unwrap();
242    /// assert_eq!(out.metadata()["bytes_read"], 1024);
243    /// assert_eq!(out.metadata()["source"], "/etc/hosts");
244    /// ```
245    pub fn with_metadata<T: serde::Serialize>(mut self, value: &T) -> Result<Self, ToolError> {
246        let json = serde_json::to_value(value)
247            .map_err(|e| ToolError::new(format!("metadata serialization failed: {e}")))?;
248        match json {
249            serde_json::Value::Object(map) => {
250                self.metadata.extend(map);
251                Ok(self)
252            }
253            other => Err(ToolError::new(format!(
254                "metadata must serialize to a JSON object, got {}",
255                other_type_name(&other),
256            ))),
257        }
258    }
259
260    /// The text content sent back to the model.
261    #[must_use]
262    pub fn content(&self) -> &str {
263        &self.content
264    }
265
266    /// Consume self and return the owned content string.
267    #[must_use]
268    pub fn into_content(self) -> String {
269        self.content
270    }
271
272    /// The structured metadata map.
273    #[must_use]
274    pub const fn metadata(&self) -> &HashMap<String, serde_json::Value> {
275        &self.metadata
276    }
277
278    /// Truncate the output text content according to the given [`TruncationPolicy`].
279    #[must_use]
280    pub fn truncated(mut self, policy: &TruncationPolicy) -> Self {
281        truncation::truncate_output(&mut self, policy);
282        self
283    }
284
285    /// Truncate the output text content according to the given [`TruncationPolicy`],
286    /// returning a detailed [`TruncationOutcome`].
287    ///
288    /// # Errors
289    ///
290    /// Returns [`SpillError`] if an [`OutputSpillSink`] is configured and fails.
291    pub fn truncate_detailed(
292        &mut self,
293        policy: &TruncationPolicy,
294    ) -> Result<TruncationOutcome<'static>, SpillError> {
295        truncation::truncate_output_detailed(self, policy)
296    }
297
298    /// Truncate the output text content with caller context according to [`TruncationPolicy`].
299    ///
300    /// # Errors
301    ///
302    /// Returns [`SpillError`] if an [`OutputSpillSink`] is configured and fails.
303    pub fn truncate_detailed_with_context(
304        &mut self,
305        policy: &TruncationPolicy,
306        ctx: &SpillContext<'_>,
307    ) -> Result<TruncationOutcome<'static>, SpillError> {
308        truncation::truncate_output_detailed_with_context(self, policy, ctx)
309    }
310}
311
312impl core::fmt::Display for ToolOutput {
313    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
314        f.write_str(&self.content)
315    }
316}
317
318impl From<String> for ToolOutput {
319    fn from(content: String) -> Self {
320        Self::new(content)
321    }
322}
323
324impl From<&str> for ToolOutput {
325    fn from(content: &str) -> Self {
326        Self::new(content)
327    }
328}
329
330impl From<i64> for ToolOutput {
331    fn from(value: i64) -> Self {
332        Self::new(value.to_string())
333    }
334}
335
336impl From<f64> for ToolOutput {
337    fn from(value: f64) -> Self {
338        Self::new(value.to_string())
339    }
340}
341
342impl From<bool> for ToolOutput {
343    fn from(value: bool) -> Self {
344        Self::new(value.to_string())
345    }
346}
347
348impl From<serde_json::Value> for ToolOutput {
349    fn from(value: serde_json::Value) -> Self {
350        // serde_json::Value::to_string() never fails.
351        Self::new(value.to_string())
352    }
353}
354
355/// Wrapper for returning serializable values as JSON tool output.
356///
357/// Implements `From<Json<T>> for ToolOutput` so it works with the
358/// `#[llm_tool]` macro's `.into()` conversion — no `Result` wrapper needed
359/// for infallible serialization.
360///
361/// # Panics
362///
363/// The `From` conversion panics if `serde_json::to_string` fails.
364/// This only happens with broken `Serialize` implementations (e.g.,
365/// maps with non-string keys). For explicit error handling, use
366/// [`ToolOutput::json()`] instead.
367///
368/// # Example
369///
370/// ```rust
371/// use llm_tool::{Json, ToolOutput};
372/// use serde::Serialize;
373///
374/// #[derive(Serialize)]
375/// struct Weather {
376///     temp: f64,
377///     city: String,
378/// }
379///
380/// let output: ToolOutput = Json(Weather {
381///     temp: 72.0,
382///     city: "NYC".into(),
383/// })
384/// .into();
385/// assert!(output.content().contains("72"));
386/// ```
387pub struct Json<T>(pub T);
388
389impl<T: serde::Serialize> From<Json<T>> for ToolOutput {
390    fn from(json: Json<T>) -> Self {
391        let json_value = serde_json::to_value(&json.0)
392            .expect("Json<T> serialization failed — this is a bug in the Serialize impl");
393        let content = json_value.to_string();
394        match json_value {
395            serde_json::Value::Object(map) => Self {
396                content,
397                metadata: map.into_iter().collect(),
398            },
399            _ => Self::new(content),
400        }
401    }
402}
403
404/// The category of a registered entity (tool, prompt, or resource).
405///
406/// Used by [`ToolError::not_found`] and the registry dispatchers to
407/// provide clear, type-safe identification of which kind of item was
408/// looked up.
409///
410/// # Example
411///
412/// ```
413/// use llm_tool::RegistryItem;
414///
415/// assert_eq!(RegistryItem::Tool.to_string(), "tool");
416/// assert_eq!(RegistryItem::Prompt.to_string(), "prompt");
417/// assert_eq!(RegistryItem::Resource.to_string(), "resource");
418/// ```
419#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
420pub enum RegistryItem {
421    /// An executable tool.
422    Tool,
423    /// A prompt template.
424    Prompt,
425    /// A readable resource.
426    Resource,
427}
428
429impl core::fmt::Display for RegistryItem {
430    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
431        match self {
432            Self::Tool => write!(f, "tool"),
433            Self::Prompt => write!(f, "prompt"),
434            Self::Resource => write!(f, "resource"),
435        }
436    }
437}
438
439/// An error returned from a tool execution.
440///
441/// The error message is sent back to the model as the tool's error response.
442/// Structured metadata can be attached for hooks and logging — it is **not**
443/// sent to the model.
444///
445/// Implements `From<String>` and `From<&str>` for ergonomic construction.
446///
447/// # Example
448///
449/// ```
450/// use llm_tool::ToolError;
451/// use serde::Serialize;
452///
453/// let err: ToolError = "something went wrong".into();
454/// assert_eq!(err.to_string(), "something went wrong");
455///
456/// let err = ToolError::new(format!("failed to read {}", "file.txt"));
457/// assert!(err.to_string().contains("file.txt"));
458///
459/// // Structured metadata from a typed struct (preferred)
460/// #[derive(Serialize)]
461/// struct HttpErrorMeta {
462///     status_code: u16,
463///     url: String,
464/// }
465///
466/// let err = ToolError::new("HTTP request failed")
467///     .with_metadata(&HttpErrorMeta {
468///         status_code: 503,
469///         url: "https://example.com".into(),
470///     })
471///     .unwrap();
472/// assert_eq!(err.metadata()["status_code"], 503);
473///
474/// // Single ad-hoc entry
475/// let err = ToolError::new("timeout").with_meta("retry_after_secs", serde_json::json!(30));
476/// assert_eq!(err.metadata()["retry_after_secs"], 30);
477/// ```
478#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
479pub struct ToolError {
480    /// Human-readable error message sent to the model.
481    pub message: String,
482    /// Structured metadata for hooks / policies / logging.
483    /// NOT sent to the model.
484    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
485    metadata: HashMap<String, serde_json::Value>,
486}
487
488impl ToolError {
489    /// Create a new tool error with no metadata.
490    pub fn new(message: impl Into<String>) -> Self {
491        Self {
492            message: message.into(),
493            metadata: HashMap::new(),
494        }
495    }
496
497    /// Metadata key identifying the structured category of an error.
498    pub const ERROR_KIND_KEY: &'static str = "error_kind";
499    /// [`ERROR_KIND_KEY`](Self::ERROR_KIND_KEY) value used by
500    /// [`not_found`](Self::not_found) to mark a registry-lookup miss.
501    pub const KIND_NOT_REGISTERED: &'static str = "not_registered";
502    /// [`ERROR_KIND_KEY`](Self::ERROR_KIND_KEY) value used by
503    /// [`destructive_in_parallel_batch`](Self::destructive_in_parallel_batch) when a
504    /// destructive tool is rejected during parallel batch execution.
505    pub const KIND_DESTRUCTIVE_BATCH: &'static str = "destructive_in_parallel_batch";
506
507    /// Construct an error for a registry lookup that found no entry named
508    /// `name` of the given [`RegistryItem`].
509    ///
510    /// The human-readable message is suitable to hand back to the model so it
511    /// can self-correct (e.g. retry with a valid name). An
512    /// [`ERROR_KIND_KEY`](Self::ERROR_KIND_KEY) metadata field is attached —
513    /// never sent to the model — so hosts can still distinguish a routing miss
514    /// from a genuine execution failure for telemetry or policy decisions.
515    /// Prefer [`is_not_found`](Self::is_not_found) over inspecting the metadata
516    /// directly.
517    ///
518    /// # Example
519    ///
520    /// ```
521    /// use llm_tool::{RegistryItem, ToolError};
522    ///
523    /// let err = ToolError::not_found(RegistryItem::Tool, "add_nummbers");
524    /// assert!(err.to_string().contains("add_nummbers"));
525    /// assert!(err.is_not_found());
526    /// ```
527    #[must_use]
528    pub fn not_found(kind: RegistryItem, name: &str) -> Self {
529        Self::new(format!("no {kind} named '{name}' is registered")).with_meta(
530            Self::ERROR_KIND_KEY,
531            serde_json::json!(Self::KIND_NOT_REGISTERED),
532        )
533    }
534
535    /// Construct a typed error when a tool with [`ToolEffect::Destructive`] is rejected
536    /// in unconfirmed parallel batch dispatch.
537    #[must_use]
538    pub fn destructive_in_parallel_batch(tool_name: &str) -> Self {
539        Self::new(format!(
540            "Tool '{tool_name}' has Destructive effect and cannot be executed in Parallel batch mode without explicit confirmation"
541        ))
542        .with_meta(
543            Self::ERROR_KIND_KEY,
544            serde_json::json!(Self::KIND_DESTRUCTIVE_BATCH),
545        )
546    }
547
548    /// Attach a single metadata key-value pair. Chainable.
549    ///
550    /// For attaching multiple fields at once, prefer
551    /// [`with_metadata`](Self::with_metadata) with a typed struct.
552    #[must_use]
553    pub fn with_meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
554        self.metadata.insert(key.into(), value);
555        self
556    }
557
558    /// Attach structured metadata from a serializable value.
559    ///
560    /// The value is serialized to a JSON object and its fields are **merged**
561    /// into the metadata map. See [`ToolOutput::with_metadata`] for details.
562    ///
563    /// # Errors
564    ///
565    /// Returns `Err(self)` if `value` doesn't serialize to a JSON object.
566    pub fn with_metadata<T: serde::Serialize>(mut self, value: &T) -> Result<Self, Self> {
567        let json = match serde_json::to_value(value) {
568            Ok(j) => j,
569            Err(e) => {
570                self.message =
571                    format!("{} (metadata serialization also failed: {e})", self.message);
572                return Err(self);
573            }
574        };
575        match json {
576            serde_json::Value::Object(map) => {
577                self.metadata.extend(map);
578                Ok(self)
579            }
580            other => {
581                self.message = format!(
582                    "{} (metadata must serialize to a JSON object, got {})",
583                    self.message,
584                    other_type_name(&other),
585                );
586                Err(self)
587            }
588        }
589    }
590
591    /// The structured metadata map.
592    #[must_use]
593    pub const fn metadata(&self) -> &HashMap<String, serde_json::Value> {
594        &self.metadata
595    }
596
597    /// Whether this error denotes a registry-lookup miss, i.e. it was produced
598    /// by [`not_found`](Self::not_found).
599    ///
600    /// Lets hosts branch on "the model asked for something that isn't
601    /// registered" versus "a registered handler failed" without matching on
602    /// message strings.
603    ///
604    /// # Example
605    ///
606    /// ```
607    /// use llm_tool::{RegistryItem, ToolError};
608    ///
609    /// assert!(ToolError::not_found(RegistryItem::Prompt, "summarize").is_not_found());
610    /// assert!(!ToolError::new("handler blew up").is_not_found());
611    /// ```
612    #[must_use]
613    pub fn is_not_found(&self) -> bool {
614        self.metadata
615            .get(Self::ERROR_KIND_KEY)
616            .and_then(serde_json::Value::as_str)
617            == Some(Self::KIND_NOT_REGISTERED)
618    }
619
620    /// Whether this error denotes a [`ToolEffect::Destructive`] tool rejected
621    /// during parallel batch dispatch.
622    #[must_use]
623    pub fn is_destructive_batch_rejected(&self) -> bool {
624        self.metadata
625            .get(Self::ERROR_KIND_KEY)
626            .and_then(serde_json::Value::as_str)
627            == Some(Self::KIND_DESTRUCTIVE_BATCH)
628    }
629}
630
631impl core::fmt::Display for ToolError {
632    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
633        write!(f, "{}", self.message)
634    }
635}
636
637impl core::error::Error for ToolError {}
638
639impl From<String> for ToolError {
640    fn from(message: String) -> Self {
641        Self::new(message)
642    }
643}
644
645impl From<&str> for ToolError {
646    fn from(message: &str) -> Self {
647        Self::new(message)
648    }
649}
650
651#[cfg(feature = "std")]
652impl From<std::io::Error> for ToolError {
653    fn from(e: std::io::Error) -> Self {
654        Self::new(e.to_string()).with_meta(
655            Self::ERROR_KIND_KEY,
656            serde_json::json!(format!("{:?}", e.kind())),
657        )
658    }
659}
660
661impl From<serde_json::Error> for ToolError {
662    fn from(e: serde_json::Error) -> Self {
663        Self::new(e.to_string())
664            .with_meta("category", serde_json::json!(format!("{:?}", e.classify())))
665    }
666}
667
668impl From<Box<dyn core::error::Error + Send + Sync>> for ToolError {
669    fn from(e: Box<dyn core::error::Error + Send + Sync>) -> Self {
670        Self::new(e.to_string())
671    }
672}
673
674impl From<core::convert::Infallible> for ToolError {
675    fn from(never: core::convert::Infallible) -> Self {
676        match never {}
677    }
678}
679
680pub mod context;
681pub mod definition;
682pub mod prompt;
683pub mod quarantine;
684pub mod resource;
685pub mod sanitized;
686pub mod truncation;
687
688pub use context::*;
689pub use definition::*;
690pub use prompt::*;
691pub use quarantine::*;
692pub use resource::*;
693pub use sanitized::*;
694pub use truncation::*;
695
696#[cfg(all(test, feature = "std"))]
697mod tests;