1#![forbid(unsafe_code)]
19#![deny(missing_docs)]
20#![deny(unreachable_pub)]
21
22use std::sync::{Arc, RwLockReadGuard, RwLockWriteGuard};
23
24use async_trait::async_trait;
25use behest_context::ToolContext;
26use behest_core::tool_types::{ToolCall, ToolSpec};
27use serde::{Deserialize, Serialize};
28use serde_json::Value;
29
30mod strategy;
31pub use strategy::{ExecutionPlan, ToolExecutionStrategy};
32
33pub use behest_core::error::ToolError;
37
38pub type ToolResult<T = ToolOutput> = std::result::Result<T, ToolError>;
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ToolOutput {
44 pub value: Value,
46}
47
48impl ToolOutput {
49 #[must_use]
51 pub fn new(value: Value) -> Self {
52 Self { value }
53 }
54
55 #[must_use]
57 pub fn text(text: impl Into<String>) -> Self {
58 Self {
59 value: Value::String(text.into()),
60 }
61 }
62
63 #[must_use]
65 pub fn error(message: impl Into<String>) -> Self {
66 Self {
67 value: serde_json::json!({"error": message.into()}),
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, Default)]
76pub struct SideEffects {
77 pub read: bool,
79 pub write: bool,
81 pub external: bool,
83 pub destructive: bool,
85}
86
87impl SideEffects {
88 #[must_use]
90 pub const fn is_concurrency_safe(&self) -> bool {
91 !self.write && !self.destructive
92 }
93}
94
95#[async_trait]
100pub trait Tool: Send + Sync {
101 fn name(&self) -> &str;
103
104 fn description(&self) -> &str;
106
107 fn parameters_schema(&self) -> Value;
109
110 fn response_schema(&self) -> Option<Value> {
112 None
113 }
114
115 async fn execute(&self, args: Value) -> ToolResult<ToolOutput>;
117
118 async fn execute_with_ctx(
123 &self,
124 _ctx: &dyn ToolContext,
125 args: Value,
126 ) -> ToolResult<ToolOutput> {
127 self.execute(args).await
128 }
129
130 fn is_read_only(&self) -> bool {
132 false
133 }
134
135 fn is_concurrency_safe(&self) -> bool {
137 false
138 }
139
140 fn is_long_running(&self) -> bool {
142 false
143 }
144
145 fn side_effects(&self) -> SideEffects {
147 SideEffects::default()
148 }
149
150 fn requires_approval(&self) -> bool {
152 false
153 }
154
155 fn approval_reason(&self) -> Option<String> {
157 None
158 }
159
160 fn required_scopes(&self) -> &[&str] {
162 &[]
163 }
164
165 #[must_use]
167 fn to_spec(&self) -> ToolSpec {
168 ToolSpec::new(self.name(), self.description(), self.parameters_schema())
169 }
170}
171
172pub type ToolHandler = dyn Fn(Value) -> std::pin::Pin<Box<dyn std::future::Future<Output = ToolResult<Value>> + Send>>
178 + Send
179 + Sync;
180
181pub struct FunctionTool {
183 name: String,
184 description: String,
185 parameters_schema: Value,
186 read_only: bool,
187 concurrency_safe: bool,
188 long_running: bool,
189 side_effects: SideEffects,
190 approval_required: bool,
191 approval_reason: Option<String>,
192 handler: Box<ToolHandler>,
193}
194
195impl std::fmt::Debug for FunctionTool {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 f.debug_struct("FunctionTool")
198 .field("name", &self.name)
199 .field("read_only", &self.read_only)
200 .finish()
201 }
202}
203
204impl FunctionTool {
205 #[must_use]
210 pub fn new<F, Fut>(
211 name: impl Into<String>,
212 description: impl Into<String>,
213 parameters_schema: Value,
214 handler: F,
215 ) -> Self
216 where
217 F: Fn(Value) -> Fut + Send + Sync + 'static,
218 Fut: std::future::Future<Output = ToolResult<Value>> + Send + 'static,
219 {
220 Self {
221 name: name.into(),
222 description: description.into(),
223 parameters_schema,
224 read_only: false,
225 concurrency_safe: false,
226 long_running: false,
227 side_effects: SideEffects::default(),
228 approval_required: false,
229 approval_reason: None,
230 handler: Box::new(move |args| Box::pin(handler(args))),
231 }
232 }
233
234 #[must_use]
236 pub fn read_only(mut self) -> Self {
237 self.read_only = true;
238 self.concurrency_safe = true;
239 self.side_effects.read = true;
240 self
241 }
242
243 #[must_use]
245 pub fn concurrency_safe(mut self) -> Self {
246 self.concurrency_safe = true;
247 self
248 }
249
250 #[must_use]
252 pub fn long_running(mut self) -> Self {
253 self.long_running = true;
254 self
255 }
256
257 #[must_use]
259 pub fn side_effects(mut self, effects: SideEffects) -> Self {
260 self.side_effects = effects;
261 self
262 }
263
264 #[must_use]
266 pub fn requires_approval(mut self, reason: impl Into<String>) -> Self {
267 self.approval_required = true;
268 self.approval_reason = Some(reason.into());
269 self
270 }
271}
272
273#[async_trait]
274impl Tool for FunctionTool {
275 fn name(&self) -> &str {
276 &self.name
277 }
278
279 fn description(&self) -> &str {
280 &self.description
281 }
282
283 fn parameters_schema(&self) -> Value {
284 self.parameters_schema.clone()
285 }
286
287 async fn execute(&self, args: Value) -> ToolResult<ToolOutput> {
288 let value = (self.handler)(args).await?;
289 Ok(ToolOutput::new(value))
290 }
291
292 fn is_read_only(&self) -> bool {
293 self.read_only
294 }
295
296 fn is_concurrency_safe(&self) -> bool {
297 self.concurrency_safe
298 }
299
300 fn is_long_running(&self) -> bool {
301 self.long_running
302 }
303
304 fn side_effects(&self) -> SideEffects {
305 self.side_effects
306 }
307
308 fn requires_approval(&self) -> bool {
309 self.approval_required
310 }
311
312 fn approval_reason(&self) -> Option<String> {
313 self.approval_reason.clone()
314 }
315}
316
317pub struct ToolRegistry {
319 tools: std::sync::RwLock<std::collections::HashMap<String, Arc<dyn Tool>>>,
320}
321
322impl std::fmt::Debug for ToolRegistry {
323 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324 let names = self.names();
325 f.debug_struct("ToolRegistry")
326 .field("tools", &names)
327 .finish()
328 }
329}
330
331impl Default for ToolRegistry {
332 fn default() -> Self {
333 Self::new()
334 }
335}
336
337impl Clone for ToolRegistry {
338 fn clone(&self) -> Self {
339 let map = self.read_tools().clone();
342 Self {
343 tools: std::sync::RwLock::new(map),
344 }
345 }
346}
347
348impl ToolRegistry {
349 #[must_use]
351 pub fn new() -> Self {
352 Self {
353 tools: std::sync::RwLock::new(std::collections::HashMap::new()),
354 }
355 }
356
357 pub fn register<T: Tool + 'static>(&self, tool: T) -> Option<Arc<dyn Tool>> {
359 self.write_tools()
360 .insert(tool.name().to_string(), Arc::new(tool))
361 }
362
363 pub fn register_arc(&self, tool: Arc<dyn Tool>) -> Option<Arc<dyn Tool>> {
366 self.write_tools().insert(tool.name().to_string(), tool)
367 }
368
369 pub fn unregister(&self, name: &str) -> Option<Arc<dyn Tool>> {
371 self.write_tools().remove(name)
372 }
373
374 #[must_use]
376 pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
377 self.read_tools().get(name).cloned()
378 }
379
380 #[must_use]
382 pub fn names(&self) -> Vec<String> {
383 self.read_tools().keys().cloned().collect()
384 }
385
386 #[must_use]
388 pub fn len(&self) -> usize {
389 self.read_tools().len()
390 }
391
392 #[must_use]
394 pub fn is_empty(&self) -> bool {
395 self.read_tools().is_empty()
396 }
397
398 #[must_use]
400 pub fn specs(&self) -> Vec<ToolSpec> {
401 let mut specs: Vec<_> = self.read_tools().values().map(|t| t.to_spec()).collect();
402 specs.sort_by(|a, b| a.name.cmp(&b.name));
403 specs
404 }
405
406 pub async fn execute(&self, ctx: &dyn ToolContext, call: &ToolCall) -> ToolResult<ToolOutput> {
408 let tool = self.get(&call.name).ok_or_else(|| ToolError::NotFound {
409 name: call.name.clone(),
410 })?;
411 tool.execute_with_ctx(ctx, call.arguments.clone()).await
412 }
413
414 fn read_tools(&self) -> RwLockReadGuard<'_, std::collections::HashMap<String, Arc<dyn Tool>>> {
415 match self.tools.read() {
416 Ok(guard) => guard,
417 Err(poisoned) => poisoned.into_inner(),
418 }
419 }
420
421 fn write_tools(
422 &self,
423 ) -> RwLockWriteGuard<'_, std::collections::HashMap<String, Arc<dyn Tool>>> {
424 match self.tools.write() {
425 Ok(guard) => guard,
426 Err(poisoned) => poisoned.into_inner(),
427 }
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 fn make_tool(name: &str, read_only: bool) -> FunctionTool {
436 let n = name.to_string();
437 FunctionTool::new(
438 name,
439 format!("Tool {name}"),
440 serde_json::json!({"type": "object", "properties": {}}),
441 move |_args| {
442 let n = n.clone();
443 Box::pin(async move { Ok(serde_json::Value::String(format!("{n} done"))) })
444 },
445 )
446 .let_ro(read_only)
447 }
448
449 impl FunctionTool {
450 fn let_ro(mut self, read_only: bool) -> Self {
451 if read_only {
452 self.read_only = true;
453 self.concurrency_safe = true;
454 }
455 self
456 }
457 }
458
459 #[test]
460 fn tool_registry_register_and_get() {
461 let reg = ToolRegistry::new();
462 reg.register(make_tool("echo", true));
463 assert!(reg.get("echo").is_some());
464 assert!(reg.get("nonexistent").is_none());
465 assert_eq!(reg.len(), 1);
466 }
467
468 #[test]
469 fn tool_registry_unregister() {
470 let reg = ToolRegistry::new();
471 reg.register(make_tool("echo", true));
472 assert!(reg.unregister("echo").is_some());
473 assert!(reg.is_empty());
474 }
475
476 #[test]
477 fn tool_specs_sorted() {
478 let reg = ToolRegistry::new();
479 reg.register(make_tool("zulu", true));
480 reg.register(make_tool("alpha", true));
481 let specs = reg.specs();
482 assert_eq!(specs.len(), 2);
483 assert_eq!(specs[0].name, "alpha");
484 assert_eq!(specs[1].name, "zulu");
485 }
486
487 #[test]
488 fn tool_registry_recovers_from_poisoned_lock() {
489 let reg = Arc::new(ToolRegistry::new());
490 reg.register(make_tool("before", true));
491
492 let cloned = Arc::clone(®);
493 let _ = std::thread::spawn(move || {
494 let _guard = cloned.tools.write();
495 panic!("poison registry");
496 })
497 .join();
498
499 reg.register(make_tool("after", true));
500
501 assert_eq!(reg.len(), 2);
502 assert!(reg.get("before").is_some());
503 assert!(reg.get("after").is_some());
504 }
505
506 #[test]
507 fn side_effects_concurrency_safety() {
508 let read_only = SideEffects {
509 read: true,
510 ..Default::default()
511 };
512 assert!(read_only.is_concurrency_safe());
513
514 let write = SideEffects {
515 write: true,
516 ..Default::default()
517 };
518 assert!(!write.is_concurrency_safe());
519
520 let destructive = SideEffects {
521 destructive: true,
522 ..Default::default()
523 };
524 assert!(!destructive.is_concurrency_safe());
525 }
526}