Skip to main content

rig_core/tool/
portable.rs

1//! Context-free tool authoring contracts.
2//!
3//! Portable tools receive owned, deserialized arguments only. Runtime identity,
4//! authorization, mutable context, capability state, and lifecycle metadata
5//! remain outside this module.
6
7use std::sync::Arc;
8
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    completion::ToolDefinition,
13    wasm_compat::{WasmBoxedFuture, WasmCompatSend, WasmCompatSync},
14};
15
16use super::{IntoToolOutput, ToolExecutionError, ToolOutput};
17
18/// A context-free typed tool that can be executed by any Rig runtime.
19pub trait PortableTool: Sized + WasmCompatSend + WasmCompatSync {
20    /// Unique registration and provider-facing name.
21    const NAME: &'static str;
22    /// Owned JSON arguments.
23    type Args: for<'de> Deserialize<'de> + WasmCompatSend + WasmCompatSync;
24    /// Canonical model-visible output.
25    type Output: IntoToolOutput + WasmCompatSend;
26    /// Concrete author-facing failure.
27    type Error: std::error::Error + WasmCompatSend + WasmCompatSync + 'static;
28
29    /// Model-facing description.
30    fn description(&self) -> String;
31
32    /// JSON Schema for arguments.
33    fn parameters(&self) -> serde_json::Value;
34
35    /// Normalize a concrete failure at the runtime effect boundary.
36    fn map_error(&self, error: Self::Error) -> ToolExecutionError {
37        ToolExecutionError::from_error(error)
38    }
39
40    /// Execute one owned invocation without runtime access.
41    fn call(
42        &self,
43        arguments: Self::Args,
44    ) -> impl Future<Output = Result<Self::Output, Self::Error>> + WasmCompatSend;
45}
46
47/// A portable tool that can be embedded and reconstructed for discovery.
48pub trait PortableToolEmbedding: PortableTool {
49    /// Failure returned while reconstructing the typed implementation.
50    type InitError: std::error::Error + WasmCompatSend + WasmCompatSync + 'static;
51    /// Serializable reconstruction data.
52    type Context: for<'de> Deserialize<'de> + Serialize;
53    /// Runtime initialization state supplied by the authoring integration.
54    type State: WasmCompatSend;
55
56    /// Documents used by a discovery implementation.
57    fn embedding_docs(&self) -> Vec<String>;
58    /// Serializable reconstruction data.
59    fn context(&self) -> Self::Context;
60    /// Reconstruct the typed implementation.
61    fn init(state: Self::State, context: Self::Context) -> Result<Self, Self::InitError>;
62}
63
64trait PortableDynamicCallback:
65    Fn(serde_json::Value) -> WasmBoxedFuture<'static, Result<ToolOutput, ToolExecutionError>>
66    + WasmCompatSend
67    + WasmCompatSync
68{
69}
70
71impl<F> PortableDynamicCallback for F where
72    F: Fn(serde_json::Value) -> WasmBoxedFuture<'static, Result<ToolOutput, ToolExecutionError>>
73        + WasmCompatSend
74        + WasmCompatSync
75{
76}
77
78/// A runtime-authored context-free tool implementation.
79#[derive(Clone)]
80pub struct PortableDynamicTool {
81    name: String,
82    description: String,
83    parameters: serde_json::Value,
84    callback: Arc<dyn PortableDynamicCallback>,
85}
86
87impl std::fmt::Debug for PortableDynamicTool {
88    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        formatter
90            .debug_struct("PortableDynamicTool")
91            .field("name", &self.name)
92            .field("description", &self.description)
93            .field("parameters", &self.parameters)
94            .finish_non_exhaustive()
95    }
96}
97
98impl PortableDynamicTool {
99    /// Create a context-free dynamic tool from an owned async callback.
100    pub fn new<F>(
101        name: impl Into<String>,
102        description: impl Into<String>,
103        parameters: serde_json::Value,
104        callback: F,
105    ) -> Self
106    where
107        F: Fn(
108                serde_json::Value,
109            ) -> WasmBoxedFuture<'static, Result<ToolOutput, ToolExecutionError>>
110            + WasmCompatSend
111            + WasmCompatSync
112            + 'static,
113    {
114        Self {
115            name: name.into(),
116            description: description.into(),
117            parameters,
118            callback: Arc::new(callback),
119        }
120    }
121
122    /// Provider-facing name.
123    pub fn name(&self) -> &str {
124        &self.name
125    }
126
127    /// Provider-facing definition.
128    pub fn definition(&self) -> ToolDefinition {
129        ToolDefinition {
130            name: self.name.clone(),
131            description: self.description.clone(),
132            parameters: self.parameters.clone(),
133        }
134    }
135
136    /// Execute the callback with owned arguments.
137    pub async fn execute(
138        &self,
139        arguments: serde_json::Value,
140    ) -> Result<ToolOutput, ToolExecutionError> {
141        (self.callback)(arguments).await
142    }
143}
144
145/// Generate provider-facing metadata for a portable typed tool.
146pub fn portable_tool_definition<T>(tool: &T) -> ToolDefinition
147where
148    T: PortableTool,
149{
150    ToolDefinition {
151        name: T::NAME.to_owned(),
152        description: tool.description(),
153        parameters: tool.parameters(),
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use std::convert::Infallible;
160
161    use serde::{Deserialize, Serialize};
162
163    use super::*;
164
165    #[derive(Deserialize)]
166    struct AddArgs {
167        left: i64,
168        right: i64,
169    }
170
171    #[derive(Serialize)]
172    struct Sum {
173        value: i64,
174    }
175
176    struct Add;
177
178    impl PortableTool for Add {
179        const NAME: &'static str = "add";
180        type Args = AddArgs;
181        type Output = Sum;
182        type Error = Infallible;
183
184        fn description(&self) -> String {
185            "Add two integers".to_string()
186        }
187
188        fn parameters(&self) -> serde_json::Value {
189            serde_json::json!({"type": "object"})
190        }
191
192        async fn call(&self, arguments: Self::Args) -> Result<Self::Output, Self::Error> {
193            Ok(Sum {
194                value: arguments.left + arguments.right,
195            })
196        }
197    }
198
199    #[tokio::test]
200    async fn portable_tools_execute_without_runtime_context() {
201        let output = Add.call(AddArgs { left: 2, right: 3 }).await;
202        let Ok(output) = output;
203        assert_eq!(output.value, 5);
204        assert_eq!(portable_tool_definition(&Add).name, "add");
205    }
206
207    #[tokio::test]
208    async fn portable_dynamic_tools_receive_owned_arguments() {
209        let tool = PortableDynamicTool::new(
210            "echo",
211            "Echo a JSON value",
212            serde_json::json!({"type": "object"}),
213            |arguments| Box::pin(async move { Ok(ToolOutput::json(arguments)) }),
214        );
215
216        let arguments = serde_json::json!({"value": "hello"});
217        let output = tool.execute(arguments.clone()).await;
218        assert!(output.is_ok());
219        let Ok(output) = output else {
220            return;
221        };
222        assert_eq!(output.as_json(), Some(&arguments));
223    }
224}