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/// Context passed to Rust tools during dispatch.
21///
22/// Provides access to the current conversation ID, a shared key-value state
23/// store that persists across tool calls within the same agent turn, and an
24/// idle flag.
25///
26/// The state is backed by `Arc<RwLock<HashMap>>` so it can be cheaply cloned
27/// and shared across concurrent tool invocations. Reads acquire a shared
28/// lock; only writes take an exclusive lock.
29///
30/// # `get_state` vs `set_state` error handling
31///
32/// These two methods intentionally handle mutex poisoning differently:
33///
34/// - **[`get_state`](Self::get_state)** acquires a **read** lock and returns
35/// the caller-supplied `default` when the lock is poisoned. Reads are
36/// best-effort — a missing value is indistinguishable from a default, so
37/// returning `default` keeps the tool running without surfacing
38/// infrastructure errors to the model.
39///
40/// - **[`set_state`](Self::set_state)** acquires a **write** lock and returns
41/// `Err` when the lock is poisoned. Writes that silently vanish can cause
42/// subtle logic bugs, so callers must handle the failure explicitly.
43/// # Typed extensions
44///
45/// In addition to the string-keyed JSON state, `ToolContext` supports
46/// **typed extensions** via [`set_ext`](Self::set_ext) /
47/// [`get_ext`](Self::get_ext). These use `std::any::Any` under the hood
48/// and are keyed by `TypeId`, so callers store and retrieve strongly-typed
49/// values (typically `Arc<T>`) without serialization.
50///
51/// ```rust
52/// use std::sync::Arc;
53///
54/// use llm_tool::ToolContext;
55///
56/// struct MyState {
57/// session_dir: String,
58/// }
59///
60/// let ctx = ToolContext::new(None);
61/// ctx.set_ext(Arc::new(MyState {
62/// session_dir: "/tmp".into(),
63/// }))
64/// .unwrap();
65///
66/// let state: Arc<MyState> = ctx.get_ext::<Arc<MyState>>().unwrap();
67/// assert_eq!(state.session_dir, "/tmp");
68/// ```
69pub struct ToolContext {
70 conversation_id: Option<String>,
71 state: Arc<RwLock<HashMap<String, serde_json::Value>>>,
72 extensions: Arc<RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>>,
73 is_idle: bool,
74}
75
76impl ToolContext {
77 /// Create a new context with the given conversation ID.
78 #[must_use]
79 pub fn new(conversation_id: Option<String>) -> Self {
80 Self {
81 conversation_id,
82 state: Arc::new(RwLock::new(HashMap::new())),
83 extensions: Arc::new(RwLock::new(HashMap::new())),
84 is_idle: false,
85 }
86 }
87
88 /// Create a context that shares an externally-provided state map.
89 ///
90 /// Use this when multiple `ToolContext` instances (e.g. successive tool
91 /// calls within the same agent) must read/write the **same** state store.
92 #[must_use]
93 pub fn with_shared_state(
94 conversation_id: Option<String>,
95 state: Arc<RwLock<HashMap<String, serde_json::Value>>>,
96 ) -> Self {
97 Self {
98 conversation_id,
99 state,
100 extensions: Arc::new(RwLock::new(HashMap::new())),
101 is_idle: false,
102 }
103 }
104
105 /// Return the conversation ID, if one has been set.
106 #[must_use]
107 pub fn conversation_id(&self) -> Option<&str> {
108 self.conversation_id.as_deref()
109 }
110
111 /// Retrieve a value from the shared state, returning `default` if the key
112 /// is absent or the lock is poisoned.
113 ///
114 /// This method never fails — on a poisoned lock it logs a warning and
115 /// returns `default`. See the [struct-level docs](Self) for rationale.
116 #[must_use]
117 pub fn get_state(&self, key: &str, default: serde_json::Value) -> serde_json::Value {
118 match read_lock(&self.state) {
119 Ok(guard) => guard.get(key).cloned().unwrap_or(default),
120 Err(e) => {
121 tracing::warn!(key, error = %e, "ToolContext::get_state: lock poisoned, returning default");
122 default
123 }
124 }
125 }
126
127 /// Insert or update a value in the shared state.
128 ///
129 /// Unlike [`get_state`](Self::get_state), this method returns `Err` on a
130 /// poisoned lock because silently dropping a write can cause subtle bugs.
131 /// See the [struct-level docs](Self) for rationale.
132 ///
133 /// # Errors
134 ///
135 /// Returns [`ToolError`]
136 /// if the lock is poisoned.
137 pub fn set_state(&self, key: &str, value: serde_json::Value) -> Result<(), ToolError> {
138 match write_lock(&self.state) {
139 Ok(mut guard) => {
140 guard.insert(key.to_owned(), value);
141 Ok(())
142 }
143 Err(e) => {
144 let msg = format!("ToolContext::set_state: lock poisoned for key '{key}': {e}");
145 tracing::warn!("{msg}");
146 Err(ToolError::new(msg))
147 }
148 }
149 }
150
151 /// Whether the agent is currently idle.
152 #[must_use]
153 pub const fn is_idle(&self) -> bool {
154 self.is_idle
155 }
156
157 /// Store a typed value in the extensions map.
158 ///
159 /// Values are keyed by `TypeId`, so each concrete type can only appear
160 /// once. Typically used to store `Arc<T>` for shared, cloneable access.
161 ///
162 /// # Errors
163 ///
164 /// Returns [`ToolError`] if the lock is poisoned.
165 pub fn set_ext<T: Send + Sync + 'static>(&self, value: T) -> Result<(), ToolError> {
166 match write_lock(&self.extensions) {
167 Ok(mut exts) => {
168 exts.insert(TypeId::of::<T>(), Box::new(value));
169 Ok(())
170 }
171 Err(e) => {
172 let msg = format!(
173 "ToolContext::set_ext: lock poisoned for type '{}': {e}",
174 core::any::type_name::<T>()
175 );
176 tracing::warn!("{msg}");
177 Err(ToolError::new(msg))
178 }
179 }
180 }
181
182 /// Retrieve a clone of a typed value from the extensions map.
183 ///
184 /// Returns `None` if no value of type `T` has been stored via
185 /// [`set_ext`](Self::set_ext).
186 #[must_use]
187 pub fn get_ext<T: Clone + Send + Sync + 'static>(&self) -> Option<T> {
188 match read_lock(&self.extensions) {
189 Ok(exts) => exts
190 .get(&TypeId::of::<T>())
191 .and_then(|v| v.downcast_ref::<T>())
192 .cloned(),
193 Err(e) => {
194 tracing::warn!(error = %e, "ToolContext::get_ext: lock poisoned, returning None");
195 None
196 }
197 }
198 }
199}
200
201/// Re-export the `#[llm_tool]` proc macro for defining tools from plain functions.
202///
203/// # Usage
204///
205/// ```
206/// use llm_tool::{RustTool, ToolContext, ToolRegistry, llm_tool};
207///
208/// /// Adds two numbers together (with a twist).
209/// #[llm_tool]
210/// fn wonky_add(
211/// /// First number.
212/// a: i64,
213/// /// Second number.
214/// b: i64,
215/// ) -> Result<String, String> {
216/// Ok(format!("{}", a + b + 1))
217/// }
218///
219/// let mut registry = ToolRegistry::new();
220/// registry.register(WonkyAdd);
221/// assert_eq!(registry.definitions().len(), 1);
222/// ```
223pub use llm_tool_macros::llm_tool;
224// Re-export `JsonSchema` derive so tool authors can write `use llm_tool::JsonSchema;`
225// without adding `schemars` to their own `Cargo.toml`.
226pub use schemars::JsonSchema;
227
228/// Human-readable JSON type name for error messages.
229fn other_type_name(value: &serde_json::Value) -> &'static str {
230 match value {
231 serde_json::Value::Null => "null",
232 serde_json::Value::Bool(_) => "bool",
233 serde_json::Value::Number(_) => "number",
234 serde_json::Value::String(_) => "string",
235 serde_json::Value::Array(_) => "array",
236 serde_json::Value::Object(_) => "object",
237 }
238}
239
240/// The return value of a Rust tool execution.
241///
242/// Every tool produces a `ToolOutput` containing:
243/// - **`content`**: the text sent back to the model.
244/// - **`metadata`**: an optional structured key-value map available to hooks,
245/// policies, and logging pipelines — but **never** sent to the model.
246///
247/// # Ergonomics
248///
249/// `ToolOutput` implements `From<String>`, `From<&str>`, and `Display`, so
250/// simple tools can return plain strings without ceremony:
251///
252/// ```rust
253/// use llm_tool::ToolOutput;
254/// use serde::Serialize;
255///
256/// // From a String
257/// let out: ToolOutput = "hello".to_string().into();
258/// assert_eq!(out.content(), "hello");
259/// assert!(out.metadata().is_empty());
260///
261/// // From &str
262/// let out: ToolOutput = "world".into();
263/// assert_eq!(out.content(), "world");
264///
265/// // Structured metadata from a typed struct (preferred)
266/// #[derive(Serialize)]
267/// struct ReadMeta {
268/// bytes_read: usize,
269/// cached: bool,
270/// }
271///
272/// let out = ToolOutput::new("file contents…")
273/// .with_metadata(&ReadMeta {
274/// bytes_read: 1024,
275/// cached: true,
276/// })
277/// .unwrap();
278/// assert_eq!(out.metadata()["bytes_read"], 1024);
279/// assert_eq!(out.metadata()["cached"], true);
280///
281/// // Single ad-hoc entry
282/// let out = ToolOutput::new("done").with_meta("exit_code", serde_json::json!(0));
283/// assert_eq!(out.metadata()["exit_code"], 0);
284/// ```
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct ToolOutput {
287 /// The text content returned to the model.
288 content: String,
289 /// Structured metadata for hooks / policies / logging.
290 /// NOT sent to the model.
291 metadata: HashMap<String, serde_json::Value>,
292}
293
294impl ToolOutput {
295 /// Create a new `ToolOutput` with the given content and no metadata.
296 pub fn new(content: impl Into<String>) -> Self {
297 Self {
298 content: content.into(),
299 metadata: HashMap::new(),
300 }
301 }
302
303 /// Serialize a value to JSON and wrap it as tool output.
304 ///
305 /// The JSON string becomes the content sent to the model, but no
306 /// metadata is attached. For the zero-redundancy path that populates
307 /// **both** content and metadata from the same struct, use
308 /// [`from_metadata`](Self::from_metadata).
309 ///
310 /// ```rust
311 /// use llm_tool::{ToolOutput, ToolError};
312 ///
313 /// let data = serde_json::json!({"temp": 72, "unit": "F"});
314 /// let output = ToolOutput::json(&data).unwrap();
315 /// assert!(output.content().contains("72"));
316 /// assert!(output.metadata().is_empty()); // no metadata attached
317 /// ```
318 ///
319 /// # Errors
320 ///
321 /// Returns `Err(ToolError)` if serialization fails.
322 pub fn json<T: serde::Serialize>(value: &T) -> Result<Self, ToolError> {
323 serde_json::to_string(value)
324 .map(Self::new)
325 .map_err(|e| ToolError::new(format!("serialization failed: {e}")))
326 }
327
328 /// Create a `ToolOutput` where **both** the content and metadata come
329 /// from the same serializable value.
330 ///
331 /// - **Content** (sent to the model): the JSON representation of `value`.
332 /// - **Metadata** (hooks / policies / logging): the flattened object fields.
333 ///
334 /// This is the zero-redundancy path: define one struct, derive
335 /// `Serialize`, and everything is populated automatically.
336 ///
337 /// # Errors
338 ///
339 /// Returns `Err(ToolError)` if `value` doesn't serialize to a JSON object.
340 ///
341 /// # Example
342 ///
343 /// ```rust
344 /// use llm_tool::ToolOutput;
345 /// use serde::Serialize;
346 ///
347 /// #[derive(Serialize)]
348 /// struct Weather {
349 /// location: String,
350 /// temp_f: i32,
351 /// condition: String,
352 /// }
353 ///
354 /// let out = ToolOutput::from_metadata(&Weather {
355 /// location: "Seattle".into(),
356 /// temp_f: 72,
357 /// condition: "Sunny".into(),
358 /// })
359 /// .unwrap();
360 ///
361 /// // Model sees the JSON string
362 /// assert!(out.content().contains("Seattle"));
363 /// assert!(out.content().contains("72"));
364 ///
365 /// // Hooks see typed fields
366 /// assert_eq!(out.metadata()["location"], "Seattle");
367 /// assert_eq!(out.metadata()["temp_f"], 72);
368 /// ```
369 pub fn from_metadata<T: serde::Serialize>(value: &T) -> Result<Self, ToolError> {
370 let json_value = serde_json::to_value(value)
371 .map_err(|e| ToolError::new(format!("metadata serialization failed: {e}")))?;
372 // Serialize to string *before* destructuring so we borrow the Value
373 // instead of cloning the inner Map.
374 let content = json_value.to_string();
375 match json_value {
376 serde_json::Value::Object(map) => Ok(Self {
377 content,
378 metadata: map.into_iter().collect(),
379 }),
380 other => Err(ToolError::new(format!(
381 "metadata must serialize to a JSON object, got {}",
382 other_type_name(&other),
383 ))),
384 }
385 }
386
387 /// Attach a single metadata key-value pair. Chainable.
388 ///
389 /// For attaching multiple fields at once, prefer
390 /// [`with_metadata`](Self::with_metadata) with a typed struct.
391 #[must_use]
392 pub fn with_meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
393 self.metadata.insert(key.into(), value);
394 self
395 }
396
397 /// Attach structured metadata from a serializable value.
398 ///
399 /// The value is serialized to a JSON object and its fields are **merged**
400 /// into the metadata map. This is the preferred way to attach metadata
401 /// because it avoids stringly-typed keys and data duplication.
402 ///
403 /// # Errors
404 ///
405 /// Returns `Err(ToolError)` if `value` doesn't serialize to a JSON object
406 /// (e.g. it serializes to a scalar or array).
407 ///
408 /// # Example
409 ///
410 /// ```rust
411 /// use llm_tool::ToolOutput;
412 /// use serde::Serialize;
413 ///
414 /// #[derive(Serialize)]
415 /// struct FileMeta {
416 /// bytes_read: usize,
417 /// source: String,
418 /// }
419 ///
420 /// let out = ToolOutput::new("file contents")
421 /// .with_metadata(&FileMeta {
422 /// bytes_read: 1024,
423 /// source: "/etc/hosts".into(),
424 /// })
425 /// .unwrap();
426 /// assert_eq!(out.metadata()["bytes_read"], 1024);
427 /// assert_eq!(out.metadata()["source"], "/etc/hosts");
428 /// ```
429 pub fn with_metadata<T: serde::Serialize>(mut self, value: &T) -> Result<Self, ToolError> {
430 let json = serde_json::to_value(value)
431 .map_err(|e| ToolError::new(format!("metadata serialization failed: {e}")))?;
432 match json {
433 serde_json::Value::Object(map) => {
434 self.metadata.extend(map);
435 Ok(self)
436 }
437 other => Err(ToolError::new(format!(
438 "metadata must serialize to a JSON object, got {}",
439 other_type_name(&other),
440 ))),
441 }
442 }
443
444 /// The text content sent back to the model.
445 #[must_use]
446 pub fn content(&self) -> &str {
447 &self.content
448 }
449
450 /// Get the text content returned to the model (alias for `content()`).
451 #[must_use]
452 pub fn text(&self) -> &str {
453 &self.content
454 }
455
456 /// Consume self and return the owned content string.
457 #[must_use]
458 pub fn into_content(self) -> String {
459 self.content
460 }
461
462 /// The structured metadata map.
463 #[must_use]
464 pub fn metadata(&self) -> &HashMap<String, serde_json::Value> {
465 &self.metadata
466 }
467}
468
469impl core::fmt::Display for ToolOutput {
470 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
471 f.write_str(&self.content)
472 }
473}
474
475impl From<String> for ToolOutput {
476 fn from(content: String) -> Self {
477 Self::new(content)
478 }
479}
480
481impl From<&str> for ToolOutput {
482 fn from(content: &str) -> Self {
483 Self::new(content)
484 }
485}
486
487impl From<i64> for ToolOutput {
488 fn from(value: i64) -> Self {
489 Self::new(value.to_string())
490 }
491}
492
493impl From<f64> for ToolOutput {
494 fn from(value: f64) -> Self {
495 Self::new(value.to_string())
496 }
497}
498
499impl From<bool> for ToolOutput {
500 fn from(value: bool) -> Self {
501 Self::new(value.to_string())
502 }
503}
504
505impl From<serde_json::Value> for ToolOutput {
506 fn from(value: serde_json::Value) -> Self {
507 // serde_json::Value::to_string() never fails.
508 Self::new(value.to_string())
509 }
510}
511
512/// Wrapper for returning serializable values as JSON tool output.
513///
514/// Implements `From<Json<T>> for ToolOutput` so it works with the
515/// `#[llm_tool]` macro's `.into()` conversion — no `Result` wrapper needed
516/// for infallible serialization.
517///
518/// # Panics
519///
520/// The `From` conversion panics if `serde_json::to_string` fails.
521/// This only happens with broken `Serialize` implementations (e.g.,
522/// maps with non-string keys). For explicit error handling, use
523/// [`ToolOutput::json()`] instead.
524///
525/// # Example
526///
527/// ```rust
528/// use llm_tool::{Json, ToolOutput};
529/// use serde::Serialize;
530///
531/// #[derive(Serialize)]
532/// struct Weather {
533/// temp: f64,
534/// city: String,
535/// }
536///
537/// let output: ToolOutput = Json(Weather {
538/// temp: 72.0,
539/// city: "NYC".into(),
540/// })
541/// .into();
542/// assert!(output.content().contains("72"));
543/// ```
544pub struct Json<T>(pub T);
545
546impl<T: serde::Serialize> From<Json<T>> for ToolOutput {
547 fn from(json: Json<T>) -> Self {
548 let json_value = serde_json::to_value(&json.0)
549 .expect("Json<T> serialization failed — this is a bug in the Serialize impl");
550 let content = json_value.to_string();
551 match json_value {
552 serde_json::Value::Object(map) => Self {
553 content,
554 metadata: map.into_iter().collect(),
555 },
556 _ => Self::new(content),
557 }
558 }
559}
560
561/// An error returned from a tool execution.
562/// The error message is sent back to the model as the tool's error response.
563/// Structured metadata can be attached for hooks and logging — it is **not**
564/// sent to the model.
565///
566/// Implements `From<String>` and `From<&str>` for ergonomic construction.
567///
568/// # Example
569///
570/// ```
571/// use llm_tool::ToolError;
572/// use serde::Serialize;
573///
574/// let err: ToolError = "something went wrong".into();
575/// assert_eq!(err.to_string(), "something went wrong");
576///
577/// let err = ToolError::new(format!("failed to read {}", "file.txt"));
578/// assert!(err.to_string().contains("file.txt"));
579///
580/// // Structured metadata from a typed struct (preferred)
581/// #[derive(Serialize)]
582/// struct HttpErrorMeta {
583/// status_code: u16,
584/// url: String,
585/// }
586///
587/// let err = ToolError::new("HTTP request failed")
588/// .with_metadata(&HttpErrorMeta {
589/// status_code: 503,
590/// url: "https://example.com".into(),
591/// })
592/// .unwrap();
593/// assert_eq!(err.metadata()["status_code"], 503);
594///
595/// // Single ad-hoc entry
596/// let err = ToolError::new("timeout").with_meta("retry_after_secs", serde_json::json!(30));
597/// assert_eq!(err.metadata()["retry_after_secs"], 30);
598/// ```
599#[derive(Debug, Clone, PartialEq, Eq)]
600pub struct ToolError {
601 /// Human-readable error message sent to the model.
602 pub message: String,
603 /// Structured metadata for hooks / policies / logging.
604 /// NOT sent to the model.
605 metadata: HashMap<String, serde_json::Value>,
606}
607
608impl ToolError {
609 /// Create a new tool error with no metadata.
610 pub fn new(message: impl Into<String>) -> Self {
611 Self {
612 message: message.into(),
613 metadata: HashMap::new(),
614 }
615 }
616
617 /// Attach a single metadata key-value pair. Chainable.
618 ///
619 /// For attaching multiple fields at once, prefer
620 /// [`with_metadata`](Self::with_metadata) with a typed struct.
621 #[must_use]
622 pub fn with_meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
623 self.metadata.insert(key.into(), value);
624 self
625 }
626
627 /// Attach structured metadata from a serializable value.
628 ///
629 /// The value is serialized to a JSON object and its fields are **merged**
630 /// into the metadata map. See [`ToolOutput::with_metadata`] for details.
631 ///
632 /// # Errors
633 ///
634 /// Returns `Err(self)` if `value` doesn't serialize to a JSON object.
635 pub fn with_metadata<T: serde::Serialize>(mut self, value: &T) -> Result<Self, Self> {
636 let json = serde_json::to_value(value).map_err(|e| {
637 Self::new(format!(
638 "{} (metadata serialization also failed: {e})",
639 self.message
640 ))
641 })?;
642 match json {
643 serde_json::Value::Object(map) => {
644 self.metadata.extend(map);
645 Ok(self)
646 }
647 other => Err(Self::new(format!(
648 "{} (metadata must serialize to a JSON object, got {})",
649 self.message,
650 other_type_name(&other),
651 ))),
652 }
653 }
654
655 /// The structured metadata map.
656 #[must_use]
657 pub fn metadata(&self) -> &HashMap<String, serde_json::Value> {
658 &self.metadata
659 }
660}
661
662impl core::fmt::Display for ToolError {
663 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
664 write!(f, "{}", self.message)
665 }
666}
667
668impl core::error::Error for ToolError {}
669
670impl From<String> for ToolError {
671 fn from(message: String) -> Self {
672 Self::new(message)
673 }
674}
675
676impl From<&str> for ToolError {
677 fn from(message: &str) -> Self {
678 Self::new(message)
679 }
680}
681
682#[cfg(feature = "std")]
683impl From<std::io::Error> for ToolError {
684 fn from(e: std::io::Error) -> Self {
685 Self::new(e.to_string())
686 .with_meta("error_kind", serde_json::json!(format!("{:?}", e.kind())))
687 }
688}
689
690impl From<serde_json::Error> for ToolError {
691 fn from(e: serde_json::Error) -> Self {
692 Self::new(e.to_string())
693 .with_meta("category", serde_json::json!(format!("{:?}", e.classify())))
694 }
695}
696
697impl From<Box<dyn core::error::Error + Send + Sync>> for ToolError {
698 fn from(e: Box<dyn core::error::Error + Send + Sync>) -> Self {
699 Self::new(e.to_string())
700 }
701}
702
703impl From<core::convert::Infallible> for ToolError {
704 fn from(never: core::convert::Infallible) -> Self {
705 match never {}
706 }
707}
708
709/// Compile-time dispatch for converting tool return values into [`ToolOutput`].
710///
711/// Uses the "autoref specialization" pattern: the compiler checks inherent
712/// methods on `Wrap<T>` first (for `String`, `ToolOutput`, `Json<T>`),
713/// then falls back to the `SerializeFallback` trait blanket impl for
714/// `T: Serialize`. This eliminates all proc-macro type-name matching.
715///
716/// **Not public API** — used only by the `#[llm_tool]` proc macro.
717#[doc(hidden)]
718pub mod __private {
719 // Re-exports for generated code to work in no_std contexts.
720 #[cfg(not(feature = "std"))]
721 pub use alloc::borrow::Cow;
722 #[cfg(not(feature = "std"))]
723 use alloc::{
724 format,
725 string::{String, ToString},
726 };
727 pub use core::{convert::Into, result::Result};
728 #[cfg(feature = "std")]
729 pub use std::borrow::Cow;
730 /// Lazy initializer — [`std::sync::LazyLock`] under `std`,
731 /// [`spin::Lazy`] under `no_std`.
732 #[cfg(feature = "std")]
733 pub use std::sync::LazyLock as Lazy;
734
735 /// Lazy initializer — [`std::sync::LazyLock`] under `std`,
736 /// [`spin::LazyLock`] under `no_std`.
737 #[cfg(not(feature = "std"))]
738 pub use spin::LazyLock as Lazy;
739
740 use super::{Json, ToolError, ToolOutput};
741
742 /// Wrapper enabling compile-time method dispatch for tool output conversion.
743 pub struct Wrap<T>(pub T);
744
745 // ── Inherent methods (highest priority in method resolution) ──
746
747 impl Wrap<ToolOutput> {
748 /// `ToolOutput` → identity pass-through.
749 pub fn __convert(self) -> Result<ToolOutput, ToolError> {
750 Ok(self.0)
751 }
752 }
753
754 impl Wrap<String> {
755 /// `String` → wrap as plain text (no JSON encoding).
756 pub fn __convert(self) -> Result<ToolOutput, ToolError> {
757 Ok(ToolOutput::new(self.0))
758 }
759 }
760
761 impl<T: serde::Serialize> Wrap<Json<T>> {
762 /// `Json<T>` → serialize to JSON string.
763 pub fn __convert(self) -> Result<ToolOutput, ToolError> {
764 Ok((self.0).into())
765 }
766 }
767
768 // ── Trait fallback (lower priority in method resolution) ──
769
770 /// Fallback conversion for any `T: Serialize` not covered by inherent methods.
771 ///
772 /// The compiler checks inherent methods first, so `String` and `ToolOutput`
773 /// use their inherent impls. Everything else falls through to this trait,
774 /// which serializes the value to JSON.
775 pub trait SerializeFallback {
776 /// Serialize `self` to JSON and wrap as [`ToolOutput`].
777 fn __convert(self) -> Result<ToolOutput, ToolError>;
778 }
779
780 impl<T: serde::Serialize> SerializeFallback for Wrap<T> {
781 fn __convert(self) -> Result<ToolOutput, ToolError> {
782 let json_value = serde_json::to_value(&self.0)
783 .map_err(|e| ToolError::new(format!("serialization failed: {e}")))?;
784 let content = json_value.to_string();
785 match json_value {
786 serde_json::Value::Object(map) => Ok(ToolOutput {
787 content,
788 metadata: map.into_iter().collect(),
789 }),
790 _ => Ok(ToolOutput::new(content)),
791 }
792 }
793 }
794}
795
796/// Describes a custom tool that can be registered with an agent.
797///
798/// This struct holds the metadata the SDK needs to expose the tool to the
799/// model. The actual handler function is registered separately via
800/// [`ToolRegistry::register`](super::ToolRegistry::register).
801#[derive(Debug, Clone, Serialize, Deserialize)]
802pub struct ToolDefinition {
803 /// Unique tool name (e.g. `"flash_device"`).
804 pub name: String,
805 /// Human-readable description shown to the model.
806 pub description: String,
807 /// JSON Schema describing the tool's parameters.
808 pub parameter_schema: serde_json::Value,
809}