use std::sync::Arc;
use bob_core::{
error::ToolError,
ports::ToolPort,
types::{ToolCall, ToolDescriptor, ToolResult},
};
pub trait ToolLayer: Send + Sync {
fn wrap(&self, inner: Arc<dyn ToolPort>) -> Arc<dyn ToolPort>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoOpToolPort;
#[async_trait::async_trait]
impl ToolPort for NoOpToolPort {
async fn list_tools(&self) -> Result<Vec<ToolDescriptor>, ToolError> {
Ok(vec![])
}
async fn call_tool(&self, call: ToolCall) -> Result<ToolResult, ToolError> {
Err(ToolError::Execution(format!("no tool port configured, cannot call '{}'", call.name)))
}
}
#[derive(Debug, Clone, Copy)]
pub struct TimeoutToolLayer {
timeout_ms: u64,
}
impl TimeoutToolLayer {
#[must_use]
pub fn new(timeout_ms: u64) -> Self {
Self { timeout_ms }
}
}
impl ToolLayer for TimeoutToolLayer {
fn wrap(&self, inner: Arc<dyn ToolPort>) -> Arc<dyn ToolPort> {
Arc::new(TimeoutToolPort { inner, timeout_ms: self.timeout_ms })
}
}
struct TimeoutToolPort {
inner: Arc<dyn ToolPort>,
timeout_ms: u64,
}
impl std::fmt::Debug for TimeoutToolPort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TimeoutToolPort")
.field("timeout_ms", &self.timeout_ms)
.finish_non_exhaustive()
}
}
#[async_trait::async_trait]
impl ToolPort for TimeoutToolPort {
async fn list_tools(&self) -> Result<Vec<ToolDescriptor>, ToolError> {
self.inner.list_tools().await
}
async fn call_tool(&self, call: ToolCall) -> Result<ToolResult, ToolError> {
let tool_name = call.name.clone();
match tokio::time::timeout(
std::time::Duration::from_millis(self.timeout_ms),
self.inner.call_tool(call),
)
.await
{
Ok(result) => result,
Err(_) => Err(ToolError::Timeout { name: tool_name }),
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
struct SleepyToolPort;
#[async_trait::async_trait]
impl ToolPort for SleepyToolPort {
async fn list_tools(&self) -> Result<Vec<ToolDescriptor>, ToolError> {
Ok(vec![
ToolDescriptor::new("local/sleep", "sleep")
.with_input_schema(serde_json::json!({"type":"object"})),
])
}
async fn call_tool(&self, call: ToolCall) -> Result<ToolResult, ToolError> {
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
Ok(ToolResult {
name: call.name,
output: serde_json::json!({"ok": true}),
is_error: false,
})
}
}
#[tokio::test]
async fn timeout_layer_times_out_slow_calls() {
let layer = TimeoutToolLayer::new(5);
let wrapped = layer.wrap(Arc::new(SleepyToolPort));
let result = wrapped.call_tool(ToolCall::new("local/sleep", serde_json::json!({}))).await;
assert!(matches!(result, Err(ToolError::Timeout { .. })));
}
}