Skip to main content

ic_rig/
tool.rs

1//! Tool trait and type-erased tool dispatch.
2//!
3//! A [`Tool`] is a typed async function the model can invoke. Define the schema
4//! via [`ToolDefinition`] and implement [`Tool::call`] with your logic.
5//!
6//! [`ToolSet`] holds a collection of type-erased tools keyed by name. The
7//! [`Agent`](crate::agent::Agent) uses it to dispatch model-requested tool calls.
8//!
9//! # Example
10//!
11//! ```rust
12//! use irig::tool::{Tool, ToolDefinition, ToolSet};
13//! use serde::Deserialize;
14//! use serde_json::json;
15//!
16//! #[derive(Deserialize)]
17//! struct AddArgs { x: f64, y: f64 }
18//!
19//! struct Adder;
20//!
21//! impl Tool for Adder {
22//!     const NAME: &'static str = "add";
23//!     type Error = std::convert::Infallible;
24//!     type Args  = AddArgs;
25//!     type Output = f64;
26//!
27//!     fn definition(&self) -> ToolDefinition {
28//!         ToolDefinition {
29//!             name: "add".into(),
30//!             description: "Add two numbers.".into(),
31//!             parameters: json!({
32//!                 "type": "object",
33//!                 "properties": {
34//!                     "x": { "type": "number" },
35//!                     "y": { "type": "number" }
36//!                 },
37//!                 "required": ["x", "y"]
38//!             }),
39//!         }
40//!     }
41//!
42//!     async fn call(&self, args: AddArgs) -> Result<f64, std::convert::Infallible> {
43//!         Ok(args.x + args.y)
44//!     }
45//! }
46//!
47//! let mut tools = ToolSet::new();
48//! tools.add(Adder);
49//! ```
50
51use crate::wasm_compat::BoxFuture;
52use serde::{Serialize, de::DeserializeOwned};
53use std::collections::HashMap;
54use thiserror::Error;
55
56// ── ToolDefinition ────────────────────────────────────────────────────────────
57
58/// Describes a tool to the LLM.
59///
60/// The `parameters` field must be a valid JSON Schema object describing the
61/// arguments the model should provide when calling this tool.
62#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
63pub struct ToolDefinition {
64    pub name: String,
65    pub description: String,
66    /// JSON Schema for the tool's arguments.
67    pub parameters: serde_json::Value,
68}
69
70// ── Tool trait ────────────────────────────────────────────────────────────────
71
72/// A typed, async tool that an [`Agent`](crate::agent::Agent) can call.
73///
74/// Implement this trait to create tools. Register them with a [`ToolSet`].
75pub trait Tool {
76    /// Must match `ToolDefinition::name` exactly.
77    const NAME: &'static str;
78
79    /// Error type returned if the tool fails.
80    type Error: std::error::Error + 'static;
81
82    /// Deserialised argument type. Must match the `parameters` schema.
83    type Args: DeserializeOwned;
84
85    /// Serialisable output type.
86    type Output: Serialize;
87
88    /// Schema + description sent to the model before each completion call.
89    fn definition(&self) -> ToolDefinition;
90
91    /// Execute the tool with the deserialised arguments.
92    fn call(&self, args: Self::Args) -> impl std::future::Future<Output = Result<Self::Output, Self::Error>>;
93}
94
95// ── ToolError ─────────────────────────────────────────────────────────────────
96
97/// Errors that can occur when dispatching a tool call.
98#[derive(Debug, Error)]
99pub enum ToolError {
100    /// The model requested a tool that is not registered.
101    #[error("Unknown tool: {0}")]
102    NotFound(String),
103
104    /// The model-supplied arguments could not be deserialised.
105    #[error("Failed to deserialise tool arguments: {0}")]
106    ArgsParse(#[from] serde_json::Error),
107
108    /// The tool itself returned an error.
109    #[error("Tool execution failed: {0}")]
110    Execution(String),
111}
112
113// ── Type-erased tool ──────────────────────────────────────────────────────────
114
115/// Object-safe wrapper around a concrete [`Tool`].
116///
117/// You don't need to use this directly — [`ToolSet::add`] handles it.
118trait ErasedTool {
119    fn definition(&self) -> ToolDefinition;
120
121    fn call<'a>(
122        &'a self,
123        args: serde_json::Value,
124    ) -> BoxFuture<'a, Result<serde_json::Value, ToolError>>;
125}
126
127/// Concrete wrapper that bridges `Tool` to `ErasedTool`.
128struct ToolWrapper<T>(T);
129
130impl<T: Tool + 'static> ErasedTool for ToolWrapper<T> {
131    fn definition(&self) -> ToolDefinition {
132        self.0.definition()
133    }
134
135    fn call<'a>(
136        &'a self,
137        raw: serde_json::Value,
138    ) -> BoxFuture<'a, Result<serde_json::Value, ToolError>> {
139        Box::pin(async move {
140            let args: T::Args =
141                serde_json::from_value(raw).map_err(ToolError::ArgsParse)?;
142            let output = self
143                .0
144                .call(args)
145                .await
146                .map_err(|e| ToolError::Execution(e.to_string()))?;
147            serde_json::to_value(output).map_err(ToolError::ArgsParse)
148        })
149    }
150}
151
152// ── ToolSet ───────────────────────────────────────────────────────────────────
153
154/// A collection of tools keyed by name.
155///
156/// Register tools with [`add`](ToolSet::add), then pass the set to
157/// [`AgentBuilder::tool`](crate::agent::AgentBuilder::tool).
158type BoxedTool = Box<dyn ErasedTool>;
159
160pub struct ToolSet {
161    tools: HashMap<String, BoxedTool>,
162}
163
164impl ToolSet {
165    pub fn new() -> Self {
166        Self { tools: HashMap::new() }
167    }
168
169    /// Register a tool. `T::NAME` is used as the lookup key.
170    pub fn add<T: Tool + 'static>(&mut self, tool: T) -> &mut Self {
171        self.tools.insert(T::NAME.to_owned(), Box::new(ToolWrapper(tool)));
172        self
173    }
174
175    /// Schema list to include in each [`CompletionRequest`](crate::completion::CompletionRequest).
176    pub fn definitions(&self) -> Vec<ToolDefinition> {
177        self.tools.values().map(|t| t.definition()).collect()
178    }
179
180    /// Dispatch a tool call by name.
181    pub async fn call(
182        &self,
183        name: &str,
184        args: serde_json::Value,
185    ) -> Result<serde_json::Value, ToolError> {
186        let tool = self.tools.get(name).ok_or_else(|| ToolError::NotFound(name.to_owned()))?;
187        tool.call(args).await
188    }
189
190    pub fn is_empty(&self) -> bool {
191        self.tools.is_empty()
192    }
193}
194
195impl Default for ToolSet {
196    fn default() -> Self {
197        Self::new()
198    }
199}