use super::{ToolDispatchContext, ToolDispatchResult, ToolMiddleware, ToolPipeline};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct TimeoutConfig {
pub timeout: Duration,
pub retry_on_timeout: bool,
pub max_retries: u32,
}
impl Default for TimeoutConfig {
fn default() -> Self {
Self {
timeout: Duration::from_secs(120),
retry_on_timeout: false,
max_retries: 0,
}
}
}
pub struct TimeoutMiddleware {
config: TimeoutConfig,
}
impl TimeoutMiddleware {
#[must_use]
pub fn new(config: TimeoutConfig) -> Self {
Self { config }
}
#[must_use]
pub fn from_secs(secs: u64) -> Self {
Self {
config: TimeoutConfig {
timeout: Duration::from_secs(secs),
..TimeoutConfig::default()
},
}
}
#[must_use]
pub fn none() -> Self {
Self {
config: TimeoutConfig {
timeout: Duration::MAX,
retry_on_timeout: false,
max_retries: 0,
},
}
}
}
impl ToolMiddleware for TimeoutMiddleware {
fn name(&self) -> &'static str {
"timeout"
}
fn dispatch<'a>(
&'a self,
ctx: &'a mut ToolDispatchContext,
next: &'a ToolPipeline,
) -> Pin<Box<dyn Future<Output = ToolDispatchResult> + Send + 'a>> {
let config = self.config.clone();
let tool_name = ctx.tool_name.clone();
let cancel = Arc::clone(&ctx.cancel);
Box::pin(async move {
let mut attempt = 0u32;
let mut current_timeout = config.timeout;
loop {
let result_future = next.dispatch(ctx);
let attempt_for_log = attempt;
tokio::select! {
result = tokio::time::timeout(current_timeout, result_future) => {
if let Ok(dispatch_result) = result {
return dispatch_result;
}
attempt = attempt.saturating_add(1);
if config.retry_on_timeout && attempt <= config.max_retries {
tracing::warn!(
tool = %tool_name,
attempt = attempt_for_log,
timeout_secs = current_timeout.as_secs(),
"tool execution timed out, retrying"
);
current_timeout = current_timeout.saturating_mul(2);
continue;
}
tracing::error!(
tool = %tool_name,
timeout_secs = current_timeout.as_secs(),
"tool execution timed out"
);
return ToolDispatchResult::err(
&tool_name,
format!(
"Tool '{}' timed out after {}s",
tool_name,
current_timeout.as_secs()
),
current_timeout,
);
}
() = cancel.notified() => {
return ToolDispatchResult::err(
&tool_name,
format!("Tool '{tool_name}' cancelled"),
Duration::ZERO,
);
}
}
}
})
}
}