agent_code_lib/services/
budget.rs1#[derive(Debug, Clone, PartialEq)]
8pub enum BudgetDecision {
9 Continue,
11 ContinueWithWarning { percent_used: f64, message: String },
13 Stop { message: String },
15}
16
17#[derive(Debug, Clone)]
19pub struct BudgetConfig {
20 pub max_cost_usd: Option<f64>,
22 pub max_tokens: Option<u64>,
24 pub warning_threshold: f64,
26}
27
28impl Default for BudgetConfig {
29 fn default() -> Self {
30 Self {
31 max_cost_usd: None,
32 max_tokens: None,
33 warning_threshold: 0.8,
34 }
35 }
36}
37
38pub fn check_budget(
40 current_cost_usd: f64,
41 current_tokens: u64,
42 config: &BudgetConfig,
43) -> BudgetDecision {
44 if let Some(max_cost) = config.max_cost_usd
46 && max_cost > 0.0
47 {
48 let ratio = current_cost_usd / max_cost;
49 if ratio >= 1.0 {
50 return BudgetDecision::Stop {
51 message: format!(
52 "Cost budget exhausted: ${:.4} / ${:.4}",
53 current_cost_usd, max_cost
54 ),
55 };
56 }
57 if ratio >= config.warning_threshold {
58 return BudgetDecision::ContinueWithWarning {
59 percent_used: ratio * 100.0,
60 message: format!("Cost at {:.0}% of ${:.4} budget", ratio * 100.0, max_cost),
61 };
62 }
63 }
64
65 if let Some(max_tokens) = config.max_tokens
67 && max_tokens > 0
68 {
69 let ratio = current_tokens as f64 / max_tokens as f64;
70 if ratio >= 1.0 {
71 return BudgetDecision::Stop {
72 message: format!(
73 "Token budget exhausted: {} / {} tokens",
74 current_tokens, max_tokens
75 ),
76 };
77 }
78 if ratio >= config.warning_threshold {
79 return BudgetDecision::ContinueWithWarning {
80 percent_used: ratio * 100.0,
81 message: format!("Tokens at {:.0}% of {} budget", ratio * 100.0, max_tokens),
82 };
83 }
84 }
85
86 BudgetDecision::Continue
87}
88
89pub fn should_continue_turn(
94 turn_tokens: u64,
95 total_budget: Option<u64>,
96 consecutive_low_progress_turns: u32,
97) -> bool {
98 let Some(budget) = total_budget else {
99 return true; };
101
102 if budget == 0 {
103 return false;
104 }
105
106 if turn_tokens >= (budget as f64 * 0.9) as u64 {
108 return false;
109 }
110
111 if consecutive_low_progress_turns >= 3 {
113 return false;
114 }
115
116 true
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_within_budget() {
125 let config = BudgetConfig {
126 max_cost_usd: Some(1.0),
127 ..Default::default()
128 };
129 assert_eq!(check_budget(0.5, 0, &config), BudgetDecision::Continue);
130 }
131
132 #[test]
133 fn test_budget_warning() {
134 let config = BudgetConfig {
135 max_cost_usd: Some(1.0),
136 warning_threshold: 0.8,
137 ..Default::default()
138 };
139 match check_budget(0.85, 0, &config) {
140 BudgetDecision::ContinueWithWarning { .. } => {}
141 other => panic!("Expected warning, got: {other:?}"),
142 }
143 }
144
145 #[test]
146 fn test_budget_exhausted() {
147 let config = BudgetConfig {
148 max_cost_usd: Some(1.0),
149 ..Default::default()
150 };
151 match check_budget(1.5, 0, &config) {
152 BudgetDecision::Stop { .. } => {}
153 other => panic!("Expected stop, got: {other:?}"),
154 }
155 }
156
157 #[test]
158 fn test_continuation_logic() {
159 assert!(should_continue_turn(1000, Some(10000), 0));
160 assert!(!should_continue_turn(9500, Some(10000), 0));
161 assert!(!should_continue_turn(1000, Some(10000), 3));
162 }
163}