Skip to main content

openclaw_node/agents/
tools.rs

1//! Tool registry and execution bindings.
2
3use napi::bindgen_prelude::*;
4use napi_derive::napi;
5use std::collections::HashMap;
6use std::sync::Arc;
7use tokio::sync::RwLock;
8
9use crate::error::OpenClawError;
10
11/// Result of a tool execution.
12#[napi(object)]
13#[derive(Debug, Clone)]
14pub struct JsToolResult {
15    /// Whether the tool executed successfully
16    pub success: bool,
17    /// Tool output content
18    pub content: String,
19    /// Error message (if success is false)
20    pub error: Option<String>,
21}
22
23impl JsToolResult {
24    /// Create a successful result.
25    pub fn success(content: impl Into<String>) -> Self {
26        Self {
27            success: true,
28            content: content.into(),
29            error: None,
30        }
31    }
32
33    /// Create an error result.
34    pub fn error(message: impl Into<String>) -> Self {
35        let msg = message.into();
36        Self {
37            success: false,
38            content: String::new(),
39            error: Some(msg),
40        }
41    }
42}
43
44/// A tool definition (for listing/inspection).
45#[napi(object)]
46#[derive(Debug, Clone)]
47pub struct JsToolDefinition {
48    /// Tool name
49    pub name: String,
50    /// Tool description
51    pub description: String,
52    /// JSON schema for input parameters
53    pub input_schema: serde_json::Value,
54}
55
56/// Internal tool storage.
57struct StoredTool {
58    name: String,
59    description: String,
60    input_schema: serde_json::Value,
61    // We store the threadsafe function for execution
62    execute_fn: Option<
63        napi::threadsafe_function::ThreadsafeFunction<
64            serde_json::Value,
65            napi::threadsafe_function::ErrorStrategy::Fatal,
66        >,
67    >,
68}
69
70/// Tool registry for managing and executing tools.
71///
72/// Tools can be registered from JavaScript and then executed by name.
73///
74/// ```javascript
75/// const registry = new ToolRegistry();
76///
77/// // Register a tool with a callback
78/// registry.registerCallback('echo', 'Echoes input', schema, async (params) => {
79///   return { success: true, content: params.message, error: null };
80/// });
81///
82/// // Execute the tool
83/// const result = await registry.execute('echo', { message: 'hello' });
84/// ```
85#[napi]
86pub struct ToolRegistry {
87    tools: Arc<RwLock<HashMap<String, StoredTool>>>,
88}
89
90#[napi]
91impl ToolRegistry {
92    /// Create a new empty tool registry.
93    #[napi(constructor)]
94    #[must_use]
95    pub fn new() -> Self {
96        Self {
97            tools: Arc::new(RwLock::new(HashMap::new())),
98        }
99    }
100
101    /// Register a tool with a JavaScript callback.
102    ///
103    /// # Arguments
104    ///
105    /// * `name` - Unique tool name
106    /// * `description` - Human-readable description
107    /// * `input_schema` - JSON schema for parameters
108    /// * `execute_fn` - Async function that takes params and returns `JsToolResult`
109    #[napi]
110    pub fn register_callback(
111        &self,
112        name: String,
113        description: String,
114        input_schema: serde_json::Value,
115        #[napi(ts_arg_type = "(params: any) => Promise<JsToolResult>")] execute_fn: JsFunction,
116    ) -> Result<()> {
117        use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction};
118
119        let tsfn: ThreadsafeFunction<serde_json::Value, ErrorStrategy::Fatal> =
120            execute_fn.create_threadsafe_function(0, |ctx| Ok(vec![ctx.value]))?;
121
122        let tool = StoredTool {
123            name: name.clone(),
124            description,
125            input_schema,
126            execute_fn: Some(tsfn),
127        };
128
129        // Use blocking to insert into the map
130        let tools = self.tools.clone();
131        std::thread::spawn(move || {
132            let rt = tokio::runtime::Runtime::new().unwrap();
133            rt.block_on(async {
134                let mut guard = tools.write().await;
135                guard.insert(name, tool);
136            });
137        })
138        .join()
139        .map_err(|_| OpenClawError::tool_error("Failed to register tool"))?;
140
141        Ok(())
142    }
143
144    /// Register a tool definition without a callback.
145    ///
146    /// Useful for documenting tools that will be executed externally.
147    #[napi]
148    pub async fn register_definition(
149        &self,
150        name: String,
151        description: String,
152        input_schema: serde_json::Value,
153    ) -> Result<()> {
154        let tool = StoredTool {
155            name: name.clone(),
156            description,
157            input_schema,
158            execute_fn: None,
159        };
160
161        let mut tools = self.tools.write().await;
162        tools.insert(name, tool);
163        Ok(())
164    }
165
166    /// Unregister a tool by name.
167    #[napi]
168    pub async fn unregister(&self, name: String) -> Result<bool> {
169        let mut tools = self.tools.write().await;
170        Ok(tools.remove(&name).is_some())
171    }
172
173    /// List all registered tool names.
174    #[napi]
175    pub async fn list(&self) -> Vec<String> {
176        let tools = self.tools.read().await;
177        tools.keys().cloned().collect()
178    }
179
180    /// Get a tool definition by name.
181    #[napi]
182    pub async fn get(&self, name: String) -> Option<JsToolDefinition> {
183        let tools = self.tools.read().await;
184        tools.get(&name).map(|t| JsToolDefinition {
185            name: t.name.clone(),
186            description: t.description.clone(),
187            input_schema: t.input_schema.clone(),
188        })
189    }
190
191    /// Get all tool definitions.
192    #[napi]
193    pub async fn get_all(&self) -> Vec<JsToolDefinition> {
194        let tools = self.tools.read().await;
195        tools
196            .values()
197            .map(|t| JsToolDefinition {
198                name: t.name.clone(),
199                description: t.description.clone(),
200                input_schema: t.input_schema.clone(),
201            })
202            .collect()
203    }
204
205    /// Check if a tool is registered.
206    #[napi]
207    pub async fn has(&self, name: String) -> bool {
208        let tools = self.tools.read().await;
209        tools.contains_key(&name)
210    }
211
212    /// Get the number of registered tools.
213    #[napi]
214    pub async fn count(&self) -> u32 {
215        let tools = self.tools.read().await;
216        tools.len() as u32
217    }
218
219    /// Clear all registered tools.
220    #[napi]
221    pub async fn clear(&self) {
222        let mut tools = self.tools.write().await;
223        tools.clear();
224    }
225}
226
227impl Default for ToolRegistry {
228    fn default() -> Self {
229        Self::new()
230    }
231}