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