Skip to main content

rig_core/tool/
mod.rs

1//! Module defining tool related structs and traits.
2//!
3//! The [Tool] trait defines a simple interface for creating tools that can be used
4//! by [Agents](crate::agent::Agent).
5//!
6//! The [ToolEmbedding] trait extends the [Tool] trait to allow for tools that can be
7//! stored in a vector store and RAGged.
8//!
9//! The [ToolSet] struct is a collection of tools that can be used by an [Agent](crate::agent::Agent)
10//! and optionally RAGged.
11//!
12//! # Structured tool results
13//!
14//! A tool call resolves to a structured [`ToolExecutionResult`] — model-visible
15//! output, a machine-readable [`ToolOutcome`] (success, a classified
16//! [`ToolFailure`], skipped, or denied), and [`ToolResultExtensions`] metadata
17//! that is never sent to the model. This is what flows to the
18//! [`StepEvent::ToolResult`](crate::agent::StepEvent::ToolResult) hook so a
19//! policy can steer on *why* a tool failed without parsing strings. A tool
20//! classifies its own error type via [`Tool::classify_error`] and can return
21//! richer outcomes/metadata via [`Tool::call_structured`] and [`ToolReturn`].
22
23pub mod builtin;
24mod extensions;
25mod result;
26pub mod server;
27
28pub use extensions::{MissingExtension, ToolCallExtensions, ToolResultExtensions};
29pub use result::{
30    ToolExecutionResult, ToolFailure, ToolFailureKind, ToolOutcome, ToolReturn, ToolReturnOutcome,
31};
32use std::collections::HashMap;
33use std::fmt;
34use std::sync::Arc;
35
36use futures::Future;
37use indexmap::IndexMap;
38use serde::{Deserialize, Serialize};
39
40use crate::{
41    completion::{self, ToolDefinition},
42    embeddings::{embed::EmbedError, tool::ToolSchema},
43    wasm_compat::{WasmBoxedFuture, WasmCompatSend, WasmCompatSync},
44};
45
46#[derive(Debug, thiserror::Error)]
47pub enum ToolError {
48    #[cfg(not(target_family = "wasm"))]
49    /// Error returned by the tool
50    ToolCallError(#[from] Box<dyn std::error::Error + Send + Sync>),
51
52    #[cfg(target_family = "wasm")]
53    /// Error returned by the tool
54    ToolCallError(#[from] Box<dyn std::error::Error>),
55    /// Error caused by a de/serialization fail
56    JsonError(#[from] serde_json::Error),
57}
58
59impl fmt::Display for ToolError {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            ToolError::ToolCallError(e) => {
63                let error_str = e.to_string();
64                // This is required due to being able to use agents as tools
65                // which means it is possible to get recursive tool call errors
66                if error_str.starts_with("ToolCallError: ") {
67                    write!(f, "{}", error_str)
68                } else {
69                    write!(f, "ToolCallError: {}", error_str)
70                }
71            }
72            ToolError::JsonError(e) => write!(f, "JsonError: {e}"),
73        }
74    }
75}
76
77/// Trait that represents a simple LLM tool.
78///
79/// Tool authors provide flat metadata (`NAME`, [`description`](Self::description),
80/// and [`parameters`](Self::parameters)). Provider-facing [`ToolDefinition`]s are
81/// generated by Rig when tools are registered in an agent request.
82///
83/// # Example
84/// ```
85/// use rig_core::tool::{Tool, ToolSet};
86///
87/// #[derive(serde::Deserialize)]
88/// struct AddArgs {
89///     x: i32,
90///     y: i32,
91/// }
92///
93/// #[derive(Debug, thiserror::Error)]
94/// #[error("Math error")]
95/// struct MathError;
96///
97/// #[derive(serde::Deserialize, serde::Serialize)]
98/// struct Adder;
99///
100/// impl Tool for Adder {
101///     const NAME: &'static str = "add";
102///
103///     type Error = MathError;
104///     type Args = AddArgs;
105///     type Output = i32;
106///
107///     fn description(&self) -> String {
108///         "Add x and y together".to_string()
109///     }
110///
111///     fn parameters(&self) -> serde_json::Value {
112///         serde_json::json!({
113///             "type": "object",
114///             "properties": {
115///                 "x": {
116///                     "type": "number",
117///                     "description": "The first number to add"
118///                 },
119///                 "y": {
120///                     "type": "number",
121///                     "description": "The second number to add"
122///                 }
123///             }
124///         })
125///     }
126///
127///     async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
128///         let result = args.x + args.y;
129///         Ok(result)
130///     }
131/// }
132/// ```
133pub trait Tool: Sized + WasmCompatSend + WasmCompatSync {
134    /// The name of the tool. This name should be unique within a single
135    /// [`ToolSet`] or other registration scope that dispatches tools by name.
136    const NAME: &'static str;
137
138    /// The error type of the tool.
139    type Error: std::error::Error + WasmCompatSend + WasmCompatSync + 'static;
140    /// The arguments type of the tool.
141    type Args: for<'a> Deserialize<'a> + WasmCompatSend + WasmCompatSync;
142    /// The output type of the tool.
143    type Output: Serialize;
144
145    /// A method returning the name of the tool.
146    fn name(&self) -> String {
147        Self::NAME.to_string()
148    }
149
150    /// Model-facing description of what the tool does.
151    fn description(&self) -> String;
152
153    /// JSON Schema for the tool arguments.
154    fn parameters(&self) -> serde_json::Value;
155
156    /// The tool execution method.
157    /// Both the arguments and return value are a String since these values are meant to
158    /// be the output and input of LLM models (respectively)
159    fn call(
160        &self,
161        args: Self::Args,
162    ) -> impl Future<Output = Result<Self::Output, Self::Error>> + WasmCompatSend;
163
164    /// Tool execution with per-call runtime extensions.
165    ///
166    /// Override this to access runtime values (auth, session IDs, etc.)
167    /// injected by the caller via [`ToolCallExtensions`]. The default ignores
168    /// the extensions and delegates to [`Tool::call`].
169    ///
170    /// **Override contract:** the default [`Tool::call_structured`] delegates
171    /// here, so overriding this method is how you read extensions for the common
172    /// case — the agent loop drives [`call_structured`](Self::call_structured),
173    /// which reaches your override (with an empty [`ToolCallExtensions`] when no
174    /// caller supplied one). Under dynamic dispatch this then becomes the single
175    /// execution entry point (`call`'s body is unreachable that way; a direct
176    /// `Tool::call` still runs it), so put your logic here and treat a missing
177    /// value as the no-extensions case (e.g. [`ToolCallExtensions::get`]
178    /// returning `None`). If you *also* override
179    /// [`call_structured`](Self::call_structured), that override supersedes this
180    /// method on the agent's structured path — put your logic there instead.
181    fn call_with_extensions(
182        &self,
183        args: Self::Args,
184        _extensions: &ToolCallExtensions,
185    ) -> impl Future<Output = Result<Self::Output, Self::Error>> + WasmCompatSend {
186        self.call(args)
187    }
188
189    /// Classify an error returned by this tool into a structured [`ToolFailure`].
190    ///
191    /// This is how a tool's own error type reaches a hook, policy, or telemetry
192    /// pipeline as a machine-readable [`ToolFailureKind`] — with no string
193    /// parsing. The default classifies every error as
194    /// [`ToolFailureKind::Other`] with the error's `Display` as the message;
195    /// override it to map your error variants onto the standard kinds (timeout,
196    /// not-found, rate-limited, …) and attach a `code` / `http_status` /
197    /// `retryable` hint:
198    ///
199    /// ```rust,ignore
200    /// fn classify_error(&self, error: &Self::Error) -> ToolFailure {
201    ///     match error {
202    ///         MyError::Timeout => ToolFailure::timeout(error.to_string()),
203    ///         MyError::Http { status: 404, .. } => {
204    ///             ToolFailure::not_found(error.to_string()).with_http_status(404)
205    ///         }
206    ///         other => ToolFailure::other(other.to_string()),
207    ///     }
208    /// }
209    /// ```
210    fn classify_error(&self, error: &Self::Error) -> ToolFailure {
211        ToolFailure::other(error.to_string())
212    }
213
214    /// Execute the tool, returning a structured [`ToolReturn`] instead of a bare
215    /// output.
216    ///
217    /// The richest tool-execution entry point. The default calls
218    /// [`call_with_extensions`](Self::call_with_extensions) and wraps the output
219    /// as a plain [`ToolReturn::success`] with no metadata, so a tool that only
220    /// implements [`call`](Self::call) needs nothing extra. Override it to:
221    ///
222    /// - attach result metadata to a success
223    ///   (`ToolReturn::success(out).with_extension(..)`);
224    /// - report a handled failure that still shows output to the model
225    ///   ([`ToolReturn::failed`]);
226    /// - mark the call [`denied`](ToolReturn::denied) — the tool refused it (a
227    ///   framework hook `Flow::Skip` is what yields a *skipped* outcome, not the tool).
228    ///
229    /// **Override contract:** this is the single entry point under *structured
230    /// dynamic dispatch* — the agent loop routes every tool call here via the
231    /// blanket [`ToolDyn`] impl. If you override it, the `call` /
232    /// `call_with_extensions` bodies are unreachable on that structured path (a
233    /// direct call still runs them), so put your logic here. A returned
234    /// `Err(Self::Error)` is still classified via
235    /// [`classify_error`](Self::classify_error).
236    fn call_structured(
237        &self,
238        args: Self::Args,
239        extensions: &ToolCallExtensions,
240    ) -> impl Future<Output = Result<ToolReturn<Self::Output>, Self::Error>> + WasmCompatSend {
241        async move {
242            self.call_with_extensions(args, extensions)
243                .await
244                .map(ToolReturn::success)
245        }
246    }
247}
248
249/// Trait that represents an LLM tool that can be stored in a vector store and RAGged
250pub trait ToolEmbedding: Tool {
251    /// Error returned when reconstructing a dynamic tool from stored context.
252    type InitError: std::error::Error + WasmCompatSend + WasmCompatSync + 'static;
253
254    /// Type of the tool' context. This context will be saved and loaded from the
255    /// vector store when ragging the tool.
256    /// This context can be used to store the tool's static configuration and local
257    /// context.
258    type Context: for<'a> Deserialize<'a> + Serialize;
259
260    /// Type of the tool's state. This state will be passed to the tool when initializing it.
261    /// This state can be used to pass runtime arguments to the tool such as clients,
262    /// API keys and other configuration.
263    type State: WasmCompatSend;
264
265    /// A method returning the documents that will be used as embeddings for the tool.
266    /// This allows for a tool to be retrieved from multiple embedding "directions".
267    /// If the tool will not be RAGged, this method should return an empty vector.
268    fn embedding_docs(&self) -> Vec<String>;
269
270    /// A method returning the context of the tool.
271    fn context(&self) -> Self::Context;
272
273    /// A method to initialize the tool from the context, and a state.
274    fn init(state: Self::State, context: Self::Context) -> Result<Self, Self::InitError>;
275}
276
277/// Wrapper trait to allow for dynamic dispatch of simple tools.
278///
279/// This is the object-safe erased form of [`Tool`]: it exposes the same flat
280/// metadata and string-based execution methods for runtime dispatch.
281pub trait ToolDyn: WasmCompatSend + WasmCompatSync {
282    /// Returns the tool name used for dispatch and provider advertisement.
283    fn name(&self) -> String;
284
285    /// Model-facing description of what the tool does.
286    fn description(&self) -> String;
287
288    /// JSON Schema for the tool arguments.
289    fn parameters(&self) -> serde_json::Value;
290
291    /// Calls the tool with JSON-encoded arguments and returns model-facing text.
292    fn call<'a>(&'a self, args: String) -> WasmBoxedFuture<'a, Result<String, ToolError>>;
293
294    /// Dynamic dispatch variant of tool execution with per-call runtime extensions.
295    ///
296    /// The default ignores the extensions and delegates to [`ToolDyn::call`].
297    /// The blanket impl for [`Tool`] types overrides this to thread the
298    /// extensions through to [`Tool::call_with_extensions`].
299    fn call_with_extensions<'a>(
300        &'a self,
301        args: String,
302        _extensions: &'a ToolCallExtensions,
303    ) -> WasmBoxedFuture<'a, Result<String, ToolError>> {
304        self.call(args)
305    }
306
307    /// Execute the tool with per-call extensions, returning a structured
308    /// [`ToolExecutionResult`] (model output + [`ToolOutcome`] + result
309    /// extensions).
310    ///
311    /// This is the structured dynamic boundary the agent loop drives: the result
312    /// flows through to the
313    /// [`StepEvent::ToolResult`](crate::agent::StepEvent::ToolResult) hook event.
314    /// Unlike [`call`](Self::call) it never returns a bare error — a failure is
315    /// carried as [`ToolOutcome::Error`] inside the result, with the
316    /// model-visible message on [`ToolExecutionResult::model_output`].
317    ///
318    /// The default wraps [`call_with_extensions`](Self::call_with_extensions): an
319    /// `Ok` output becomes a [`ToolOutcome::Success`]; a [`ToolError`] is
320    /// classified ([`ToolError::JsonError`] as
321    /// [`ToolFailureKind::InvalidArgs`], otherwise [`ToolFailureKind::Other`]).
322    /// The blanket impl for [`Tool`] types overrides this to route through
323    /// [`Tool::call_structured`] and [`Tool::classify_error`]; a manual `ToolDyn`
324    /// impl should override it to emit precise outcomes (e.g. a real timeout).
325    fn call_structured<'a>(
326        &'a self,
327        args: String,
328        extensions: &'a ToolCallExtensions,
329    ) -> WasmBoxedFuture<'a, ToolExecutionResult> {
330        Box::pin(async move {
331            match self.call_with_extensions(args, extensions).await {
332                Ok(model_output) => ToolExecutionResult::success(model_output),
333                Err(err) => tool_error_to_execution_result(err),
334            }
335        })
336    }
337}
338
339fn serialize_tool_output(output: impl Serialize) -> serde_json::Result<String> {
340    match serde_json::to_value(output)? {
341        serde_json::Value::String(text) => Ok(text),
342        value => Ok(value.to_string()),
343    }
344}
345
346/// Deserialize JSON tool arguments, normalizing a bare `null` (which LLMs
347/// frequently send for tools whose arguments are all optional) to `{}`.
348///
349/// `serde_json::from_str::<T>("null")` fails for struct types even when every
350/// field is `Option<_>`, because JSON null does not deserialize to an empty
351/// object. Any args type that already accepts `null` (such as `()` or
352/// `Option<T>`) is preserved; the fallback to `{}` only applies after the
353/// original parse fails.
354fn parse_tool_args<A>(args: &str) -> serde_json::Result<A>
355where
356    A: for<'de> Deserialize<'de>,
357{
358    match serde_json::from_str(args) {
359        Ok(parsed) => Ok(parsed),
360        Err(err) if args.trim() == "null" => serde_json::from_str("{}").map_err(|_| err),
361        Err(err) => Err(err),
362    }
363}
364
365/// Map a [`ToolError`] surfaced by a string-returning [`ToolDyn`] path into a
366/// structured [`ToolExecutionResult`], classifying a JSON error as invalid
367/// arguments. Used by the default [`ToolDyn::call_structured`] for manual
368/// implementations that only provide the string [`ToolDyn::call`].
369fn tool_error_to_execution_result(err: ToolError) -> ToolExecutionResult {
370    let message = err.to_string();
371    let failure = match err {
372        ToolError::JsonError(_) => ToolFailure::invalid_args(message.clone()),
373        ToolError::ToolCallError(_) => ToolFailure::other(message.clone()),
374    };
375    ToolExecutionResult::failed(message, failure)
376}
377
378/// Generate a provider-facing [`ToolDefinition`] from a registered tool's
379/// flat metadata.
380///
381/// The tool name is always taken from [`ToolDyn::name`], making that registered
382/// name the single source of truth for provider advertisement and dispatch.
383pub fn tool_definition(tool: &dyn ToolDyn) -> ToolDefinition {
384    tool_definition_with_name(tool.name(), tool)
385}
386
387pub(crate) fn tool_definition_with_name(
388    name: impl Into<String>,
389    tool: &dyn ToolDyn,
390) -> ToolDefinition {
391    ToolDefinition {
392        name: name.into(),
393        description: tool.description(),
394        parameters: tool.parameters(),
395    }
396}
397
398impl<T: Tool> ToolDyn for T {
399    fn name(&self) -> String {
400        <Self as Tool>::name(self)
401    }
402
403    fn description(&self) -> String {
404        <Self as Tool>::description(self)
405    }
406
407    fn parameters(&self) -> serde_json::Value {
408        <Self as Tool>::parameters(self)
409    }
410
411    fn call<'a>(&'a self, args: String) -> WasmBoxedFuture<'a, Result<String, ToolError>> {
412        ToolDyn::call_with_extensions(self, args, &ToolCallExtensions::EMPTY)
413    }
414
415    fn call_with_extensions<'a>(
416        &'a self,
417        args: String,
418        extensions: &'a ToolCallExtensions,
419    ) -> WasmBoxedFuture<'a, Result<String, ToolError>> {
420        Box::pin(async move {
421            match parse_tool_args::<T::Args>(&args) {
422                Ok(args) => <Self as Tool>::call_with_extensions(self, args, extensions)
423                    .await
424                    .map_err(|e| ToolError::ToolCallError(Box::new(e)))
425                    .and_then(|output| serialize_tool_output(output).map_err(ToolError::JsonError)),
426                Err(e) => Err(ToolError::JsonError(e)),
427            }
428        })
429    }
430
431    /// Routes through [`Tool::call_structured`] so rich returns
432    /// ([`ToolReturn`]) and [`Tool::classify_error`] are honored: a JSON
433    /// argument parse failure becomes an
434    /// [`InvalidArgs`](ToolFailureKind::InvalidArgs) outcome, a returned
435    /// `Err(Self::Error)` is classified, and a successful [`ToolReturn`] is
436    /// serialized while preserving its outcome and extensions.
437    fn call_structured<'a>(
438        &'a self,
439        args: String,
440        extensions: &'a ToolCallExtensions,
441    ) -> WasmBoxedFuture<'a, ToolExecutionResult> {
442        Box::pin(async move {
443            let parsed = match parse_tool_args::<T::Args>(&args) {
444                Ok(parsed) => parsed,
445                Err(err) => {
446                    return ToolExecutionResult::failed(
447                        format!("failed to parse tool arguments: {err}"),
448                        ToolFailure::invalid_args(err.to_string()),
449                    );
450                }
451            };
452            match <Self as Tool>::call_structured(self, parsed, extensions).await {
453                Ok(tool_return) => tool_return.into_execution_result(),
454                Err(err) => {
455                    let failure = self.classify_error(&err);
456                    ToolExecutionResult::failed(err.to_string(), failure)
457                }
458            }
459        })
460    }
461}
462
463#[cfg(feature = "rmcp")]
464#[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
465pub mod rmcp;
466
467/// Wrapper trait to allow for dynamic dispatch of raggable tools
468pub trait ToolEmbeddingDyn: ToolDyn {
469    /// Serializes context needed to reconstruct this dynamic tool.
470    fn context(&self) -> serde_json::Result<serde_json::Value>;
471
472    /// Returns text fragments used to retrieve this tool from a vector store.
473    fn embedding_docs(&self) -> Vec<String>;
474}
475
476impl<T> ToolEmbeddingDyn for T
477where
478    T: ToolEmbedding + 'static,
479{
480    fn context(&self) -> serde_json::Result<serde_json::Value> {
481        serde_json::to_value(self.context())
482    }
483
484    fn embedding_docs(&self) -> Vec<String> {
485        self.embedding_docs()
486    }
487}
488
489#[derive(Clone)]
490pub(crate) enum ToolType {
491    Simple(Arc<dyn ToolDyn>),
492    Embedding(Arc<dyn ToolEmbeddingDyn>),
493}
494
495impl ToolType {
496    pub fn name(&self) -> String {
497        match self {
498            ToolType::Simple(tool) => tool.name(),
499            ToolType::Embedding(tool) => tool.name(),
500        }
501    }
502
503    pub fn definition_with_name(&self, name: impl Into<String>) -> ToolDefinition {
504        match self {
505            ToolType::Simple(tool) => tool_definition_with_name(name, &**tool),
506            ToolType::Embedding(tool) => tool_definition_with_name(name, &**tool),
507        }
508    }
509
510    pub async fn call_with_extensions(
511        &self,
512        args: String,
513        extensions: &ToolCallExtensions,
514    ) -> Result<String, ToolError> {
515        match self {
516            ToolType::Simple(tool) => tool.call_with_extensions(args, extensions).await,
517            ToolType::Embedding(tool) => tool.call_with_extensions(args, extensions).await,
518        }
519    }
520
521    /// Execute the tool, returning the structured [`ToolExecutionResult`].
522    pub async fn call_structured(
523        &self,
524        args: String,
525        extensions: &ToolCallExtensions,
526    ) -> ToolExecutionResult {
527        match self {
528            ToolType::Simple(tool) => tool.call_structured(args, extensions).await,
529            ToolType::Embedding(tool) => tool.call_structured(args, extensions).await,
530        }
531    }
532}
533
534#[derive(Debug, thiserror::Error)]
535pub enum ToolSetError {
536    /// Error returned by the tool
537    #[error("ToolCallError: {0}")]
538    ToolCallError(#[from] ToolError),
539
540    /// Could not find a tool
541    #[error("ToolNotFoundError: {0}")]
542    ToolNotFoundError(String),
543
544    /// JSON serialization or deserialization failed while preparing tool data.
545    #[error("JsonError: {0}")]
546    JsonError(#[from] serde_json::Error),
547
548    /// Tool call was interrupted. Primarily useful for agent multi-step/turn prompting.
549    #[error("Tool call interrupted")]
550    Interrupted,
551}
552
553/// A struct that holds a set of tools.
554///
555/// Tools are stored in an [`IndexMap`] keyed by name, so iteration
556/// (definitions, documents, schemas) follows registration order and the tool
557/// list sent to providers is deterministic across processes. Re-registering an
558/// existing name replaces the implementation but keeps its original position.
559#[derive(Default)]
560pub struct ToolSet {
561    pub(crate) tools: IndexMap<String, ToolType>,
562}
563
564impl ToolSet {
565    /// Create a new ToolSet from a list of tools
566    pub fn from_tools(tools: Vec<impl ToolDyn + 'static>) -> Self {
567        let mut toolset = Self::default();
568        tools.into_iter().for_each(|tool| {
569            toolset.add_tool(tool);
570        });
571        toolset
572    }
573
574    /// Create a new `ToolSet` from boxed dynamically-dispatched tools.
575    pub fn from_tools_boxed(tools: Vec<Box<dyn ToolDyn + 'static>>) -> Self {
576        let mut toolset = Self::default();
577        tools.into_iter().for_each(|tool| {
578            toolset.add_tool_boxed(tool);
579        });
580        toolset
581    }
582
583    /// Create a toolset builder
584    pub fn builder() -> ToolSetBuilder {
585        ToolSetBuilder::default()
586    }
587
588    /// Check if the toolset contains a tool with the given name
589    pub fn contains(&self, toolname: &str) -> bool {
590        self.tools.contains_key(toolname)
591    }
592
593    /// Add a tool to the toolset, returning the registered key used for it.
594    pub fn add_tool(&mut self, tool: impl ToolDyn + 'static) -> String {
595        self.insert(ToolType::Simple(Arc::new(tool)))
596    }
597
598    /// Adds a boxed tool to the toolset. Useful for situations when dynamic dispatch is required.
599    /// Returns the registered key used for the tool.
600    pub fn add_tool_boxed(&mut self, tool: Box<dyn ToolDyn>) -> String {
601        self.insert(ToolType::Simple(Arc::from(tool)))
602    }
603
604    pub(crate) fn insert(&mut self, tool: ToolType) -> String {
605        let name = tool.name();
606        self.insert_with_name(name, tool)
607    }
608
609    fn insert_with_name(&mut self, name: String, tool: ToolType) -> String {
610        // `IndexMap::insert` replaces the value while keeping the existing
611        // slot position, and returns the previous value when the name was
612        // already registered.
613        if self.tools.insert(name.clone(), tool).is_some() {
614            tracing::warn!(
615                tool_name = %name,
616                "a tool named {name:?} was already registered; replacing it with the new registration"
617            );
618        }
619        name
620    }
621
622    /// Remove a tool by name. Missing tools are ignored.
623    pub fn delete_tool(&mut self, tool_name: &str) {
624        // `shift_remove` preserves the order of the remaining tools;
625        // `swap_remove` would not.
626        self.tools.shift_remove(tool_name);
627    }
628
629    /// Merge another toolset into this one. Tools keep `toolset`'s
630    /// registration order; names that already exist are replaced in place.
631    pub fn add_tools(&mut self, toolset: ToolSet) {
632        for (name, tool) in toolset.tools {
633            self.insert_with_name(name, tool);
634        }
635    }
636
637    pub(crate) fn get(&self, toolname: &str) -> Option<&ToolType> {
638        self.tools.get(toolname)
639    }
640
641    /// Tool names in registration order.
642    pub(crate) fn ordered_names(&self) -> impl Iterator<Item = &String> {
643        self.tools.keys()
644    }
645
646    /// Registered tool names and tools in registration order.
647    fn ordered_entries(&self) -> impl Iterator<Item = (&String, &ToolType)> {
648        self.tools.iter()
649    }
650
651    /// Return definitions for all tools currently registered in the set, in
652    /// registration order.
653    pub fn get_tool_definitions(&self) -> Result<Vec<ToolDefinition>, ToolSetError> {
654        Ok(self
655            .ordered_entries()
656            .map(|(name, tool)| tool.definition_with_name(name.clone()))
657            .collect::<Vec<_>>())
658    }
659
660    /// Call a tool with the given name and arguments
661    pub async fn call(&self, toolname: &str, args: String) -> Result<String, ToolSetError> {
662        self.call_with_extensions(toolname, args, &ToolCallExtensions::EMPTY)
663            .await
664    }
665
666    /// Call a tool with the given name, arguments, and per-call runtime extensions.
667    ///
668    /// The extensions are threaded through to [`Tool::call_with_extensions`],
669    /// allowing tools to access caller-provided values (auth tokens, session
670    /// IDs, etc.).
671    pub async fn call_with_extensions(
672        &self,
673        toolname: &str,
674        args: String,
675        extensions: &ToolCallExtensions,
676    ) -> Result<String, ToolSetError> {
677        if let Some(tool) = self.tools.get(toolname) {
678            tracing::debug!(target: "rig",
679                "Calling tool {toolname} with args:\n{}",
680                args
681            );
682            Ok(tool.call_with_extensions(args, extensions).await?)
683        } else {
684            Err(ToolSetError::ToolNotFoundError(toolname.to_string()))
685        }
686    }
687
688    /// Call a tool by name, returning the structured [`ToolExecutionResult`].
689    ///
690    /// The structured counterpart of [`call`](Self::call): a failure is carried
691    /// as [`ToolOutcome::Error`] inside the result rather than a `Result::Err`,
692    /// and an unknown tool name resolves to a
693    /// [`NotFound`](ToolFailureKind::NotFound) outcome. This is the path the
694    /// agent loop drives so hooks and telemetry observe the structured outcome.
695    pub async fn call_structured(
696        &self,
697        toolname: &str,
698        args: String,
699        extensions: &ToolCallExtensions,
700    ) -> ToolExecutionResult {
701        match self.tools.get(toolname) {
702            Some(tool) => {
703                tracing::debug!(target: "rig", "Calling tool {toolname} with args:\n{args}");
704                tool.call_structured(args, extensions).await
705            }
706            None => ToolExecutionResult::failed(
707                format!("tool `{toolname}` not found"),
708                ToolFailure::not_found(format!("no tool named `{toolname}` is registered")),
709            ),
710        }
711    }
712
713    /// Get the documents of all the tools in the toolset
714    pub async fn documents(&self) -> Result<Vec<completion::Document>, ToolSetError> {
715        let mut docs = Vec::new();
716        for (name, tool) in self.ordered_entries() {
717            let definition = tool.definition_with_name(name.clone());
718            docs.push(completion::Document {
719                id: name.clone(),
720                text: format!(
721                    "\
722                    Tool: {}\n\
723                    Definition: \n\
724                    {}\
725                ",
726                    name,
727                    serde_json::to_string_pretty(&definition)?
728                ),
729                additional_props: HashMap::new(),
730            });
731        }
732        Ok(docs)
733    }
734
735    /// Convert tools in self to objects of type ToolSchema.
736    /// This is necessary because when adding tools to the EmbeddingBuilder because all
737    /// documents added to the builder must all be of the same type.
738    pub fn schemas(&self) -> Result<Vec<ToolSchema>, EmbedError> {
739        self.ordered_entries()
740            .filter_map(|(name, tool_type)| {
741                if let ToolType::Embedding(tool) = tool_type {
742                    Some(ToolSchema::from_tool(name.clone(), &**tool))
743                } else {
744                    None
745                }
746            })
747            .collect::<Result<Vec<_>, _>>()
748    }
749}
750
751#[derive(Default)]
752/// Builder for constructing a [`ToolSet`] with static and dynamic tools.
753pub struct ToolSetBuilder {
754    tools: Vec<ToolType>,
755}
756
757impl ToolSetBuilder {
758    /// Add a regular tool that is always available when the set is used.
759    pub fn static_tool(mut self, tool: impl ToolDyn + 'static) -> Self {
760        self.tools.push(ToolType::Simple(Arc::new(tool)));
761        self
762    }
763
764    /// Add a tool that can be represented as embeddings for dynamic retrieval.
765    pub fn dynamic_tool(mut self, tool: impl ToolEmbeddingDyn + 'static) -> Self {
766        self.tools.push(ToolType::Embedding(Arc::new(tool)));
767        self
768    }
769
770    /// Build the tool set, keyed by each tool's name.
771    pub fn build(self) -> ToolSet {
772        let mut toolset = ToolSet::default();
773        for tool in self.tools {
774            toolset.insert(tool);
775        }
776        toolset
777    }
778}
779
780#[cfg(test)]
781mod tests {
782    use crate::message::{DocumentSourceKind, ToolResultContent};
783    use crate::test_utils::{
784        MockExampleTool, MockImageOutputTool, MockObjectOutputTool, MockStringOutputTool,
785        MockToolError, mock_math_toolset,
786    };
787    use serde_json::json;
788
789    use super::*;
790
791    fn get_test_toolset() -> ToolSet {
792        mock_math_toolset()
793    }
794
795    #[test]
796    fn test_get_tool_definitions() {
797        let toolset = get_test_toolset();
798        let tools = toolset.get_tool_definitions().unwrap();
799        assert_eq!(tools.len(), 2);
800        assert_eq!(
801            tools
802                .iter()
803                .map(|tool| tool.name.as_str())
804                .collect::<Vec<_>>(),
805            vec!["add", "subtract"],
806            "provider definitions must use registered tool names in order"
807        );
808        assert!(tools.iter().all(|tool| !tool.description.is_empty()));
809        assert!(tools.iter().all(|tool| tool.parameters.is_object()));
810    }
811
812    #[test]
813    fn test_tool_deletion() {
814        let mut toolset = get_test_toolset();
815        assert_eq!(toolset.tools.len(), 2);
816        toolset.delete_tool("add");
817        assert!(!toolset.contains("add"));
818        assert_eq!(toolset.tools.len(), 1);
819        assert_eq!(
820            toolset.ordered_names().cloned().collect::<Vec<_>>(),
821            vec!["subtract".to_string()]
822        );
823    }
824
825    #[test]
826    fn deleting_a_middle_tool_preserves_order_of_survivors() {
827        // Guards the `shift_remove` (not `swap_remove`) choice in `delete_tool`.
828        // `swap_remove` would move the last tool into the deleted slot, so this
829        // only catches a regression with 3+ tools and a non-last deletion: here
830        // a `swap_remove("beta")` would yield [alpha, delta, gamma].
831        let mut toolset = ToolSet::default();
832        for name in ["alpha", "beta", "gamma", "delta"] {
833            toolset.add_tool(named_tool(name, "test tool"));
834        }
835
836        toolset.delete_tool("beta");
837
838        assert_eq!(
839            toolset.ordered_names().cloned().collect::<Vec<_>>(),
840            vec![
841                "alpha".to_string(),
842                "gamma".to_string(),
843                "delta".to_string()
844            ],
845            "survivors must keep their registration order after a middle deletion"
846        );
847    }
848
849    /// A tool whose name and definition are chosen at runtime, for ordering
850    /// and duplicate-registration tests.
851    struct NamedTool {
852        name: String,
853        description: String,
854    }
855
856    impl ToolDyn for NamedTool {
857        fn name(&self) -> String {
858            self.name.clone()
859        }
860
861        fn description(&self) -> String {
862            self.description.clone()
863        }
864
865        fn parameters(&self) -> serde_json::Value {
866            json!({ "type": "object", "properties": {} })
867        }
868
869        fn call(&self, _args: String) -> WasmBoxedFuture<'_, Result<String, ToolError>> {
870            let output = format!("called {}", self.description);
871            Box::pin(async move { Ok(output) })
872        }
873    }
874
875    fn named_tool(name: &str, description: &str) -> NamedTool {
876        NamedTool {
877            name: name.to_string(),
878            description: description.to_string(),
879        }
880    }
881
882    #[test]
883    fn tool_definition_uses_flattened_dyn_metadata() {
884        let tool = named_tool("alpha", "runtime description");
885        let definition = tool_definition(&tool);
886
887        assert_eq!(definition.name, "alpha");
888        assert_eq!(definition.description, "runtime description");
889        assert_eq!(definition.parameters["type"], "object");
890    }
891
892    #[tokio::test]
893    async fn tool_definitions_follow_registration_order() {
894        // Enough names that any non-order-preserving storage would almost
895        // surely surface a regression: its iteration order would differ from
896        // insertion order.
897        let names: Vec<String> = (0..32).map(|i| format!("tool_{i:02}")).collect();
898        let mut toolset = ToolSet::default();
899        for name in &names {
900            toolset.add_tool(named_tool(name, "test tool"));
901        }
902
903        let defs = toolset.get_tool_definitions().unwrap();
904        let def_names: Vec<String> = defs.into_iter().map(|def| def.name).collect();
905        assert_eq!(def_names, names);
906
907        let docs = toolset.documents().await.unwrap();
908        let doc_ids: Vec<String> = docs.into_iter().map(|doc| doc.id).collect();
909        assert_eq!(doc_ids, names);
910    }
911
912    #[tokio::test]
913    async fn registered_name_is_definition_source_of_truth() {
914        use std::sync::atomic::{AtomicUsize, Ordering};
915
916        struct ChangingNameTool {
917            calls: AtomicUsize,
918        }
919
920        impl ToolDyn for ChangingNameTool {
921            fn name(&self) -> String {
922                match self.calls.fetch_add(1, Ordering::SeqCst) {
923                    0 => "registered".to_string(),
924                    _ => "changed".to_string(),
925                }
926            }
927
928            fn description(&self) -> String {
929                "changes name after registration".to_string()
930            }
931
932            fn parameters(&self) -> serde_json::Value {
933                json!({ "type": "object", "properties": {} })
934            }
935
936            fn call(&self, _args: String) -> WasmBoxedFuture<'_, Result<String, ToolError>> {
937                Box::pin(async { Ok("ok".to_string()) })
938            }
939        }
940
941        let mut toolset = ToolSet::default();
942        toolset.add_tool(ChangingNameTool {
943            calls: AtomicUsize::new(0),
944        });
945
946        let defs = toolset.get_tool_definitions().unwrap();
947        assert_eq!(defs[0].name, "registered");
948
949        let docs = toolset.documents().await.unwrap();
950        assert_eq!(docs[0].id, "registered");
951        assert!(docs[0].text.contains("registered"));
952        assert!(!docs[0].text.contains("changed"));
953    }
954
955    #[test]
956    fn dynamic_tool_schemas_use_registered_name() {
957        use std::sync::atomic::{AtomicUsize, Ordering};
958
959        #[derive(Debug, thiserror::Error)]
960        #[error("init error")]
961        struct InitError;
962
963        struct ChangingDynamicTool {
964            calls: AtomicUsize,
965        }
966
967        impl Tool for ChangingDynamicTool {
968            const NAME: &'static str = "unused";
969            type Error = MockToolError;
970            type Args = serde_json::Value;
971            type Output = String;
972
973            fn name(&self) -> String {
974                match self.calls.fetch_add(1, Ordering::SeqCst) {
975                    0 => "registered_dynamic".to_string(),
976                    _ => "changed_dynamic".to_string(),
977                }
978            }
979
980            fn description(&self) -> String {
981                "dynamic tool".to_string()
982            }
983
984            fn parameters(&self) -> serde_json::Value {
985                json!({ "type": "object", "properties": {} })
986            }
987
988            async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
989                Ok("ok".to_string())
990            }
991        }
992
993        impl ToolEmbedding for ChangingDynamicTool {
994            type InitError = InitError;
995            type Context = ();
996            type State = ();
997
998            fn embedding_docs(&self) -> Vec<String> {
999                vec!["dynamic tool docs".to_string()]
1000            }
1001
1002            fn context(&self) -> Self::Context {}
1003
1004            fn init(_state: Self::State, _context: Self::Context) -> Result<Self, Self::InitError> {
1005                Ok(Self {
1006                    calls: AtomicUsize::new(0),
1007                })
1008            }
1009        }
1010
1011        let toolset = ToolSet::builder()
1012            .dynamic_tool(ChangingDynamicTool {
1013                calls: AtomicUsize::new(0),
1014            })
1015            .build();
1016
1017        let schemas = toolset.schemas().unwrap();
1018        assert_eq!(schemas.len(), 1);
1019        assert_eq!(schemas[0].name, "registered_dynamic");
1020        assert_eq!(schemas[0].embedding_docs, vec!["dynamic tool docs"]);
1021    }
1022
1023    #[tokio::test]
1024    async fn duplicate_registration_replaces_in_place() {
1025        let mut toolset = ToolSet::default();
1026        toolset.add_tool(named_tool("alpha", "first alpha"));
1027        toolset.add_tool(named_tool("beta", "beta"));
1028        toolset.add_tool(named_tool("alpha", "second alpha"));
1029
1030        let defs = toolset.get_tool_definitions().unwrap();
1031        assert_eq!(
1032            defs.iter().map(|def| def.name.as_str()).collect::<Vec<_>>(),
1033            vec!["alpha", "beta"],
1034            "the duplicate should be deduped and keep its original position"
1035        );
1036        assert_eq!(
1037            defs[0].description, "second alpha",
1038            "the last registration should win"
1039        );
1040
1041        let output = toolset.call("alpha", "{}".to_string()).await.unwrap();
1042        assert_eq!(output, "called second alpha");
1043    }
1044
1045    #[tokio::test]
1046    async fn add_tools_merges_in_order_and_replaces_existing() {
1047        let mut base = ToolSet::default();
1048        base.add_tool(named_tool("alpha", "base alpha"));
1049        base.add_tool(named_tool("beta", "base beta"));
1050
1051        let mut incoming = ToolSet::default();
1052        incoming.add_tool(named_tool("gamma", "incoming gamma"));
1053        incoming.add_tool(named_tool("alpha", "incoming alpha"));
1054
1055        base.add_tools(incoming);
1056
1057        let defs = base.get_tool_definitions().unwrap();
1058        assert_eq!(
1059            defs.iter().map(|def| def.name.as_str()).collect::<Vec<_>>(),
1060            vec!["alpha", "beta", "gamma"],
1061            "merged tools should follow registration order with replaced names keeping position"
1062        );
1063        assert_eq!(defs[0].description, "incoming alpha");
1064    }
1065
1066    #[tokio::test]
1067    async fn string_tool_outputs_are_preserved_verbatim() {
1068        let mut toolset = ToolSet::default();
1069        toolset.add_tool(MockStringOutputTool);
1070
1071        let output = toolset
1072            .call("string_output", "{}".to_string())
1073            .await
1074            .expect("tool should succeed");
1075
1076        assert_eq!(output, "Hello\nWorld");
1077    }
1078
1079    #[tokio::test]
1080    async fn structured_string_tool_outputs_remain_parseable() {
1081        let mut toolset = ToolSet::default();
1082        toolset.add_tool(MockImageOutputTool);
1083
1084        let output = toolset
1085            .call("image_output", "{}".to_string())
1086            .await
1087            .expect("tool should succeed");
1088        let content = ToolResultContent::from_tool_output(output);
1089
1090        assert_eq!(content.len(), 1);
1091        match content.first() {
1092            ToolResultContent::Image(image) => {
1093                assert!(matches!(image.data, DocumentSourceKind::Base64(_)));
1094                assert_eq!(image.media_type, Some(crate::message::ImageMediaType::PNG));
1095            }
1096            other => panic!("expected image tool result content, got {other:?}"),
1097        }
1098    }
1099
1100    #[tokio::test]
1101    async fn object_tool_outputs_still_serialize_as_json() {
1102        let mut toolset = ToolSet::default();
1103        toolset.add_tool(MockObjectOutputTool);
1104
1105        let output = toolset
1106            .call("object_output", "{}".to_string())
1107            .await
1108            .expect("tool should succeed");
1109
1110        assert!(output.starts_with('{'));
1111        assert_eq!(
1112            serde_json::from_str::<serde_json::Value>(&output).unwrap(),
1113            json!({
1114                "status": "ok",
1115                "count": 42
1116            })
1117        );
1118    }
1119
1120    #[tokio::test]
1121    async fn null_args_are_preserved_for_unit_args() {
1122        let mut toolset = ToolSet::default();
1123        toolset.add_tool(MockExampleTool);
1124
1125        let output = toolset
1126            .call("example_tool", "null".to_string())
1127            .await
1128            .expect("unit args should accept null without object fallback");
1129
1130        assert_eq!(output, "Example answer");
1131    }
1132
1133    // Struct-typed args with all-optional fields — serde rejects `null` for these
1134    // even though the fields are optional. The normalization in `ToolDyn::call`
1135    // falls back from `null` to `{}` so callers can omit the
1136    // wrapping `Option<Args>` workaround.
1137    #[tokio::test]
1138    async fn null_args_are_normalized_to_empty_object() {
1139        use crate::test_utils::MockToolError;
1140
1141        #[derive(serde::Deserialize, serde::Serialize)]
1142        struct NoRequiredArgs {
1143            label: Option<String>,
1144        }
1145
1146        struct NoArgTool;
1147
1148        impl Tool for NoArgTool {
1149            const NAME: &'static str = "no_arg_tool";
1150            type Error = MockToolError;
1151            type Args = NoRequiredArgs;
1152            type Output = String;
1153
1154            fn description(&self) -> String {
1155                "Tool with no required arguments".to_string()
1156            }
1157
1158            fn parameters(&self) -> serde_json::Value {
1159                json!({"type": "object", "properties": {}})
1160            }
1161
1162            async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
1163                Ok(args.label.unwrap_or_else(|| "default".to_string()))
1164            }
1165        }
1166
1167        let mut toolset = ToolSet::default();
1168        toolset.add_tool(NoArgTool);
1169
1170        // `null` is what LLMs send when no arguments are provided; without the
1171        // normalization this would return `ToolError::JsonError`.
1172        let output = toolset
1173            .call("no_arg_tool", "null".to_string())
1174            .await
1175            .expect("null args should succeed after normalisation");
1176
1177        assert_eq!(output, "default");
1178    }
1179}