1use std::sync::Arc;
4
5use bob_core::{
6 error::ToolError,
7 ports::ToolPort,
8 types::{ToolCall, ToolDescriptor, ToolResult},
9};
10
11pub trait ToolLayer: Send + Sync {
13 fn wrap(&self, inner: Arc<dyn ToolPort>) -> Arc<dyn ToolPort>;
15}
16
17#[derive(Debug, Clone, Copy, Default)]
19pub struct NoOpToolPort;
20
21#[async_trait::async_trait]
22impl ToolPort for NoOpToolPort {
23 async fn list_tools(&self) -> Result<Vec<ToolDescriptor>, ToolError> {
24 Ok(vec![])
25 }
26
27 async fn call_tool(&self, call: ToolCall) -> Result<ToolResult, ToolError> {
28 Err(ToolError::Execution(format!("no tool port configured, cannot call '{}'", call.name)))
29 }
30}
31
32#[derive(Debug, Clone, Copy)]
34pub struct TimeoutToolLayer {
35 timeout_ms: u64,
36}
37
38impl TimeoutToolLayer {
39 #[must_use]
40 pub fn new(timeout_ms: u64) -> Self {
41 Self { timeout_ms }
42 }
43}
44
45impl ToolLayer for TimeoutToolLayer {
46 fn wrap(&self, inner: Arc<dyn ToolPort>) -> Arc<dyn ToolPort> {
47 Arc::new(TimeoutToolPort { inner, timeout_ms: self.timeout_ms })
48 }
49}
50
51struct TimeoutToolPort {
52 inner: Arc<dyn ToolPort>,
53 timeout_ms: u64,
54}
55
56impl std::fmt::Debug for TimeoutToolPort {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.debug_struct("TimeoutToolPort")
59 .field("timeout_ms", &self.timeout_ms)
60 .finish_non_exhaustive()
61 }
62}
63
64#[async_trait::async_trait]
65impl ToolPort for TimeoutToolPort {
66 async fn list_tools(&self) -> Result<Vec<ToolDescriptor>, ToolError> {
67 self.inner.list_tools().await
68 }
69
70 async fn call_tool(&self, call: ToolCall) -> Result<ToolResult, ToolError> {
71 let tool_name = call.name.clone();
72 match tokio::time::timeout(
73 std::time::Duration::from_millis(self.timeout_ms),
74 self.inner.call_tool(call),
75 )
76 .await
77 {
78 Ok(result) => result,
79 Err(_) => Err(ToolError::Timeout { name: tool_name }),
80 }
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use std::sync::Arc;
87
88 use super::*;
89
90 struct SleepyToolPort;
91
92 #[async_trait::async_trait]
93 impl ToolPort for SleepyToolPort {
94 async fn list_tools(&self) -> Result<Vec<ToolDescriptor>, ToolError> {
95 Ok(vec![
96 ToolDescriptor::new("local/sleep", "sleep")
97 .with_input_schema(serde_json::json!({"type":"object"})),
98 ])
99 }
100
101 async fn call_tool(&self, call: ToolCall) -> Result<ToolResult, ToolError> {
102 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
103 Ok(ToolResult {
104 name: call.name,
105 output: serde_json::json!({"ok": true}),
106 is_error: false,
107 })
108 }
109 }
110
111 #[tokio::test]
112 async fn timeout_layer_times_out_slow_calls() {
113 let layer = TimeoutToolLayer::new(5);
114 let wrapped = layer.wrap(Arc::new(SleepyToolPort));
115 let result = wrapped.call_tool(ToolCall::new("local/sleep", serde_json::json!({}))).await;
116 assert!(matches!(result, Err(ToolError::Timeout { .. })));
117 }
118}