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;