Skip to main content

rig_core/tool/
contextual.rs

1//! Contextual tool authoring and JSON-argument dispatch adapters.
2//! [`ToolContext`] carries typed inbound values and host-only result metadata;
3//! model-visible outputs retain their text, JSON, or multimodal representation.
4//!
5//! ```
6//! use rig_core::{message::ToolName, tool::{DynamicTool, ToolOutput}};
7//!
8//! let tool = DynamicTool::new_with_context(ToolName::new("echo")?, "Echo JSON", serde_json::json!({}),
9//!     |_context, args| Box::pin(async move { Ok(ToolOutput::json(args)) }));
10//! assert_eq!(tool.name(), "echo");
11//! # Ok::<(), rig_core::message::EmptyToolName>(())
12//! ```
13
14use std::{future::Future, sync::Arc};
15
16use serde::{Deserialize, Serialize};
17
18use crate::{
19    completion::{ToolAnnotations, ToolDefinition, message::ToolName},
20    serve::{ErasedHandler, adapters::ToolCallback},
21    wasm_compat::{WasmBoxedFuture, WasmCompatSend, WasmCompatSync},
22};
23
24use super::{IntoToolOutput, ToolContext, ToolExecutionError, ToolOutput, ToolResult, Touches};
25
26/// A typed LLM tool.
27///
28/// Tool authors provide metadata and exactly one execution method. Runtime
29/// context and host-only result metadata share the [`ToolContext`] path. Rig's
30/// object-safe dispatch boundary is private; use [`DynamicTool`] when the tool
31/// name or callback is only known at runtime.
32pub trait Tool: Sized + WasmCompatSend + WasmCompatSync {
33    /// Unique registration and provider-facing name.
34    const NAME: &'static str;
35    /// Typed JSON arguments.
36    type Args: for<'de> Deserialize<'de> + WasmCompatSend + WasmCompatSync;
37    /// Output convertible into Rig's canonical model presentation.
38    ///
39    /// Every owned serializable value implements [`IntoToolOutput`]
40    /// automatically. [`ToolResultContent`](crate::message::ToolResultContent)
41    /// and `Vec<ToolResultContent>` preserve rich content when returned
42    /// directly; use [`ToolOutput`] when constructing the presentation
43    /// explicitly.
44    type Output: IntoToolOutput;
45    /// Typed error returned by direct calls to this tool.
46    ///
47    /// Rig normalizes this error into [`ToolExecutionError`] only at the erased
48    /// dispatch boundary. This keeps ordinary `?` propagation and typed unit
49    /// tests available to tool authors without creating a second runtime error
50    /// representation.
51    type Error: std::error::Error + WasmCompatSend + WasmCompatSync + 'static;
52
53    /// Model-facing description.
54    fn description(&self) -> String;
55
56    /// JSON Schema for arguments.
57    fn parameters(&self) -> serde_json::Value;
58
59    /// What calling the tool does to the world, for the host's policies
60    /// (approval, plan mode, which calls run side by side). Nothing is
61    /// stated by default, which reads as the protocol's defaults
62    /// ([`ToolAnnotations`]).
63    fn annotations(&self) -> ToolAnnotations {
64        ToolAnnotations::default()
65    }
66
67    /// What a call with `args` may read and change, so a host runs the
68    /// calls of one reply that touch different things side by side. The
69    /// default reads it from the [annotations](Self::annotations): a
70    /// read-only tool reads anything, any other writes anything.
71    fn footprint(&self, args: &Self::Args) -> Touches {
72        let _ = args;
73        Touches::from_annotations(&self.annotations())
74    }
75
76    /// Normalize a typed author-facing error for runtime policy and telemetry.
77    ///
78    /// The default preserves concrete sources for operators and exposes safe
79    /// kind-level model feedback. An existing [`ToolExecutionError`] retains its
80    /// classification and model output. Override to supply deliberate domain feedback.
81    fn map_error(&self, error: Self::Error) -> ToolExecutionError {
82        ToolExecutionError::from_error(error)
83    }
84
85    /// Execute the tool.
86    fn call(
87        &self,
88        context: &mut ToolContext,
89        args: Self::Args,
90    ) -> impl Future<Output = Result<Self::Output, Self::Error>> + WasmCompatSend;
91}
92
93/// A tool that can be stored in a vector store and reconstructed for RAG.
94pub trait ToolEmbedding: Tool {
95    /// Error returned while reconstructing the tool.
96    type InitError: std::error::Error + WasmCompatSend + WasmCompatSync + 'static;
97    /// Serializable static context.
98    type Context: for<'de> Deserialize<'de> + Serialize;
99    /// Runtime initialization state.
100    type State: WasmCompatSend;
101
102    /// Documents used to retrieve the tool.
103    fn embedding_docs(&self) -> Vec<String>;
104    /// Serializable tool context.
105    fn context(&self) -> Self::Context;
106    /// Reconstruct the tool.
107    fn init(state: Self::State, context: Self::Context) -> Result<Self, Self::InitError>;
108}
109
110pub(crate) fn parse_tool_args<A>(args: &str) -> Result<A, ToolExecutionError>
111where
112    A: serde::de::DeserializeOwned,
113{
114    match serde_json::from_str(args) {
115        Ok(parsed) => Ok(parsed),
116        Err(original) if args.trim() == "null" => serde_json::from_str("{}").map_err(|_| {
117            ToolExecutionError::invalid_args(format!("failed to parse tool arguments: {original}"))
118                .with_source(original)
119        }),
120        Err(error) => Err(ToolExecutionError::invalid_args(format!(
121            "failed to parse tool arguments: {error}"
122        ))
123        .with_source(error)),
124    }
125}
126
127pub(crate) fn tool_result_from<O>(outcome: Result<O, ToolExecutionError>) -> ToolResult
128where
129    O: IntoToolOutput,
130{
131    match outcome.and_then(IntoToolOutput::into_tool_output) {
132        Ok(output) => ToolResult::success(output),
133        Err(error) => ToolResult::failed(error),
134    }
135}
136
137/// Reports whether a tool's owner still serves it. Registries check lazily on
138/// reads or reconciliation; calls before retirement may fail with a transport
139/// error rather than `HandlerUnavailable`.
140#[cfg(not(target_family = "wasm"))]
141pub type LivenessFn = Arc<dyn Fn() -> bool + Send + Sync>;
142/// A liveness probe (browser wasm: no `Send + Sync`, no threads).
143#[cfg(target_family = "wasm")]
144pub type LivenessFn = Arc<dyn Fn() -> bool>;
145
146/// A tool defined at runtime: its definition and a callback over JSON
147/// arguments. It is its own handler ([`Serve`](crate::serve::Serve)), keyed
148/// by its name. The optional liveness probe supports registry retirement;
149/// serving does not consult it.
150#[derive(Clone)]
151pub struct DynamicTool {
152    definition: ToolDefinition,
153    callback: Arc<dyn ToolCallback>,
154    liveness: Option<LivenessFn>,
155}
156
157impl DynamicTool {
158    /// Define a tool from a context-free callback over owned arguments.
159    pub fn new<F>(
160        name: ToolName,
161        description: impl Into<String>,
162        parameters: serde_json::Value,
163        callback: F,
164    ) -> Self
165    where
166        F: Fn(
167                serde_json::Value,
168            ) -> WasmBoxedFuture<'static, Result<ToolOutput, ToolExecutionError>>
169            + WasmCompatSend
170            + WasmCompatSync
171            + 'static,
172    {
173        Self::new_with_context(
174            name,
175            description,
176            parameters,
177            move |_context: &mut ToolContext, arguments| callback(arguments),
178        )
179    }
180
181    /// Define a tool from a callback over the dispatch-scoped context.
182    pub fn new_with_context<F>(
183        name: ToolName,
184        description: impl Into<String>,
185        parameters: serde_json::Value,
186        callback: F,
187    ) -> Self
188    where
189        F: ToolCallback + 'static,
190    {
191        Self {
192            definition: ToolDefinition::new(name, description, parameters),
193            callback: Arc::new(callback),
194            liveness: None,
195        }
196    }
197
198    /// The same tool, stating what calling it does to the world.
199    pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
200        self.definition.annotations = annotations;
201        self
202    }
203
204    /// Attach a liveness probe.
205    pub fn with_liveness<F>(mut self, is_live: F) -> Self
206    where
207        F: Fn() -> bool + WasmCompatSend + WasmCompatSync + 'static,
208    {
209        self.liveness = Some(Arc::new(is_live));
210        self
211    }
212
213    /// The tool's name.
214    pub fn name(&self) -> &ToolName {
215        &self.definition.name
216    }
217
218    /// The tool's definition.
219    pub fn definition(&self) -> ToolDefinition {
220        self.definition.clone()
221    }
222
223    /// Consumes the tool into its definition, its erased handler, and its
224    /// optional liveness probe.
225    pub fn into_parts(self) -> (ToolDefinition, ErasedHandler, Option<LivenessFn>) {
226        let Self {
227            definition,
228            callback,
229            liveness,
230        } = self;
231        let handler = ErasedHandler::new(Self {
232            definition: definition.clone(),
233            callback,
234            liveness: None,
235        });
236        (definition, handler, liveness)
237    }
238
239    /// Runs the tool on JSON `args` with an empty [`ToolContext`], as
240    /// [`Self::run_with`] does.
241    ///
242    /// ```
243    /// use rig_core::{message::ToolName, tool::{DynamicTool, ToolExecutionError, ToolOutput}};
244    /// use serde_json::json;
245    ///
246    /// let add = DynamicTool::new(ToolName::new("add")?, "Adds a and b", json!({}), |args| {
247    ///     Box::pin(async move {
248    ///         let int = |key| args.get(key).and_then(serde_json::Value::as_i64);
249    ///         let sum = int("a").zip(int("b")).map(|(a, b)| a + b);
250    ///         sum.map(|sum| ToolOutput::json(json!(sum)))
251    ///             .ok_or_else(|| ToolExecutionError::invalid_args("a and b must be integers"))
252    ///     })
253    /// });
254    /// let output = futures::executor::block_on(add.run(json!({ "a": 2, "b": 3 })))?;
255    /// assert_eq!(output.as_json(), Some(&json!(5)));
256    /// assert!(futures::executor::block_on(add.run(json!({ "a": "two" }))).is_err());
257    /// # Ok::<(), Box<dyn std::error::Error>>(())
258    /// ```
259    pub async fn run(&self, args: serde_json::Value) -> Result<ToolOutput, ToolExecutionError> {
260        self.run_with(&mut ToolContext::new(), args).await
261    }
262
263    /// Runs the tool on JSON `args` as a dispatch of its call runs it, and
264    /// returns its output or its error (invalid arguments included). The
265    /// tool sees `context`'s inbound values, and what it publishes with
266    /// `insert_result` lands in `context`. No registry or agent is involved.
267    ///
268    /// ```
269    /// use rig_core::{message::ToolName, tool::{ContextValue, DynamicTool, ToolContext, ToolOutput}};
270    /// use serde_json::json;
271    ///
272    /// #[derive(serde::Serialize, serde::Deserialize)]
273    /// struct User(String);
274    /// impl ContextValue for User { const KEY: &'static str = "user"; }
275    /// #[derive(serde::Serialize, serde::Deserialize)]
276    /// struct Greeted(bool);
277    /// impl ContextValue for Greeted { const KEY: &'static str = "greeted"; }
278    ///
279    /// let greet = DynamicTool::new_with_context(ToolName::new("greet")?, "Greets the user", json!({}),
280    ///     |context, _args| Box::pin(async move {
281    ///         let user = context.require::<User>()?;
282    ///         context.insert_result(Greeted(true))?;
283    ///         Ok(ToolOutput::text(format!("hello {}", user.0)))
284    ///     }));
285    /// let mut context = ToolContext::new();
286    /// context.insert(User("ada".into()))?;
287    /// let output = futures::executor::block_on(greet.run_with(&mut context, json!({})))?;
288    /// assert_eq!(output.as_text(), Some("hello ada"));
289    /// assert_eq!(context.result::<Greeted>()?.map(|greeted| greeted.0), Some(true));
290    /// # Ok::<(), Box<dyn std::error::Error>>(())
291    /// ```
292    pub async fn run_with(
293        &self,
294        context: &mut ToolContext,
295        args: serde_json::Value,
296    ) -> Result<ToolOutput, ToolExecutionError> {
297        crate::serve::adapters::run_direct(context, args, async |context, args| {
298            (self.callback)(context, args).await
299        })
300        .await
301    }
302
303    /// Whether the tool's owner still serves it (`true` without a probe).
304    pub fn is_live(&self) -> bool {
305        self.liveness.as_ref().is_none_or(|probe| probe())
306    }
307}
308
309impl crate::serve::Serve for DynamicTool {
310    type Family = crate::effect::family::Tool;
311
312    fn descriptor(&self) -> crate::effect::HandlerDescriptor {
313        let ToolDefinition {
314            name,
315            description,
316            parameters,
317            ..
318        } = &self.definition;
319        crate::effect::HandlerDescriptor::tool(name, description, parameters.clone())
320    }
321
322    fn key(&self) -> crate::effect::HandlerKey {
323        crate::effect::tool_key(&self.definition.name)
324    }
325
326    async fn serve(
327        &self,
328        kind: crate::effect::EffectKind,
329        dispatch: crate::serve::Dispatch,
330    ) -> crate::serve::Reply {
331        crate::serve::adapters::call_tool(kind, dispatch, None, async |context, args| {
332            (self.callback)(context, args).await
333        })
334        .await
335    }
336}
337
338impl std::fmt::Debug for DynamicTool {
339    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340        f.debug_struct("DynamicTool")
341            .field("name", &self.definition.name)
342            .finish_non_exhaustive()
343    }
344}
345
346/// A typed tool's [`Tool::NAME`] as a [`ToolName`].
347///
348/// An empty `NAME` fails the build where this is instantiated.
349///
350/// ```
351/// use rig_core::{message::ToolChoice, tool::{Tool, ToolContext, tool_name}};
352///
353/// struct Ping;
354///
355/// impl Tool for Ping {
356///     const NAME: &'static str = "ping";
357///     type Args = serde_json::Value;
358///     type Output = String;
359///     type Error = std::convert::Infallible;
360///
361///     fn description(&self) -> String {
362///         "Replies with pong.".to_string()
363///     }
364///
365///     fn parameters(&self) -> serde_json::Value {
366///         serde_json::json!({ "type": "object" })
367///     }
368///
369///     async fn call(
370///         &self,
371///         _context: &mut ToolContext,
372///         _args: Self::Args,
373///     ) -> Result<String, Self::Error> {
374///         Ok("pong".to_string())
375///     }
376/// }
377///
378/// let choice = ToolChoice::Specific { function_names: vec![tool_name::<Ping>()] };
379/// assert_eq!(tool_name::<Ping>(), "ping");
380/// # let _ = choice;
381/// ```
382pub fn tool_name<T: Tool>() -> ToolName {
383    const { assert!(!T::NAME.is_empty(), "Tool::NAME cannot be empty") };
384    ToolName::new_unchecked(T::NAME)
385}
386
387/// A tool's [`ToolDefinition`] from a typed tool, with its annotations.
388pub fn tool_definition<T: Tool>(tool: &T) -> ToolDefinition {
389    ToolDefinition::new(tool_name::<T>(), tool.description(), tool.parameters())
390        .with_annotations(tool.annotations())
391}