use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
pub const EMA_ALPHA: f32 = 0.7;
pub const PRUNE_THRESHOLD: f32 = 0.1;
pub const NEUTRAL_PRIOR: f32 = 0.5;
pub const CAPABILITY_EMA_BETA: f32 = 0.8;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct AgentStats {
pub successes: u64,
pub failures: u64,
pub ema_success_rate: f32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub learned_vector: Vec<f32>,
}
impl AgentStats {
fn total(&self) -> u64 {
self.successes + self.failures
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct RoutingSnapshot {
#[serde(default)]
pub agents: HashMap<String, AgentStats>,
#[serde(default)]
pub edges: HashMap<String, HashMap<String, f32>>,
}
impl RoutingSnapshot {
pub fn success_prior(&self, agent: &str) -> f32 {
match self.agents.get(agent) {
Some(s) if s.total() > 0 => s.ema_success_rate,
_ => NEUTRAL_PRIOR,
}
}
pub fn learned_capability(&self, agent: &str) -> Option<&[f32]> {
self.agents
.get(agent)
.map(|s| s.learned_vector.as_slice())
.filter(|v| !v.is_empty())
}
pub fn edge_weight(&self, from: &str, to: &str) -> f32 {
self.edges
.get(from)
.and_then(|m| m.get(to))
.copied()
.unwrap_or(0.0)
}
pub fn best_forward(&self, agent: &str) -> Option<(String, f32)> {
self.edges
.get(agent)?
.iter()
.max_by(|a, b| a.1.total_cmp(b.1))
.map(|(to, w)| (to.clone(), *w))
}
}
fn ema(prev: f32, sample: f32, seeded: bool) -> f32 {
if seeded {
EMA_ALPHA * prev + (1.0 - EMA_ALPHA) * sample
} else {
sample
}
}
fn ema_vector(prev: &mut Vec<f32>, sample: &[f32], beta: f32) {
if prev.len() != sample.len() {
*prev = sample.to_vec();
} else {
for (p, s) in prev.iter_mut().zip(sample) {
*p = beta * *p + (1.0 - beta) * *s;
}
}
}
fn prune_edges_in_place(state: &mut RoutingSnapshot, theta: f32) -> usize {
let mut pruned = 0usize;
for inner in state.edges.values_mut() {
let before = inner.len();
inner.retain(|_, w| *w >= theta);
pruned += before - inner.len();
}
state.edges.retain(|_, inner| !inner.is_empty());
pruned
}
pub struct RoutingStore {
path: PathBuf,
lock: std::sync::Mutex<()>,
}
impl RoutingStore {
fn new(path: PathBuf) -> Self {
Self {
path,
lock: std::sync::Mutex::new(()),
}
}
pub fn user_default() -> Result<Self, String> {
if let Some(p) = std::env::var_os("CAR_ROUTING_PATH") {
return Ok(Self::new(PathBuf::from(p)));
}
let home = std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.ok_or("cannot resolve home directory (HOME/USERPROFILE unset)")?;
Ok(Self::new(
PathBuf::from(home).join(".car").join("routing.json"),
))
}
pub fn at(path: impl Into<PathBuf>) -> Self {
Self::new(path.into())
}
fn read(&self) -> RoutingSnapshot {
let contents = match std::fs::read_to_string(&self.path) {
Ok(c) => c,
Err(_) => return RoutingSnapshot::default(),
};
match serde_json::from_str(&contents) {
Ok(state) => state,
Err(e) => {
tracing::warn!(
path = %self.path.display(),
error = %e,
"routing.json failed to parse — resetting learned routing state to empty"
);
RoutingSnapshot::default()
}
}
}
fn write(&self, state: &RoutingSnapshot) -> Result<(), String> {
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("create {}: {e}", parent.display()))?;
}
let json = serde_json::to_string_pretty(state).map_err(|e| e.to_string())?;
let tmp = self.path.with_extension("json.tmp");
std::fs::write(&tmp, json).map_err(|e| format!("write {}: {e}", tmp.display()))?;
std::fs::rename(&tmp, &self.path)
.map_err(|e| format!("rename into {}: {e}", self.path.display()))
}
pub fn snapshot(&self) -> RoutingSnapshot {
self.read()
}
pub fn record_outcome(&self, agent: &str, ok: bool) -> Result<(), String> {
let _guard = self.lock.lock().unwrap_or_else(|e| e.into_inner());
let mut state = self.read();
let stats = state.agents.entry(agent.to_string()).or_default();
let seeded = stats.total() > 0;
stats.ema_success_rate = ema(stats.ema_success_rate, if ok { 1.0 } else { 0.0 }, seeded);
if ok {
stats.successes += 1;
} else {
stats.failures += 1;
}
self.write(&state)
}
pub fn record_capability(&self, agent: &str, task_emb: &[f32]) -> Result<(), String> {
if task_emb.is_empty() || task_emb.iter().any(|x| !x.is_finite()) {
return Ok(());
}
let _guard = self.lock.lock().unwrap_or_else(|e| e.into_inner());
let mut state = self.read();
let stats = state.agents.entry(agent.to_string()).or_default();
ema_vector(&mut stats.learned_vector, task_emb, CAPABILITY_EMA_BETA);
self.write(&state)
}
pub fn record_edge(&self, from: &str, to: &str, ok: bool) -> Result<(), String> {
let _guard = self.lock.lock().unwrap_or_else(|e| e.into_inner());
let mut state = self.read();
let w = state
.edges
.entry(from.to_string())
.or_default()
.entry(to.to_string())
.or_insert(0.0);
*w = EMA_ALPHA * *w + (1.0 - EMA_ALPHA) * if ok { 1.0 } else { 0.0 };
prune_edges_in_place(&mut state, PRUNE_THRESHOLD);
self.write(&state)
}
pub fn prune_below(&self, theta: f32) -> Result<usize, String> {
let _guard = self.lock.lock().unwrap_or_else(|e| e.into_inner());
let mut state = self.read();
let pruned = prune_edges_in_place(&mut state, theta);
if pruned > 0 {
self.write(&state)?;
}
Ok(pruned)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_store() -> (tempfile::TempDir, RoutingStore) {
let dir = tempfile::tempdir().unwrap();
let store = RoutingStore::at(dir.path().join("routing.json"));
(dir, store)
}
#[test]
fn no_history_yields_neutral_prior() {
let (_d, store) = temp_store();
assert_eq!(store.snapshot().success_prior("ghost"), NEUTRAL_PRIOR);
}
#[test]
fn first_outcome_seeds_ema_then_blends() {
let (_d, store) = temp_store();
store.record_outcome("a", true).unwrap();
let snap = store.snapshot();
assert_eq!(snap.success_prior("a"), 1.0);
assert_eq!(snap.agents["a"].successes, 1);
store.record_outcome("a", false).unwrap();
let snap = store.snapshot();
assert!((snap.success_prior("a") - 0.7).abs() < 1e-6);
assert_eq!(snap.agents["a"].failures, 1);
}
#[test]
fn weak_edge_auto_pruned_on_decay() {
let (_d, store) = temp_store();
store.record_edge("a", "b", true).unwrap();
assert!((store.snapshot().best_forward("a").unwrap().1 - 0.3).abs() < 1e-6);
store.record_edge("a", "c", true).unwrap(); store.record_edge("a", "c", false).unwrap(); store.record_edge("a", "c", false).unwrap(); store.record_edge("a", "c", false).unwrap(); store.record_edge("a", "c", false).unwrap(); let snap = store.snapshot();
assert_eq!(snap.edge_weight("a", "c"), 0.0); assert_eq!(snap.best_forward("a").unwrap().0, "b"); }
#[test]
fn prune_below_runs_as_maintenance_with_strict_theta() {
let (_d, store) = temp_store();
store.record_edge("a", "b", true).unwrap(); assert_eq!(store.prune_below(0.5).unwrap(), 1);
assert_eq!(store.snapshot().best_forward("a"), None);
}
#[test]
fn edge_weight_reads_directed_pair() {
let (_d, store) = temp_store();
store.record_edge("a", "b", true).unwrap();
let snap = store.snapshot();
assert!((snap.edge_weight("a", "b") - 0.3).abs() < 1e-6);
assert_eq!(snap.edge_weight("b", "a"), 0.0); assert_eq!(snap.edge_weight("a", "ghost"), 0.0);
}
#[test]
fn best_forward_picks_max_weight() {
let (_d, store) = temp_store();
store.record_edge("a", "weak", true).unwrap(); for _ in 0..3 {
store.record_edge("a", "strong", true).unwrap(); }
assert_eq!(store.snapshot().best_forward("a").unwrap().0, "strong");
}
#[test]
fn capability_seeds_then_ema_folds() {
let (_d, store) = temp_store();
assert_eq!(store.snapshot().learned_capability("a"), None);
store.record_capability("a", &[1.0, 0.0, 0.0]).unwrap();
assert_eq!(
store.snapshot().learned_capability("a"),
Some(&[1.0, 0.0, 0.0][..])
);
store.record_capability("a", &[0.0, 1.0, 0.0]).unwrap();
let v = store.snapshot();
let c = v.learned_capability("a").unwrap();
assert!((c[0] - 0.8).abs() < 1e-6);
assert!((c[1] - 0.2).abs() < 1e-6);
}
#[test]
fn capability_reseeds_on_dimension_change() {
let (_d, store) = temp_store();
store.record_capability("a", &[1.0, 2.0]).unwrap();
store.record_capability("a", &[9.0, 9.0, 9.0]).unwrap();
assert_eq!(
store.snapshot().learned_capability("a"),
Some(&[9.0, 9.0, 9.0][..])
);
}
#[test]
fn empty_capability_embedding_is_noop() {
let (_d, store) = temp_store();
store.record_capability("a", &[]).unwrap();
assert_eq!(store.snapshot().learned_capability("a"), None);
}
#[test]
fn non_finite_capability_is_refused_and_store_survives() {
let (_d, store) = temp_store();
store.record_capability("a", &[1.0, 2.0, 3.0]).unwrap();
store.record_outcome("a", true).unwrap();
store.record_capability("a", &[f32::NAN, 0.0, 0.0]).unwrap();
store
.record_capability("a", &[f32::INFINITY, 0.0, 0.0])
.unwrap();
let snap = store.snapshot();
assert_eq!(snap.learned_capability("a"), Some(&[1.0, 2.0, 3.0][..]));
assert_eq!(snap.agents["a"].successes, 1);
}
#[test]
fn corrupt_file_reads_as_empty() {
let (dir, store) = temp_store();
std::fs::write(dir.path().join("routing.json"), b"{ not json").unwrap();
assert!(store.snapshot().agents.is_empty());
}
}