use chrono::{DateTime, Utc};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct CheckpointTriggerConfig {
pub agent_completion_interval: Option<usize>,
pub time_interval: Option<Duration>,
pub on_signal: bool,
pub on_phase_completion: bool,
}
impl Default for CheckpointTriggerConfig {
fn default() -> Self {
Self {
agent_completion_interval: Some(5), time_interval: Some(Duration::from_secs(30)), on_signal: true,
on_phase_completion: true,
}
}
}
impl CheckpointTriggerConfig {
pub fn none() -> Self {
Self {
agent_completion_interval: None,
time_interval: None,
on_signal: true, on_phase_completion: false,
}
}
pub fn item_interval(interval: usize) -> Self {
Self {
agent_completion_interval: Some(interval),
time_interval: None,
on_signal: true,
on_phase_completion: true,
}
}
pub fn time_interval(interval: Duration) -> Self {
Self {
agent_completion_interval: None,
time_interval: Some(interval),
on_signal: true,
on_phase_completion: true,
}
}
}
pub fn should_checkpoint(
items_since_last_checkpoint: usize,
last_checkpoint_time: DateTime<Utc>,
current_time: DateTime<Utc>,
config: &CheckpointTriggerConfig,
) -> bool {
should_checkpoint_by_items(items_since_last_checkpoint, config)
|| should_checkpoint_by_time(last_checkpoint_time, current_time, config)
}
fn should_checkpoint_by_items(
items_since_last_checkpoint: usize,
config: &CheckpointTriggerConfig,
) -> bool {
config
.agent_completion_interval
.map(|interval| items_since_last_checkpoint >= interval)
.unwrap_or(false)
}
fn should_checkpoint_by_time(
last_checkpoint_time: DateTime<Utc>,
current_time: DateTime<Utc>,
config: &CheckpointTriggerConfig,
) -> bool {
config.time_interval.is_some_and(|interval| {
let elapsed = current_time.signed_duration_since(last_checkpoint_time);
elapsed >= chrono::Duration::from_std(interval).unwrap_or(chrono::TimeDelta::MAX)
})
}
pub fn items_until_checkpoint(
items_since_last_checkpoint: usize,
config: &CheckpointTriggerConfig,
) -> Option<usize> {
config
.agent_completion_interval
.map(|interval| interval.saturating_sub(items_since_last_checkpoint))
}
pub fn time_until_checkpoint(
last_checkpoint_time: DateTime<Utc>,
current_time: DateTime<Utc>,
config: &CheckpointTriggerConfig,
) -> Option<Duration> {
config.time_interval.and_then(|interval| {
let elapsed = current_time.signed_duration_since(last_checkpoint_time);
let interval_chrono = chrono::Duration::from_std(interval).ok()?;
if elapsed >= interval_chrono {
Some(Duration::ZERO)
} else {
(interval_chrono - elapsed).to_std().ok()
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_checkpoint_item_interval() {
let config = CheckpointTriggerConfig {
agent_completion_interval: Some(5),
time_interval: None,
on_signal: true,
on_phase_completion: true,
};
let now = Utc::now();
let last_checkpoint = now - chrono::Duration::seconds(10);
assert!(!should_checkpoint(3, last_checkpoint, now, &config));
assert!(should_checkpoint(5, last_checkpoint, now, &config));
assert!(should_checkpoint(10, last_checkpoint, now, &config));
}
#[test]
fn test_should_checkpoint_time_interval() {
let config = CheckpointTriggerConfig {
agent_completion_interval: None,
time_interval: Some(Duration::from_secs(30)),
on_signal: true,
on_phase_completion: true,
};
let now = Utc::now();
let recent = now - chrono::Duration::seconds(20);
assert!(!should_checkpoint(100, recent, now, &config));
let old = now - chrono::Duration::seconds(35);
assert!(should_checkpoint(0, old, now, &config));
}
#[test]
fn test_should_checkpoint_either_trigger() {
let config = CheckpointTriggerConfig::default();
let now = Utc::now();
let recent = now - chrono::Duration::seconds(5);
assert!(should_checkpoint(5, recent, now, &config));
let old = now - chrono::Duration::seconds(35);
assert!(should_checkpoint(1, old, now, &config));
assert!(!should_checkpoint(2, recent, now, &config));
}
#[test]
fn test_items_until_checkpoint() {
let config = CheckpointTriggerConfig::item_interval(5);
assert_eq!(items_until_checkpoint(0, &config), Some(5));
assert_eq!(items_until_checkpoint(3, &config), Some(2));
assert_eq!(items_until_checkpoint(5, &config), Some(0));
assert_eq!(items_until_checkpoint(7, &config), Some(0));
}
#[test]
fn test_time_until_checkpoint() {
let config = CheckpointTriggerConfig::time_interval(Duration::from_secs(30));
let now = Utc::now();
let past = now - chrono::Duration::seconds(10);
let remaining = time_until_checkpoint(past, now, &config);
assert!(remaining.is_some());
let remaining = remaining.unwrap();
assert!(remaining.as_secs() >= 19 && remaining.as_secs() <= 21);
let old = now - chrono::Duration::seconds(40);
assert_eq!(
time_until_checkpoint(old, now, &config),
Some(Duration::ZERO)
);
}
#[test]
fn test_config_none_disables_triggers() {
let config = CheckpointTriggerConfig::none();
let now = Utc::now();
let old = now - chrono::Duration::hours(1);
assert!(!should_checkpoint(1000, old, now, &config));
}
}