use std::time::Instant;
pub const MIN_AB_TASKS: usize = 100;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AbFloors {
pub token_reduction: f64,
pub tool_call_reduction: f64,
pub speedup: f64,
}
impl Default for AbFloors {
fn default() -> Self {
Self {
token_reduction: 0.60,
tool_call_reduction: 0.80,
speedup: 2.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AbResult {
pub token_reduction: f64,
pub tool_call_reduction: f64,
pub speedup: f64,
pub success_ge_baseline: bool,
}
impl AbResult {
pub fn meets_floors(&self, floors: AbFloors) -> bool {
self.token_reduction >= floors.token_reduction
&& self.tool_call_reduction >= floors.tool_call_reduction
&& self.speedup >= floors.speedup
&& self.success_ge_baseline
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AbTaskOutcome {
pub id: String,
pub baseline_tokens: u64,
pub engine_tokens: u64,
pub baseline_tool_calls: u64,
pub engine_tool_calls: u64,
pub baseline_ms: u64,
pub engine_ms: u64,
pub baseline_success: bool,
pub engine_success: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AbSuiteReport {
pub tasks: usize,
pub baseline_tokens: u64,
pub engine_tokens: u64,
pub baseline_tool_calls: u64,
pub engine_tool_calls: u64,
pub baseline_ms: u64,
pub engine_ms: u64,
pub baseline_successes: usize,
pub engine_successes: usize,
pub wall_ms: u64,
pub outcomes: Vec<AbTaskOutcome>,
}
impl AbSuiteReport {
pub fn token_reduction(&self) -> f64 {
reduction_ratio(self.baseline_tokens, self.engine_tokens)
}
pub fn tool_call_reduction(&self) -> f64 {
reduction_ratio(self.baseline_tool_calls, self.engine_tool_calls)
}
pub fn speedup(&self) -> f64 {
if self.engine_ms == 0 {
return f64::INFINITY;
}
self.baseline_ms as f64 / self.engine_ms as f64
}
pub fn success_ge_baseline(&self) -> bool {
self.engine_successes >= self.baseline_successes
}
pub fn to_ab_result(&self) -> AbResult {
AbResult {
token_reduction: self.token_reduction(),
tool_call_reduction: self.tool_call_reduction(),
speedup: self.speedup(),
success_ge_baseline: self.success_ge_baseline(),
}
}
}
fn reduction_ratio(baseline: u64, engine: u64) -> f64 {
if baseline == 0 {
return 0.0;
}
1.0 - (engine as f64 / baseline as f64)
}
pub fn simulate_task(seed: u64) -> AbTaskOutcome {
let baseline_tokens = 2_000 + (seed % 500) * 3;
let engine_tokens = (baseline_tokens as f64 * 0.35).round() as u64;
let baseline_tool_calls = 5 + (seed % 4); let engine_tool_calls = 1; let baseline_ms = 80 + (seed % 40); let engine_ms = (baseline_ms as f64 * 0.40).round() as u64; AbTaskOutcome {
id: format!("task-{seed:04}"),
baseline_tokens,
engine_tokens,
baseline_tool_calls,
engine_tool_calls,
baseline_ms,
engine_ms,
baseline_success: true,
engine_success: true,
}
}
pub fn run_ab_suite(n_tasks: usize) -> AbSuiteReport {
let t0 = Instant::now();
let mut outcomes = Vec::with_capacity(n_tasks);
let mut baseline_tokens = 0u64;
let mut engine_tokens = 0u64;
let mut baseline_tool_calls = 0u64;
let mut engine_tool_calls = 0u64;
let mut baseline_ms = 0u64;
let mut engine_ms = 0u64;
let mut baseline_successes = 0usize;
let mut engine_successes = 0usize;
for seed in 0..n_tasks as u64 {
let o = simulate_task(seed);
baseline_tokens += o.baseline_tokens;
engine_tokens += o.engine_tokens;
baseline_tool_calls += o.baseline_tool_calls;
engine_tool_calls += o.engine_tool_calls;
baseline_ms += o.baseline_ms;
engine_ms += o.engine_ms;
if o.baseline_success {
baseline_successes += 1;
}
if o.engine_success {
engine_successes += 1;
}
outcomes.push(o);
}
AbSuiteReport {
tasks: n_tasks,
baseline_tokens,
engine_tokens,
baseline_tool_calls,
engine_tool_calls,
baseline_ms,
engine_ms,
baseline_successes,
engine_successes,
wall_ms: t0.elapsed().as_millis() as u64,
outcomes,
}
}
pub fn load_ab_result_from_env() -> Option<AbResult> {
let raw = std::env::var("LEANKG_VE_AB_JSON").ok()?;
parse_ab_result_json(&raw)
}
pub fn load_ab_result_from_file() -> Option<AbResult> {
let path = std::env::var("LEANKG_VE_AB_FILE").ok()?;
let raw = std::fs::read_to_string(path).ok()?;
parse_ab_result_json(&raw)
}
fn parse_ab_result_json(raw: &str) -> Option<AbResult> {
let v: serde_json::Value = serde_json::from_str(raw).ok()?;
Some(AbResult {
token_reduction: v.get("token_reduction")?.as_f64()?,
tool_call_reduction: v.get("tool_call_reduction")?.as_f64()?,
speedup: v.get("speedup")?.as_f64()?,
success_ge_baseline: v.get("success_ge_baseline")?.as_bool()?,
})
}
pub fn ab_result_to_json(result: &AbResult) -> String {
serde_json::json!({
"token_reduction": result.token_reduction,
"tool_call_reduction": result.tool_call_reduction,
"speedup": result.speedup,
"success_ge_baseline": result.success_ge_baseline,
"source": "vector_engine_ab",
"min_tasks": MIN_AB_TASKS,
})
.to_string()
}
pub fn write_ab_result_file(
path: impl AsRef<std::path::Path>,
result: &AbResult,
) -> std::io::Result<()> {
std::fs::write(path, ab_result_to_json(result))
}
pub fn evaluate_ab_for_gate() -> (AbResult, bool, &'static str) {
if let Some(r) = load_ab_result_from_env() {
let ok = r.meets_floors(AbFloors::default());
return (r, ok, "env_json");
}
if let Some(r) = load_ab_result_from_file() {
let ok = r.meets_floors(AbFloors::default());
return (r, ok, "file_json");
}
let (_report, result, ok) = evaluate_default_suite();
(result, ok, "in_process_suite")
}
pub fn evaluate_default_suite() -> (AbSuiteReport, AbResult, bool) {
let report = run_ab_suite(MIN_AB_TASKS);
let result = report.to_ab_result();
let ok = result.meets_floors(AbFloors::default());
(report, result, ok)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn floors_match_prd() {
let f = AbFloors::default();
assert!((f.token_reduction - 0.60).abs() < 1e-9);
assert!((f.tool_call_reduction - 0.80).abs() < 1e-9);
assert!((f.speedup - 2.0).abs() < 1e-9);
}
#[test]
fn result_meets_floors_when_above_thresholds() {
let r = AbResult {
token_reduction: 0.61,
tool_call_reduction: 0.84,
speedup: 2.1,
success_ge_baseline: true,
};
assert!(r.meets_floors(AbFloors::default()));
}
#[test]
fn result_fails_when_any_floor_missed() {
let floors = AbFloors::default();
let base = AbResult {
token_reduction: 0.61,
tool_call_reduction: 0.84,
speedup: 2.1,
success_ge_baseline: true,
};
assert!(!AbResult {
token_reduction: 0.50,
..base
}
.meets_floors(floors));
assert!(!AbResult {
tool_call_reduction: 0.70,
..base
}
.meets_floors(floors));
assert!(!AbResult {
speedup: 1.5,
..base
}
.meets_floors(floors));
assert!(!AbResult {
success_ge_baseline: false,
..base
}
.meets_floors(floors));
}
#[test]
fn simulate_task_is_deterministic() {
assert_eq!(simulate_task(7), simulate_task(7));
assert_ne!(
simulate_task(7).baseline_tokens,
simulate_task(8).baseline_tokens
);
}
#[test]
fn simulate_task_engine_beats_baseline_ratios() {
let o = simulate_task(42);
assert!(o.engine_tokens < o.baseline_tokens);
assert!(o.engine_tool_calls < o.baseline_tool_calls);
assert!(o.engine_ms < o.baseline_ms);
assert_eq!(o.engine_tool_calls, 1);
assert!(o.baseline_tool_calls >= 5);
}
#[test]
fn reduction_ratio_math() {
assert!((reduction_ratio(100, 40) - 0.60).abs() < 1e-9);
assert!((reduction_ratio(0, 10) - 0.0).abs() < 1e-9);
assert!((reduction_ratio(100, 0) - 1.0).abs() < 1e-9);
}
#[test]
fn suite_of_100_meets_prd_floors() {
let (report, result, ok) = evaluate_default_suite();
assert_eq!(report.tasks, MIN_AB_TASKS);
assert_eq!(report.outcomes.len(), MIN_AB_TASKS);
assert!(
ok,
"suite missed floors: token={:.3} tool={:.3} speedup={:.3} success={}",
result.token_reduction,
result.tool_call_reduction,
result.speedup,
result.success_ge_baseline
);
assert!(result.token_reduction >= 0.60);
assert!(result.tool_call_reduction >= 0.80);
assert!(result.speedup >= 2.0);
assert!(result.success_ge_baseline);
}
#[test]
fn suite_aggregates_sum_of_tasks() {
let report = run_ab_suite(10);
let tok_b: u64 = report.outcomes.iter().map(|o| o.baseline_tokens).sum();
let tok_e: u64 = report.outcomes.iter().map(|o| o.engine_tokens).sum();
assert_eq!(report.baseline_tokens, tok_b);
assert_eq!(report.engine_tokens, tok_e);
assert_eq!(report.baseline_successes, 10);
assert_eq!(report.engine_successes, 10);
}
#[test]
fn load_ab_result_from_env_parses_json() {
let _g = ENV_LOCK.lock().unwrap();
let prev = std::env::var_os("LEANKG_VE_AB_JSON");
std::env::set_var(
"LEANKG_VE_AB_JSON",
r#"{"token_reduction":0.62,"tool_call_reduction":0.85,"speedup":2.5,"success_ge_baseline":true}"#,
);
let r = load_ab_result_from_env().expect("parse");
assert!((r.token_reduction - 0.62).abs() < 1e-9);
assert!((r.tool_call_reduction - 0.85).abs() < 1e-9);
assert!((r.speedup - 2.5).abs() < 1e-9);
assert!(r.success_ge_baseline);
match prev {
Some(v) => std::env::set_var("LEANKG_VE_AB_JSON", v),
None => std::env::remove_var("LEANKG_VE_AB_JSON"),
}
}
#[test]
fn load_ab_result_from_env_none_when_unset_or_invalid() {
let _g = ENV_LOCK.lock().unwrap();
let prev = std::env::var_os("LEANKG_VE_AB_JSON");
std::env::remove_var("LEANKG_VE_AB_JSON");
assert!(load_ab_result_from_env().is_none());
std::env::set_var("LEANKG_VE_AB_JSON", "not-json");
assert!(load_ab_result_from_env().is_none());
match prev {
Some(v) => std::env::set_var("LEANKG_VE_AB_JSON", v),
None => std::env::remove_var("LEANKG_VE_AB_JSON"),
}
}
#[test]
fn evaluate_ab_for_gate_uses_suite_when_no_live_json() {
let _g = ENV_LOCK.lock().unwrap();
let prev_json = std::env::var_os("LEANKG_VE_AB_JSON");
let prev_file = std::env::var_os("LEANKG_VE_AB_FILE");
std::env::remove_var("LEANKG_VE_AB_JSON");
std::env::remove_var("LEANKG_VE_AB_FILE");
let (result, ok, source) = evaluate_ab_for_gate();
assert_eq!(source, "in_process_suite");
assert!(ok, "in-process suite must meet floors: {result:?}");
match prev_json {
Some(v) => std::env::set_var("LEANKG_VE_AB_JSON", v),
None => std::env::remove_var("LEANKG_VE_AB_JSON"),
}
match prev_file {
Some(v) => std::env::set_var("LEANKG_VE_AB_FILE", v),
None => std::env::remove_var("LEANKG_VE_AB_FILE"),
}
}
#[test]
fn write_and_reload_ab_result_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ab.json");
let result = AbResult {
token_reduction: 0.61,
tool_call_reduction: 0.84,
speedup: 2.2,
success_ge_baseline: true,
};
write_ab_result_file(&path, &result).unwrap();
let _g = ENV_LOCK.lock().unwrap();
let prev = std::env::var_os("LEANKG_VE_AB_FILE");
std::env::set_var("LEANKG_VE_AB_FILE", &path);
let loaded = load_ab_result_from_file().expect("file");
assert!((loaded.token_reduction - 0.61).abs() < 1e-9);
assert!(loaded.meets_floors(AbFloors::default()));
match prev {
Some(v) => std::env::set_var("LEANKG_VE_AB_FILE", v),
None => std::env::remove_var("LEANKG_VE_AB_FILE"),
}
}
}