use std::sync::{
Arc, OnceLock,
atomic::{AtomicBool, AtomicU64, Ordering},
};
use std::time::SystemTime;
use crate::runtime::error::RuntimeError;
const DEFAULT_PROVIDER_RETRY_BUDGET: usize = 5;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum EarlyEnd {
StopRequested,
TokenBudget,
}
#[derive(Clone, Default)]
pub struct CancellationToken {
cancelled: Arc<AtomicBool>,
}
impl std::fmt::Debug for CancellationToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CancellationToken")
.field("cancelled", &self.is_cancelled())
.finish()
}
}
pub type CancellationFlag = CancellationToken;
impl CancellationToken {
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::SeqCst);
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::SeqCst)
}
}
#[derive(Clone)]
pub struct RunOptions {
pub cancellation: Option<CancellationToken>,
pub stop: Option<CancellationToken>,
pub deadline: Option<SystemTime>,
pub retry_budget: usize,
pub tool_budget: Option<usize>,
pub model_budget: Option<usize>,
pub round_strategy: Option<Arc<dyn crate::agent::RoundStrategy>>,
pub token_budget: Option<u64>,
pub token_usage: Arc<AtomicU64>,
pub early_end: Arc<OnceLock<EarlyEnd>>,
}
impl Default for RunOptions {
fn default() -> Self {
Self {
cancellation: None,
stop: None,
deadline: None,
retry_budget: DEFAULT_PROVIDER_RETRY_BUDGET,
tool_budget: None,
model_budget: None,
round_strategy: None,
token_budget: None,
token_usage: Arc::new(AtomicU64::new(0)),
early_end: Arc::new(OnceLock::new()),
}
}
}
impl RunOptions {
pub fn with_round_strategy(mut self, strategy: Arc<dyn crate::agent::RoundStrategy>) -> Self {
self.round_strategy = Some(strategy);
self
}
pub fn child(&self) -> RunOptions {
RunOptions {
cancellation: self.cancellation.clone(),
stop: self.stop.clone(),
deadline: self.deadline,
token_budget: self.token_budget,
token_usage: Arc::clone(&self.token_usage),
..RunOptions::default()
}
}
pub fn reported_tokens(&self) -> u64 {
self.token_usage.load(Ordering::SeqCst)
}
pub(crate) fn record_tokens(&self, tokens: u64) {
self.token_usage.fetch_add(tokens, Ordering::SeqCst);
}
pub fn ended_early(&self) -> Option<EarlyEnd> {
self.early_end.get().copied()
}
pub(crate) fn record_early_end(&self, end: EarlyEnd) {
let _ = self.early_end.set(end);
}
pub(crate) fn token_budget_exceeded(&self) -> bool {
self.token_budget
.is_some_and(|budget| self.reported_tokens() >= budget)
}
pub(crate) fn check_limits(&self) -> Result<(), RuntimeError> {
if self
.cancellation
.as_ref()
.is_some_and(CancellationToken::is_cancelled)
{
return Err(RuntimeError::Cancelled);
}
if self
.deadline
.is_some_and(|deadline| SystemTime::now() >= deadline)
{
return Err(RuntimeError::DeadlineExceeded);
}
Ok(())
}
pub(crate) fn stop_requested(&self) -> bool {
self.stop
.as_ref()
.is_some_and(CancellationToken::is_cancelled)
}
pub(crate) fn tool_budget(&self) -> usize {
self.tool_budget.unwrap_or(usize::MAX)
}
pub(crate) fn model_budget(&self) -> usize {
self.model_budget.unwrap_or(usize::MAX)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_cancellation_token_shows_whether_it_was_tripped() {
let token = CancellationToken::default();
assert!(format!("{token:?}").contains("cancelled: false"));
token.cancel();
assert!(format!("{token:?}").contains("cancelled: true"));
}
#[test]
fn a_clone_of_a_runs_options_reads_what_that_run_recorded() {
let options = RunOptions {
token_budget: Some(100),
..RunOptions::default()
};
let held = options.clone();
options.record_early_end(EarlyEnd::TokenBudget);
assert_eq!(held.ended_early(), Some(EarlyEnd::TokenBudget));
}
#[test]
fn a_child_run_records_its_early_end_apart_from_its_parent() {
let parent = RunOptions {
token_budget: Some(100),
..RunOptions::default()
};
let child = parent.child();
child.record_early_end(EarlyEnd::TokenBudget);
assert_eq!(child.ended_early(), Some(EarlyEnd::TokenBudget));
assert_eq!(parent.ended_early(), None);
child.record_tokens(60);
assert_eq!(
parent.reported_tokens(),
60,
"what the two do share is the accounting, unchanged"
);
}
#[test]
fn the_first_early_end_recorded_is_the_one_that_stays() {
let options = RunOptions::default();
options.record_early_end(EarlyEnd::StopRequested);
options.record_early_end(EarlyEnd::TokenBudget);
assert_eq!(options.ended_early(), Some(EarlyEnd::StopRequested));
}
}