#[allow(clippy::struct_field_names)] #[derive(Debug, Default, Clone, Copy)]
pub(crate) struct TurnUsage {
pub(crate) input_tokens: u64,
pub(crate) output_tokens: u64,
pub(crate) cache_read_tokens: u64,
pub(crate) cache_write_tokens: u64,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct SessionUsageAccumulator {
pub(crate) total_input_tokens: u64,
pub(crate) total_output_tokens: u64,
pub(crate) total_cache_read_tokens: u64,
pub(crate) total_cache_write_tokens: u64,
pub(crate) last_cost_cents: f64,
pub(crate) last_context_window: u64,
}
impl SessionUsageAccumulator {
pub(crate) fn record(
&mut self,
input_tokens: u64,
output_tokens: u64,
cache_read_tokens: u64,
cache_write_tokens: u64,
cost_cents: f64,
context_window: u64,
) {
self.total_input_tokens = self.total_input_tokens.saturating_add(input_tokens);
self.total_output_tokens = self.total_output_tokens.saturating_add(output_tokens);
self.total_cache_read_tokens = self
.total_cache_read_tokens
.saturating_add(cache_read_tokens);
self.total_cache_write_tokens = self
.total_cache_write_tokens
.saturating_add(cache_write_tokens);
self.last_cost_cents = cost_cents;
self.last_context_window = context_window;
}
}
#[cfg(test)]
mod tests {
use super::{SessionUsageAccumulator, TurnUsage};
use crate::agent::build_prompt_response;
use agent_client_protocol as acp;
#[test]
fn session_accumulator_sums_tokens_and_overwrites_cost_and_context_window() {
let mut acc = SessionUsageAccumulator::default();
acc.record(100, 50, 10, 5, 1.5, 128_000);
acc.record(200, 80, 0, 0, 3.0, 64_000); assert_eq!(acc.total_input_tokens, 300);
assert_eq!(acc.total_output_tokens, 130);
assert_eq!(acc.total_cache_read_tokens, 10);
assert_eq!(acc.total_cache_write_tokens, 5);
assert!((acc.last_cost_cents - 3.0).abs() < f64::EPSILON);
assert_eq!(acc.last_context_window, 64_000);
}
#[test]
fn session_accumulator_default_is_zero() {
let acc = SessionUsageAccumulator::default();
assert_eq!(acc.total_input_tokens, 0);
assert!(acc.last_cost_cents.abs() < f64::EPSILON);
assert_eq!(acc.last_context_window, 0);
}
#[test]
fn build_prompt_response_attaches_usage() {
let turn_usage = TurnUsage {
input_tokens: 100,
output_tokens: 50,
cache_read_tokens: 10,
cache_write_tokens: 0,
};
let resp = build_prompt_response(acp::schema::v1::StopReason::EndTurn, turn_usage);
let u = resp.usage.expect("usage should be set");
assert_eq!(u.total_tokens, 150);
assert_eq!(u.input_tokens, 100);
assert_eq!(u.output_tokens, 50);
assert_eq!(u.cached_read_tokens, Some(10));
assert_eq!(u.cached_write_tokens, None);
assert_eq!(u.thought_tokens, None);
}
#[test]
fn build_prompt_response_zero_usage_still_attaches() {
let turn_usage = TurnUsage::default();
let resp = build_prompt_response(acp::schema::v1::StopReason::EndTurn, turn_usage);
let u = resp
.usage
.expect("usage should be set even for zero tokens");
assert_eq!(u.total_tokens, 0);
assert_eq!(u.cached_read_tokens, None);
assert_eq!(u.cached_write_tokens, None);
}
}