use super::AgentError;
use crate::metrics::AgentMetrics;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Default)]
pub struct BudgetConfig {
pub max_tool_calls: Option<usize>,
pub max_tokens: Option<usize>,
pub max_duration: Option<Duration>,
pub max_iterations: Option<usize>,
pub max_cost_usd: Option<f64>,
}
#[derive(Debug, Clone)]
pub enum BudgetExceeded {
ToolCalls {
limit: usize,
actual: usize,
},
Tokens {
limit: usize,
actual: usize,
},
Duration {
limit: Duration,
elapsed: Duration,
},
Iterations {
limit: usize,
},
Cost {
limit: f64,
actual: f64,
},
}
pub(crate) fn budget_iteration_gate(
budget: Option<&BudgetConfig>,
max_iterations: usize,
iteration: usize,
loop_start: Instant,
) -> Option<AgentError> {
let budget = budget?;
if let Some(limit) = budget.max_iterations {
let effective = limit.min(max_iterations);
if iteration >= effective {
return Some(AgentError::BudgetExceeded(BudgetExceeded::Iterations {
limit: effective,
}));
}
}
if let Some(limit) = budget.max_duration {
let elapsed = loop_start.elapsed();
if elapsed >= limit {
return Some(AgentError::BudgetExceeded(BudgetExceeded::Duration {
limit,
elapsed,
}));
}
}
None
}
pub(crate) fn budget_token_gate(
budget: Option<&BudgetConfig>,
metrics: &AgentMetrics,
) -> Option<AgentError> {
let budget = budget?;
let limit = budget.max_tokens?;
let actual = metrics.total_tokens.unwrap_or(0);
if actual >= limit {
return Some(AgentError::BudgetExceeded(BudgetExceeded::Tokens {
limit,
actual,
}));
}
None
}
pub(crate) fn budget_cost_gate(
budget: Option<&BudgetConfig>,
current_cost_usd: f64,
) -> Option<AgentError> {
let budget = budget?;
let limit = budget.max_cost_usd?;
if current_cost_usd >= limit {
return Some(AgentError::BudgetExceeded(BudgetExceeded::Cost {
limit,
actual: current_cost_usd,
}));
}
None
}
pub(crate) fn budget_tool_gate(
budget: Option<&BudgetConfig>,
metrics: &AgentMetrics,
loop_start: Instant,
) -> Option<AgentError> {
let budget = budget?;
if let Some(limit) = budget.max_tool_calls {
if metrics.tool_calls > limit {
return Some(AgentError::BudgetExceeded(BudgetExceeded::ToolCalls {
limit,
actual: metrics.tool_calls,
}));
}
}
if let Some(limit) = budget.max_duration {
let elapsed = loop_start.elapsed();
if elapsed >= limit {
return Some(AgentError::BudgetExceeded(BudgetExceeded::Duration {
limit,
elapsed,
}));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn cost_cfg(limit: f64) -> BudgetConfig {
BudgetConfig {
max_cost_usd: Some(limit),
..Default::default()
}
}
#[test]
fn cost_gate_without_budget_is_inert() {
assert!(budget_cost_gate(None, 9_999.0).is_none());
}
#[test]
fn cost_gate_without_limit_is_inert_even_with_budget() {
assert!(budget_cost_gate(Some(&BudgetConfig::default()), 9_999.0).is_none());
}
#[test]
fn cost_gate_below_limit_passes() {
assert!(budget_cost_gate(Some(&cost_cfg(1.0)), 0.99).is_none());
}
#[test]
fn cost_gate_at_and_above_limit_stops() {
match budget_cost_gate(Some(&cost_cfg(1.0)), 1.0) {
Some(AgentError::BudgetExceeded(BudgetExceeded::Cost { limit, actual })) => {
assert_eq!(limit, 1.0);
assert_eq!(actual, 1.0);
}
other => panic!("expected Cost stop at the limit, got {other:?}"),
}
assert!(budget_cost_gate(Some(&cost_cfg(1.0)), 1.5).is_some());
}
}