use std::sync::OnceLock;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy)]
pub struct Deadline {
start: Instant,
budget: Duration,
}
impl Deadline {
pub fn start(budget: Duration) -> Self {
Self {
start: Instant::now(),
budget,
}
}
#[cfg(test)]
pub fn from_start(start: Instant, budget: Duration) -> Self {
Self { start, budget }
}
pub fn budget(&self) -> Duration {
self.budget
}
pub fn remaining_at(&self, now: Instant) -> Duration {
self.budget
.saturating_sub(now.saturating_duration_since(self.start))
}
pub fn remaining(&self) -> Duration {
self.remaining_at(Instant::now())
}
pub fn is_expired(&self) -> bool {
self.remaining().is_zero()
}
}
#[derive(Debug, Clone, Copy)]
pub struct Timeouts {
pub stream_chunk: Duration,
pub tool_call_gap: Duration,
pub mcp_call: Duration,
pub mcp_init: Duration,
pub lsp_request: Duration,
pub lsp_initialize: Duration,
pub bash: Duration,
}
impl Timeouts {
pub const DEFAULT_STREAM_CHUNK_SECS: u64 = 300;
pub const DEFAULT_TOOL_CALL_GAP_SECS: u64 = 30;
pub const DEFAULT_MCP_CALL_SECS: u64 = 120;
pub const DEFAULT_MCP_INIT_SECS: u64 = 10;
pub const DEFAULT_LSP_REQUEST_SECS: u64 = 30;
pub const DEFAULT_LSP_INITIALIZE_SECS: u64 = 45;
pub const DEFAULT_BASH_SECS: u64 = 120;
pub const DEFAULT: Timeouts = Timeouts {
stream_chunk: Duration::from_secs(Self::DEFAULT_STREAM_CHUNK_SECS),
tool_call_gap: Duration::from_secs(Self::DEFAULT_TOOL_CALL_GAP_SECS),
mcp_call: Duration::from_secs(Self::DEFAULT_MCP_CALL_SECS),
mcp_init: Duration::from_secs(Self::DEFAULT_MCP_INIT_SECS),
lsp_request: Duration::from_secs(Self::DEFAULT_LSP_REQUEST_SECS),
lsp_initialize: Duration::from_secs(Self::DEFAULT_LSP_INITIALIZE_SECS),
bash: Duration::from_secs(Self::DEFAULT_BASH_SECS),
};
pub fn init(resolved: Timeouts) {
let _ = RESOLVED.set(resolved);
}
pub fn get() -> Timeouts {
RESOLVED.get().copied().unwrap_or(Self::DEFAULT)
}
}
static RESOLVED: OnceLock<Timeouts> = OnceLock::new();
impl Default for Timeouts {
fn default() -> Self {
Self::DEFAULT
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deadline_reports_remaining() {
let start = Instant::now();
let d = Deadline::from_start(start, Duration::from_secs(120));
assert_eq!(d.remaining_at(start), Duration::from_secs(120));
assert_eq!(
d.remaining_at(start + Duration::from_secs(50)),
Duration::from_secs(70),
);
assert_eq!(d.budget(), Duration::from_secs(120));
}
#[test]
fn deadline_saturates_at_zero_past_the_budget() {
let start = Instant::now();
let d = Deadline::from_start(start, Duration::from_secs(10));
assert_eq!(
d.remaining_at(start + Duration::from_secs(25)),
Duration::ZERO,
);
}
#[test]
fn zero_budget_is_immediately_expired() {
assert!(Deadline::start(Duration::ZERO).is_expired());
}
#[test]
fn fresh_budget_is_not_expired() {
assert!(!Deadline::start(Duration::from_secs(60)).is_expired());
}
#[test]
fn defaults_match_documented_values() {
let t = Timeouts::DEFAULT;
assert_eq!(t.stream_chunk, Duration::from_secs(300));
assert_eq!(t.tool_call_gap, Duration::from_secs(30));
assert_eq!(t.mcp_call, Duration::from_secs(120));
assert_eq!(t.mcp_init, Duration::from_secs(10));
assert_eq!(t.lsp_request, Duration::from_secs(30));
assert_eq!(t.lsp_initialize, Duration::from_secs(45));
assert_eq!(t.bash, Duration::from_secs(120));
assert_eq!(Timeouts::default().mcp_call, t.mcp_call);
}
}