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::error::ToolError;
27use behest_core::tool_types::{ToolCall, ToolSpec};
28use serde::{Deserialize, Serialize};
29use serde_json::Value;
30
31mod strategy;
32pub use strategy::{ExecutionPlan, ToolExecutionStrategy};
33
34pub type ToolResult<T = ToolOutput> = std::result::Result<T, ToolError>;
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ToolOutput {
40 pub value: Value,
42}
43
44impl ToolOutput {
45 #[must_use]
47 pub fn new(value: Value) -> Self {
48 Self { value }
49 }
50
51 #[must_use]
53 pub fn text(text: impl Into<String>) -> Self {
54 Self {
55 value: Value::String(text.into()),
56 }
57 }
58
59 #[must_use]
61 pub fn error(message: impl Into<String>) -> Self {
62 Self {
63 value: serde_json::json!({"error": message.into()}),
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, Default)]
72pub struct SideEffects {
73 pub read: bool,
75 pub write: bool,
77 pub external: bool,
79 pub destructive: bool,
81}
82
83impl SideEffects {
84 #[must_use]
86 pub const fn is_concurrency_safe(&self) -> bool {
87 !self.write && !self.destructive
88 }
89}
90
91#[async_trait]
93pub trait Tool: Send + Sync {
94 fn name(&self) -> &str;
96
97 fn description(&self) -> &str;
99
100 fn parameters_schema(&self) -> Value;
102
103 fn response_schema(&self) -> Option<Value> {
105 None
106 }
107
108 async fn execute(&self, ctx: &dyn ToolContext, args: Value) -> ToolResult<ToolOutput>;
110
111 fn is_read_only(&self) -> bool {
113 false
114 }
115
116 fn is_concurrency_safe(&self) -> bool {
118 false
119 }
120
121 fn is_long_running(&self) -> bool {
123 false
124 }
125
126 fn side_effects(&self) -> SideEffects {
128 SideEffects::default()
129 }
130
131 fn requires_approval(&self) -> bool {
133 false
134 }
135
136 fn approval_reason(&self) -> Option<String> {
138 None
139 }
140
141 fn required_scopes(&self) -> &[&str] {
143 &[]
144 }
145
146 #[must_use]
148 fn to_spec(&self) -> ToolSpec {
149 ToolSpec::new(self.name(), self.description(), self.parameters_schema())
150 }
151}
152
153pub type ToolHandler = dyn Fn(Value) -> std::pin::Pin<Box<dyn std::future::Future<Output = ToolResult<ToolOutput>> + Send>>
155 + Send
156 + Sync;
157
158pub struct FunctionTool {
160 name: String,
161 description: String,
162 parameters_schema: Value,
163 read_only: bool,
164 concurrency_safe: bool,
165 long_running: bool,
166 side_effects: SideEffects,
167 approval_required: bool,
168 approval_reason: Option<String>,
169 handler: Box<ToolHandler>,
170}
171
172impl std::fmt::Debug for FunctionTool {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 f.debug_struct("FunctionTool")
175 .field("name", &self.name)
176 .field("read_only", &self.read_only)
177 .finish()
178 }
179}
180
181impl FunctionTool {
182 #[must_use]
184 pub fn new<F, Fut>(
185 name: impl Into<String>,
186 description: impl Into<String>,
187 parameters_schema: Value,
188 handler: F,
189 ) -> Self
190 where
191 F: Fn(Value) -> Fut + Send + Sync + 'static,
192 Fut: std::future::Future<Output = ToolResult<ToolOutput>> + Send + 'static,
193 {
194 Self {
195 name: name.into(),
196 description: description.into(),
197 parameters_schema,
198 read_only: false,
199 concurrency_safe: false,
200 long_running: false,
201 side_effects: SideEffects::default(),
202 approval_required: false,
203 approval_reason: None,
204 handler: Box::new(move |args| Box::pin(handler(args))),
205 }
206 }
207
208 #[must_use]
210 pub fn read_only(mut self) -> Self {
211 self.read_only = true;
212 self.concurrency_safe = true;
213 self.side_effects.read = true;
214 self
215 }
216
217 #[must_use]
219 pub fn concurrency_safe(mut self) -> Self {
220 self.concurrency_safe = true;
221 self
222 }
223
224 #[must_use]
226 pub fn long_running(mut self) -> Self {
227 self.long_running = true;
228 self
229 }
230
231 #[must_use]
233 pub fn side_effects(mut self, effects: SideEffects) -> Self {
234 self.side_effects = effects;
235 self
236 }
237
238 #[must_use]
240 pub fn requires_approval(mut self, reason: impl Into<String>) -> Self {
241 self.approval_required = true;
242 self.approval_reason = Some(reason.into());
243 self
244 }
245}
246
247#[async_trait]
248impl Tool for FunctionTool {
249 fn name(&self) -> &str {
250 &self.name
251 }
252
253 fn description(&self) -> &str {
254 &self.description
255 }
256
257 fn parameters_schema(&self) -> Value {
258 self.parameters_schema.clone()
259 }
260
261 async fn execute(&self, _ctx: &dyn ToolContext, args: Value) -> ToolResult<ToolOutput> {
262 (self.handler)(args).await
263 }
264
265 fn is_read_only(&self) -> bool {
266 self.read_only
267 }
268
269 fn is_concurrency_safe(&self) -> bool {
270 self.concurrency_safe
271 }
272
273 fn is_long_running(&self) -> bool {
274 self.long_running
275 }
276
277 fn side_effects(&self) -> SideEffects {
278 self.side_effects
279 }
280
281 fn requires_approval(&self) -> bool {
282 self.approval_required
283 }
284
285 fn approval_reason(&self) -> Option<String> {
286 self.approval_reason.clone()
287 }
288}
289
290pub struct ToolRegistry {
292 tools: std::sync::RwLock<std::collections::HashMap<String, Arc<dyn Tool>>>,
293}
294
295impl std::fmt::Debug for ToolRegistry {
296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297 let names = self.names();
298 f.debug_struct("ToolRegistry")
299 .field("tools", &names)
300 .finish()
301 }
302}
303
304impl Default for ToolRegistry {
305 fn default() -> Self {
306 Self::new()
307 }
308}
309
310impl ToolRegistry {
311 #[must_use]
313 pub fn new() -> Self {
314 Self {
315 tools: std::sync::RwLock::new(std::collections::HashMap::new()),
316 }
317 }
318
319 pub fn register<T: Tool + 'static>(&self, tool: T) -> Option<Arc<dyn Tool>> {
321 self.write_tools()
322 .insert(tool.name().to_string(), Arc::new(tool))
323 }
324
325 pub fn unregister(&self, name: &str) -> Option<Arc<dyn Tool>> {
327 self.write_tools().remove(name)
328 }
329
330 #[must_use]
332 pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
333 self.read_tools().get(name).cloned()
334 }
335
336 #[must_use]
338 pub fn names(&self) -> Vec<String> {
339 self.read_tools().keys().cloned().collect()
340 }
341
342 #[must_use]
344 pub fn len(&self) -> usize {
345 self.read_tools().len()
346 }
347
348 #[must_use]
350 pub fn is_empty(&self) -> bool {
351 self.read_tools().is_empty()
352 }
353
354 #[must_use]
356 pub fn specs(&self) -> Vec<ToolSpec> {
357 let mut specs: Vec<_> = self.read_tools().values().map(|t| t.to_spec()).collect();
358 specs.sort_by(|a, b| a.name.cmp(&b.name));
359 specs
360 }
361
362 pub async fn execute(&self, ctx: &dyn ToolContext, call: &ToolCall) -> ToolResult<ToolOutput> {
364 let tool = self.get(&call.name).ok_or_else(|| ToolError::NotFound {
365 name: call.name.clone(),
366 })?;
367 tool.execute(ctx, call.arguments.clone()).await
368 }
369
370 fn read_tools(&self) -> RwLockReadGuard<'_, std::collections::HashMap<String, Arc<dyn Tool>>> {
371 match self.tools.read() {
372 Ok(guard) => guard,
373 Err(poisoned) => poisoned.into_inner(),
374 }
375 }
376
377 fn write_tools(
378 &self,
379 ) -> RwLockWriteGuard<'_, std::collections::HashMap<String, Arc<dyn Tool>>> {
380 match self.tools.write() {
381 Ok(guard) => guard,
382 Err(poisoned) => poisoned.into_inner(),
383 }
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 fn make_tool(name: &str, read_only: bool) -> FunctionTool {
392 let n = name.to_string();
393 FunctionTool::new(
394 name,
395 format!("Tool {name}"),
396 serde_json::json!({"type": "object", "properties": {}}),
397 move |_args| {
398 let n = n.clone();
399 Box::pin(async move { Ok(ToolOutput::text(format!("{n} done"))) })
400 },
401 )
402 .let_ro(read_only)
403 }
404
405 impl FunctionTool {
406 fn let_ro(mut self, read_only: bool) -> Self {
407 if read_only {
408 self.read_only = true;
409 self.concurrency_safe = true;
410 }
411 self
412 }
413 }
414
415 #[test]
416 fn tool_registry_register_and_get() {
417 let reg = ToolRegistry::new();
418 reg.register(make_tool("echo", true));
419 assert!(reg.get("echo").is_some());
420 assert!(reg.get("nonexistent").is_none());
421 assert_eq!(reg.len(), 1);
422 }
423
424 #[test]
425 fn tool_registry_unregister() {
426 let reg = ToolRegistry::new();
427 reg.register(make_tool("echo", true));
428 assert!(reg.unregister("echo").is_some());
429 assert!(reg.is_empty());
430 }
431
432 #[test]
433 fn tool_specs_sorted() {
434 let reg = ToolRegistry::new();
435 reg.register(make_tool("zulu", true));
436 reg.register(make_tool("alpha", true));
437 let specs = reg.specs();
438 assert_eq!(specs.len(), 2);
439 assert_eq!(specs[0].name, "alpha");
440 assert_eq!(specs[1].name, "zulu");
441 }
442
443 #[test]
444 fn tool_registry_recovers_from_poisoned_lock() {
445 let reg = Arc::new(ToolRegistry::new());
446 reg.register(make_tool("before", true));
447
448 let cloned = Arc::clone(®);
449 let _ = std::thread::spawn(move || {
450 let _guard = cloned.tools.write();
451 panic!("poison registry");
452 })
453 .join();
454
455 reg.register(make_tool("after", true));
456
457 assert_eq!(reg.len(), 2);
458 assert!(reg.get("before").is_some());
459 assert!(reg.get("after").is_some());
460 }
461
462 #[test]
463 fn side_effects_concurrency_safety() {
464 let read_only = SideEffects {
465 read: true,
466 ..Default::default()
467 };
468 assert!(read_only.is_concurrency_safe());
469
470 let write = SideEffects {
471 write: true,
472 ..Default::default()
473 };
474 assert!(!write.is_concurrency_safe());
475
476 let destructive = SideEffects {
477 destructive: true,
478 ..Default::default()
479 };
480 assert!(!destructive.is_concurrency_safe());
481 }
482}