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