use std::time::Duration;
use tracing::info;
use super::metrics::MetricsCollector;
const SLO_TOOL_SUCCESS_RATE: f64 = 0.95;
const SLO_P95_LATENCY_SECS: f64 = 10.0;
#[derive(Debug, Clone)]
pub struct SessionSLO {
pub tool_success_rate: f64,
pub tool_success_met: bool,
pub agent_completed: bool,
pub p95_latency: Option<Duration>,
pub p95_latency_met: bool,
pub total_tool_calls: u64,
pub session_duration: Duration,
}
impl SessionSLO {
pub fn evaluate(metrics: &MetricsCollector, agent_completed: bool) -> Self {
let tool_success_rate = metrics.aggregate_success_rate();
let p95_latency = metrics.worst_case_tool_latency();
let p95_latency_met = match p95_latency {
Some(d) => d.as_secs_f64() < SLO_P95_LATENCY_SECS,
None => true,
};
Self {
tool_success_rate,
tool_success_met: tool_success_rate >= SLO_TOOL_SUCCESS_RATE,
agent_completed,
p95_latency,
p95_latency_met,
total_tool_calls: metrics.total_tool_calls(),
session_duration: metrics.session_duration(),
}
}
pub fn all_met(&self) -> bool {
self.tool_success_met && self.agent_completed && self.p95_latency_met
}
pub fn emit(&self) {
let p95_ms = self.p95_latency.map(|d| d.as_millis() as u64).unwrap_or(0);
info!(
event = "session_slo",
tool_success_rate = format!("{:.2}", self.tool_success_rate),
tool_success_met = self.tool_success_met,
agent_completed = self.agent_completed,
p95_latency_ms = p95_ms,
p95_latency_met = self.p95_latency_met,
all_met = self.all_met(),
total_tool_calls = self.total_tool_calls,
session_duration_secs = self.session_duration.as_secs(),
"Session SLO evaluation"
);
}
pub fn summary(&self) -> String {
let p95_str = match self.p95_latency {
Some(d) => format!("{:.1}s", d.as_secs_f64()),
None => "N/A".to_string(),
};
format!(
"SLOs: tool_success={:.0}% [{}] | completed={} | p95={} [{}]",
self.tool_success_rate * 100.0,
if self.tool_success_met { "OK" } else { "MISS" },
if self.agent_completed { "yes" } else { "no" },
p95_str,
if self.p95_latency_met { "OK" } else { "MISS" },
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_session_slo_all_met() {
let metrics = MetricsCollector::new();
for _ in 0..10 {
metrics.record_tool_call("shell", Duration::from_millis(100), true);
}
let slo = SessionSLO::evaluate(&metrics, true);
assert!(slo.all_met());
assert!(slo.tool_success_met);
assert!(slo.agent_completed);
assert!(slo.p95_latency_met);
assert_eq!(slo.total_tool_calls, 10);
}
#[test]
fn test_session_slo_tool_success_miss() {
let metrics = MetricsCollector::new();
metrics.record_tool_call("shell", Duration::from_millis(100), true);
metrics.record_tool_call("shell", Duration::from_millis(100), false);
let slo = SessionSLO::evaluate(&metrics, true);
assert!(!slo.tool_success_met);
assert!(!slo.all_met());
assert!((slo.tool_success_rate - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_session_slo_agent_not_completed() {
let metrics = MetricsCollector::new();
let slo = SessionSLO::evaluate(&metrics, false);
assert!(!slo.agent_completed);
assert!(!slo.all_met());
}
#[test]
fn test_session_slo_no_tools() {
let metrics = MetricsCollector::new();
let slo = SessionSLO::evaluate(&metrics, true);
assert!(slo.all_met());
assert_eq!(slo.total_tool_calls, 0);
assert!(slo.p95_latency.is_none());
}
#[test]
fn test_session_slo_summary_format() {
let metrics = MetricsCollector::new();
metrics.record_tool_call("shell", Duration::from_millis(200), true);
let slo = SessionSLO::evaluate(&metrics, true);
let summary = slo.summary();
assert!(summary.contains("tool_success="));
assert!(summary.contains("completed="));
assert!(summary.contains("p95="));
}
#[test]
fn test_session_slo_emit_does_not_panic() {
let metrics = MetricsCollector::new();
metrics.record_tool_call("shell", Duration::from_millis(100), true);
let slo = SessionSLO::evaluate(&metrics, true);
slo.emit();
}
}