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