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