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 bob_core::types::ToolSource;
89
90 use super::*;
91
92 struct SleepyToolPort;
93
94 #[async_trait::async_trait]
95 impl ToolPort for SleepyToolPort {
96 async fn list_tools(&self) -> Result<Vec<ToolDescriptor>, ToolError> {
97 Ok(vec![ToolDescriptor {
98 id: "local/sleep".to_string(),
99 description: "sleep".to_string(),
100 input_schema: serde_json::json!({"type":"object"}),
101 source: ToolSource::Local,
102 }])
103 }
104
105 async fn call_tool(&self, call: ToolCall) -> Result<ToolResult, ToolError> {
106 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
107 Ok(ToolResult {
108 name: call.name,
109 output: serde_json::json!({"ok": true}),
110 is_error: false,
111 })
112 }
113 }
114
115 #[tokio::test]
116 async fn timeout_layer_times_out_slow_calls() {
117 let layer = TimeoutToolLayer::new(5);
118 let wrapped = layer.wrap(Arc::new(SleepyToolPort));
119 let result = wrapped
120 .call_tool(ToolCall {
121 name: "local/sleep".to_string(),
122 arguments: serde_json::json!({}),
123 })
124 .await;
125 assert!(matches!(result, Err(ToolError::Timeout { .. })));
126 }
127}