// Budget: call-count and token-usage quotas for agent sessions.
//
// Modeled after Linux cgroup resource limits:
// - max_calls: CPU runtime quota (how many tool invocations allowed)
// - max_tokens: memory limit (how many tokens consumed)
// - window_seconds: time window for rate limiting (cgroup period)
///|
#derive(Eq, Debug)
pub struct Budget {
max_calls : Int64?
max_tokens : Int64?
window_seconds : Int64?
consumed_calls : Int64
consumed_tokens : Int64
window_start : Int64
}
///|
pub extend Budget with Eq::{not_equal, equal}
///|
pub extend Budget with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// Create a Budget with optional limits.
/// Pass None for any limit to disable it.
pub fn Budget::new(
max_calls~ : Int64?,
max_tokens~ : Int64?,
window_seconds~ : Int64?,
) -> Budget {
{
max_calls,
max_tokens,
window_seconds,
consumed_calls: 0L,
consumed_tokens: 0L,
window_start: 0L,
}
}
///|
/// Attempt to consume the given amount. Returns the updated Budget and whether allowed.
pub fn Budget::consume(
self : Budget,
calls : Int64,
tokens : Int64,
) -> (Budget, Bool) {
let fresh = self.consume_at(calls, tokens)
let over_calls = match fresh.max_calls {
None => false
Some(limit) => fresh.consumed_calls > limit
}
let over_tokens = match fresh.max_tokens {
None => false
Some(limit) => fresh.consumed_tokens > limit
}
(fresh, !(over_calls || over_tokens))
}
///|
/// Check how many calls remain (None means unlimited).
pub fn Budget::remaining_calls(self : Budget) -> Int64? {
match self.max_calls {
None => None
Some(m) => Some(m - self.consumed_calls)
}
}
///|
/// Check how many tokens remain (None means unlimited).
pub fn Budget::remaining_tokens(self : Budget) -> Int64? {
match self.max_tokens {
None => None
Some(m) => Some(m - self.consumed_tokens)
}
}
///|
/// Reset consumed counters to zero. Keeps the same limits.
pub fn Budget::reset(self : Budget) -> Budget {
{
max_calls: self.max_calls,
max_tokens: self.max_tokens,
window_seconds: self.window_seconds,
consumed_calls: 0L,
consumed_tokens: 0L,
window_start: 0L,
}
}
///|
/// Internal: apply consumption without checking limits.
fn Budget::consume_at(self : Budget, calls : Int64, tokens : Int64) -> Budget {
{
max_calls: self.max_calls,
max_tokens: self.max_tokens,
window_seconds: self.window_seconds,
consumed_calls: self.consumed_calls + calls,
consumed_tokens: self.consumed_tokens + tokens,
window_start: self.window_start,
}
}