use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use super::trust_tier::ModelTrustTier;
static GLOBAL: LazyLock<HeuristicTelemetry> = LazyLock::new(HeuristicTelemetry::default);
pub fn global() -> &'static HeuristicTelemetry {
&GLOBAL
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeuristicAction {
Enforced,
ShadowSkipped,
}
impl HeuristicAction {
pub fn as_str(self) -> &'static str {
match self {
HeuristicAction::Enforced => "enforced",
HeuristicAction::ShadowSkipped => "shadow_skipped",
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct HeuristicFireStats {
pub enforced: u64,
pub shadow_skipped: u64,
}
impl HeuristicFireStats {
#[allow(dead_code)]
pub fn total(self) -> u64 {
self.enforced + self.shadow_skipped
}
}
#[derive(Debug, Default)]
pub struct HeuristicTelemetry {
counters: Mutex<HashMap<(String, String), HeuristicFireStats>>,
}
impl HeuristicTelemetry {
pub fn record(
&self,
heuristic: &str,
model: &str,
tier: ModelTrustTier,
action: HeuristicAction,
) {
tracing::info!(
target: "heuristic_telemetry",
heuristic,
model,
tier = tier.as_str(),
action = action.as_str(),
"supervision heuristic fired"
);
let mut counters = match self.counters.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let stats = counters
.entry((heuristic.to_string(), model.to_string()))
.or_default();
match action {
HeuristicAction::Enforced => stats.enforced += 1,
HeuristicAction::ShadowSkipped => stats.shadow_skipped += 1,
}
}
#[allow(dead_code)]
pub fn stats_for(&self, heuristic: &str, model: &str) -> HeuristicFireStats {
let counters = match self.counters.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
counters
.get(&(heuristic.to_string(), model.to_string()))
.copied()
.unwrap_or_default()
}
#[allow(dead_code)]
pub fn snapshot(&self) -> Vec<(String, String, HeuristicFireStats)> {
let counters = match self.counters.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let mut entries: Vec<_> = counters
.iter()
.map(|((heuristic, model), stats)| (heuristic.clone(), model.clone(), *stats))
.collect();
entries.sort_by(|a, b| (&a.0, &a.1).cmp(&(&b.0, &b.1)));
entries
}
}
pub fn gate_action_for_tier(tier: ModelTrustTier) -> HeuristicAction {
match tier {
ModelTrustTier::Guided => HeuristicAction::Enforced,
ModelTrustTier::Autonomous => HeuristicAction::ShadowSkipped,
}
}
impl crate::agent::Agent {
pub(crate) fn supervision_gate_enforced(&self, heuristic: &'static str, model: &str) -> bool {
let tier = self.trust_tier_for_model(model);
let action = gate_action_for_tier(tier);
global().record(heuristic, model, tier, action);
matches!(action, HeuristicAction::Enforced)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_accumulates_per_heuristic_and_model() {
let t = HeuristicTelemetry::default();
t.record(
"uncertainty_gate",
"gemma-3-27b-it",
ModelTrustTier::Guided,
HeuristicAction::Enforced,
);
t.record(
"uncertainty_gate",
"gemma-3-27b-it",
ModelTrustTier::Guided,
HeuristicAction::Enforced,
);
t.record(
"uncertainty_gate",
"claude-opus-4-8",
ModelTrustTier::Autonomous,
HeuristicAction::ShadowSkipped,
);
let gemma = t.stats_for("uncertainty_gate", "gemma-3-27b-it");
assert_eq!(gemma.enforced, 2);
assert_eq!(gemma.shadow_skipped, 0);
let claude = t.stats_for("uncertainty_gate", "claude-opus-4-8");
assert_eq!(claude.enforced, 0);
assert_eq!(claude.shadow_skipped, 1);
assert_eq!(claude.total(), 1);
}
#[test]
fn stats_for_unknown_pair_is_zero() {
let t = HeuristicTelemetry::default();
assert_eq!(
t.stats_for("never_fired", "any-model"),
HeuristicFireStats::default()
);
}
#[test]
fn snapshot_is_sorted_and_complete() {
let t = HeuristicTelemetry::default();
t.record(
"b_gate",
"model-1",
ModelTrustTier::Guided,
HeuristicAction::Enforced,
);
t.record(
"a_gate",
"model-2",
ModelTrustTier::Autonomous,
HeuristicAction::ShadowSkipped,
);
let snap = t.snapshot();
assert_eq!(snap.len(), 2);
assert_eq!(snap[0].0, "a_gate");
assert_eq!(snap[1].0, "b_gate");
}
#[test]
fn gate_action_follows_tier() {
assert_eq!(
gate_action_for_tier(ModelTrustTier::Guided),
HeuristicAction::Enforced
);
assert_eq!(
gate_action_for_tier(ModelTrustTier::Autonomous),
HeuristicAction::ShadowSkipped
);
}
}