use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::quota_tracker::QuotaTrackerSnapshot;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MaxingWindow {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MaxingStart {
Scheduled(MaxingWindow),
Manual,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TaskSource {
Skill,
Project,
Recurring,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProviderWindowRecord {
pub started: DateTime<Utc>,
pub ended_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProviderSessionRecord {
pub provider: String,
pub models_used: Vec<String>,
pub tasks_run: u64,
pub tokens_consumed: u64,
pub windows_drained: Vec<ProviderWindowRecord>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskRecord {
pub source: TaskSource,
pub source_name: String,
pub goal: String,
pub provider: String,
pub model: String,
pub success: bool,
pub tokens: u64,
pub duration_secs: f64,
pub summary: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
WindowEnded,
NoWork,
Cancelled,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SessionTotals {
pub tasks: u64,
pub tokens: u64,
pub providers_fully_drained: u64,
pub resets_observed: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenMaxingSession {
pub id: String,
pub started_at: DateTime<Utc>,
pub ended_at: Option<DateTime<Utc>>,
pub window: Option<MaxingWindow>,
pub manual: bool,
pub providers: Vec<ProviderSessionRecord>,
pub tasks: Vec<TaskRecord>,
pub totals: SessionTotals,
pub stop_reason: Option<StopReason>,
}
impl TokenMaxingSession {
pub fn start(window: Option<MaxingWindow>, manual: bool) -> Self {
let now = Utc::now();
let id = format!("tm-{}", now.timestamp_millis());
Self {
id,
started_at: now,
ended_at: None,
window,
manual,
providers: Vec::new(),
tasks: Vec::new(),
totals: SessionTotals::default(),
stop_reason: None,
}
}
pub fn within_window(&self) -> bool {
match &self.window {
Some(w) => Utc::now() < w.end,
None => true,
}
}
#[allow(clippy::too_many_arguments)]
pub fn record_task(
&mut self,
source: TaskSource,
source_name: String,
goal: String,
provider: String,
model: String,
success: bool,
tokens: u64,
duration_secs: f64,
summary: String,
) {
if !self.providers.iter().any(|r| r.provider == provider) {
self.providers.push(ProviderSessionRecord {
provider: provider.clone(),
..Default::default()
});
}
let rec = match self.providers.iter_mut().find(|r| r.provider == provider) {
Some(r) => r,
None => {
tracing::error!(
provider = %provider,
"token_maxing: provider record missing after ensure — skipping per-task rollup"
);
return;
}
};
rec.tasks_run += 1;
rec.tokens_consumed += tokens;
if !rec.models_used.contains(&model) {
rec.models_used.push(model.clone());
}
self.tasks.push(TaskRecord {
source,
source_name,
goal,
provider,
model,
success,
tokens,
duration_secs,
summary,
});
self.totals.tasks += 1;
self.totals.tokens += tokens;
}
pub fn finalize(&mut self, reason: StopReason) {
self.ended_at = Some(Utc::now());
self.stop_reason = Some(reason);
}
}
#[derive(Debug, Clone, Serialize)]
pub struct MaxerStatus {
pub running: bool,
pub current_session_id: Option<String>,
pub current_provider: Option<String>,
pub current_task: Option<String>,
pub manual: bool,
pub window: Option<MaxingWindow>,
pub tokens_this_session: u64,
pub tasks_this_session: u64,
pub providers: Vec<QuotaTrackerSnapshot>,
}