use kimetsu_core::KimetsuResult;
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
pub const FEATURE_COUNT: usize = 7;
pub const LEGACY_MIN_SCORE: f32 = 0.45;
pub const LEGACY_LOOP_MIN_SCORE: f32 = 0.35;
pub const POLICY_RECALL_FLOOR: f32 = 0.20;
pub const MIN_TRAINING_EXAMPLES: usize = 40;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Features {
pub score: f32,
pub loop_mode: f32,
pub is_failure_pattern: f32,
pub novelty: f32,
pub repeat_count: f32,
pub recovery: f32,
pub evidence: f32,
}
impl Features {
pub fn to_vec(self) -> [f32; FEATURE_COUNT] {
[
self.score,
self.loop_mode,
self.is_failure_pattern,
self.novelty,
self.repeat_count,
self.recovery,
self.evidence,
]
}
pub const NAMES: [&'static str; FEATURE_COUNT] = [
"score",
"loop_mode",
"is_failure_pattern",
"novelty",
"repeat_count",
"recovery",
"evidence",
];
pub fn from_slice(v: &[f32]) -> Option<Self> {
if v.len() != FEATURE_COUNT {
return None;
}
Some(Self {
score: v[0],
loop_mode: v[1],
is_failure_pattern: v[2],
novelty: v[3],
repeat_count: v[4],
recovery: v[5],
evidence: v[6],
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Policy {
pub weights: Vec<f32>,
pub bias: f32,
pub trained_on: usize,
pub trained_at: Option<String>,
}
impl Policy {
pub fn prior() -> Self {
const W_SCORE: f32 = 20.0;
let mut weights = vec![0.0; FEATURE_COUNT];
weights[0] = W_SCORE;
weights[1] = 0.10 * W_SCORE;
Self {
weights,
bias: -LEGACY_MIN_SCORE * W_SCORE,
trained_on: 0,
trained_at: None,
}
}
pub fn is_prior(&self) -> bool {
self.trained_on == 0
}
pub fn is_valid(&self) -> bool {
self.weights.len() == FEATURE_COUNT
&& self.bias.is_finite()
&& self.weights.iter().all(|w| w.is_finite())
}
pub fn probability(&self, features: &Features) -> f32 {
let z: f32 = self
.weights
.iter()
.zip(features.to_vec())
.map(|(w, x)| w * x)
.sum::<f32>()
+ self.bias;
sigmoid(z)
}
pub fn should_inject(&self, features: &Features) -> bool {
self.probability(features) >= 0.5
}
}
impl Default for Policy {
fn default() -> Self {
Self::prior()
}
}
fn sigmoid(z: f32) -> f32 {
if z >= 0.0 {
1.0 / (1.0 + (-z).exp())
} else {
let e = z.exp();
e / (1.0 + e)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Example {
pub features: Features,
pub useful: bool,
}
pub fn fit(examples: &[Example]) -> Policy {
const EPOCHS: usize = 400;
const LEARNING_RATE: f32 = 0.05;
const L2: f32 = 0.01;
if examples.len() < MIN_TRAINING_EXAMPLES {
return Policy::prior();
}
let positives = examples.iter().filter(|e| e.useful).count();
if positives == 0 || positives == examples.len() {
return Policy::prior();
}
let prior = Policy::prior();
let mut weights = prior.weights.clone();
let mut bias = prior.bias;
let n = examples.len() as f32;
for _ in 0..EPOCHS {
let mut grad_w = [0.0f32; FEATURE_COUNT];
let mut grad_b = 0.0f32;
for example in examples {
let x = example.features.to_vec();
let z: f32 = weights.iter().zip(x).map(|(w, xi)| w * xi).sum::<f32>() + bias;
let error = sigmoid(z) - if example.useful { 1.0 } else { 0.0 };
for (g, xi) in grad_w.iter_mut().zip(x) {
*g += error * xi;
}
grad_b += error;
}
for (i, g) in grad_w.iter().enumerate() {
let pull = L2 * (weights[i] - prior.weights[i]);
weights[i] -= LEARNING_RATE * (g / n + pull);
}
bias -= LEARNING_RATE * (grad_b / n + L2 * (bias - prior.bias));
}
let fitted = Policy {
weights,
bias,
trained_on: examples.len(),
trained_at: None,
};
if fitted.is_valid() { fitted } else { prior }
}
pub fn accuracy(policy: &Policy, examples: &[Example]) -> f32 {
if examples.is_empty() {
return 0.0;
}
let correct = examples
.iter()
.filter(|e| policy.should_inject(&e.features) == e.useful)
.count();
correct as f32 / examples.len() as f32
}
pub const INJECTED_EVENT: &str = "proactive.injected";
pub fn policy_path(kimetsu_dir: &std::path::Path) -> std::path::PathBuf {
kimetsu_dir.join("inject-policy.json")
}
pub fn load(kimetsu_dir: &std::path::Path) -> Policy {
std::fs::read_to_string(policy_path(kimetsu_dir))
.ok()
.and_then(|text| serde_json::from_str::<Policy>(&text).ok())
.filter(Policy::is_valid)
.unwrap_or_else(Policy::prior)
}
pub fn save(kimetsu_dir: &std::path::Path, policy: &Policy) -> KimetsuResult<()> {
let path = policy_path(kimetsu_dir);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let tmp = path.with_extension("json.tmp");
std::fs::write(&tmp, serde_json::to_string_pretty(policy)?)?;
std::fs::rename(&tmp, &path)?;
Ok(())
}
pub fn reset(kimetsu_dir: &std::path::Path) -> KimetsuResult<()> {
let path = policy_path(kimetsu_dir);
if path.exists() {
std::fs::remove_file(&path)?;
}
Ok(())
}
pub fn record_injection(
start: &std::path::Path,
memory_id: &str,
features: &Features,
injected: bool,
surface: Surface,
) {
let payload = serde_json::json!({
"memory_id": memory_id,
"features": features.to_vec().to_vec(),
"injected": injected,
"surface": surface.as_str(),
});
let _ = crate::feedback::log_telemetry_event(start, INJECTED_EVENT, payload);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Surface {
PreToolCommand,
PreToolPrefetch,
PostTool,
}
impl Surface {
pub fn as_str(self) -> &'static str {
match self {
Surface::PreToolCommand => "pretool_command",
Surface::PreToolPrefetch => "pretool_prefetch",
Surface::PostTool => "posttool",
}
}
fn from_str(s: &str) -> Option<Self> {
match s {
"pretool_command" => Some(Surface::PreToolCommand),
"pretool_prefetch" => Some(Surface::PreToolPrefetch),
"posttool" => Some(Surface::PostTool),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SurfaceStats {
pub surface: &'static str,
pub injected: usize,
pub cited: usize,
}
impl SurfaceStats {
pub fn acceptance(&self) -> f32 {
if self.injected == 0 {
return 0.0;
}
self.cited as f32 / self.injected as f32
}
}
pub fn surface_acceptance(conn: &Connection) -> KimetsuResult<Vec<SurfaceStats>> {
let mut stmt = conn.prepare(
"SELECT e.payload_json, e.ts
FROM events AS e
WHERE e.kind = ?1
ORDER BY e.ts",
)?;
let rows = stmt
.query_map(rusqlite::params![INJECTED_EVENT], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?
.collect::<Result<Vec<_>, _>>()?;
let mut tally: Vec<SurfaceStats> = [
Surface::PreToolCommand,
Surface::PreToolPrefetch,
Surface::PostTool,
]
.iter()
.map(|s| SurfaceStats {
surface: s.as_str(),
injected: 0,
cited: 0,
})
.collect();
for (payload_json, ts) in rows {
let Ok(payload) = serde_json::from_str::<serde_json::Value>(&payload_json) else {
continue;
};
if payload.get("injected").and_then(serde_json::Value::as_bool) != Some(true) {
continue;
}
let Some(surface) = payload
.get("surface")
.and_then(serde_json::Value::as_str)
.and_then(Surface::from_str)
else {
continue;
};
let Some(memory_id) = payload.get("memory_id").and_then(serde_json::Value::as_str) else {
continue;
};
let cited: bool = conn
.query_row(
"SELECT EXISTS(
SELECT 1 FROM memory_citations
WHERE memory_id = ?1 AND cited_at >= ?2
)",
rusqlite::params![memory_id, ts],
|r| r.get(0),
)
.unwrap_or(false);
if let Some(stats) = tally.iter_mut().find(|s| s.surface == surface.as_str()) {
stats.injected += 1;
if cited {
stats.cited += 1;
}
}
}
tally.retain(|s| s.injected > 0);
Ok(tally)
}
pub fn collect_examples(conn: &Connection) -> KimetsuResult<Vec<Example>> {
let mut stmt = conn.prepare(
"SELECT e.payload_json, e.ts
FROM events AS e
WHERE e.kind = ?1
ORDER BY e.ts",
)?;
let rows = stmt
.query_map(rusqlite::params![INJECTED_EVENT], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?
.collect::<Result<Vec<_>, _>>()?;
let mut examples = Vec::new();
for (payload_json, ts) in rows {
let Ok(payload) = serde_json::from_str::<serde_json::Value>(&payload_json) else {
continue;
};
if payload.get("injected").and_then(serde_json::Value::as_bool) == Some(false) {
continue;
}
let Some(memory_id) = payload.get("memory_id").and_then(serde_json::Value::as_str) else {
continue;
};
let Some(values) = payload
.get("features")
.and_then(serde_json::Value::as_array)
else {
continue;
};
let floats: Vec<f32> = values
.iter()
.filter_map(|v| v.as_f64().map(|f| f as f32))
.collect();
let Some(features) = Features::from_slice(&floats) else {
continue; };
let cited: bool = conn
.query_row(
"SELECT EXISTS(
SELECT 1 FROM memory_citations
WHERE memory_id = ?1 AND cited_at >= ?2
)",
rusqlite::params![memory_id, ts],
|r| r.get(0),
)
.unwrap_or(false);
examples.push(Example {
features,
useful: cited,
});
}
Ok(examples)
}
#[cfg(test)]
mod tests {
use super::*;
fn features(score: f32, loop_mode: bool) -> Features {
Features {
score,
loop_mode: if loop_mode { 1.0 } else { 0.0 },
is_failure_pattern: 0.0,
novelty: 1.0,
repeat_count: 0.0,
recovery: 1.0,
evidence: 0.5,
}
}
#[test]
fn the_prior_reproduces_the_legacy_threshold() {
let policy = Policy::prior();
assert!(policy.is_prior());
assert!(!policy.should_inject(&features(LEGACY_MIN_SCORE - 0.01, false)));
assert!(policy.should_inject(&features(LEGACY_MIN_SCORE + 0.01, false)));
assert!(
(policy.probability(&features(LEGACY_MIN_SCORE, false)) - 0.5).abs() < 1e-4,
"p must be exactly 0.5 at the legacy threshold"
);
assert!(!policy.should_inject(&features(LEGACY_LOOP_MIN_SCORE - 0.01, true)));
assert!(policy.should_inject(&features(LEGACY_LOOP_MIN_SCORE + 0.01, true)));
assert!(
(policy.probability(&features(LEGACY_LOOP_MIN_SCORE, true)) - 0.5).abs() < 1e-4,
"loop mode must cross at the legacy loop threshold"
);
}
#[test]
fn the_prior_ignores_every_untrained_feature() {
let policy = Policy::prior();
let base = features(0.5, false);
let loud = Features {
is_failure_pattern: 1.0,
novelty: 0.0,
repeat_count: 1.0,
recovery: 0.0,
evidence: 1.0,
..base
};
assert!(
(policy.probability(&base) - policy.probability(&loud)).abs() < 1e-6,
"untrained features must have zero weight"
);
}
fn dataset(n: usize, boundary: f32) -> Vec<Example> {
(0..n)
.map(|i| {
let score = i as f32 / n as f32;
Example {
features: features(score, false),
useful: score >= boundary,
}
})
.collect()
}
#[test]
fn a_small_dataset_does_not_move_the_policy() {
let fitted = fit(&dataset(MIN_TRAINING_EXAMPLES - 1, 0.8));
assert_eq!(fitted, Policy::prior());
assert!(fitted.is_prior());
}
#[test]
fn single_class_data_does_not_move_the_policy() {
let all_useful: Vec<Example> = (0..100)
.map(|_| Example {
features: features(0.5, false),
useful: true,
})
.collect();
assert_eq!(fit(&all_useful), Policy::prior());
let none_useful: Vec<Example> = all_useful
.iter()
.map(|e| Example {
useful: false,
..*e
})
.collect();
assert_eq!(fit(&none_useful), Policy::prior());
}
#[test]
fn training_moves_the_boundary_towards_the_evidence() {
let examples = dataset(200, 0.8);
let fitted = fit(&examples);
assert!(
!fitted.is_prior(),
"a large clean dataset must produce a fit"
);
assert_eq!(fitted.trained_on, 200);
assert!(fitted.is_valid());
assert!(
Policy::prior().should_inject(&features(0.5, false)),
"sanity: the prior does speak here"
);
assert!(
!fitted.should_inject(&features(0.5, false)),
"the fit must learn to stay quiet where injections went unused"
);
assert!(
fitted.should_inject(&features(0.95, false)),
"and must still speak where they landed"
);
assert!(
accuracy(&fitted, &examples) > accuracy(&Policy::prior(), &examples),
"the fit must beat the prior on its own data"
);
}
#[test]
fn training_can_also_make_the_policy_speak_sooner() {
let examples = dataset(200, 0.2);
let fitted = fit(&examples);
assert!(!fitted.is_prior());
assert!(
fitted.should_inject(&features(0.3, false)),
"injections that paid off below the legacy floor must raise the odds"
);
}
#[test]
fn a_policy_round_trips_through_json() {
let fitted = fit(&dataset(200, 0.7));
let json = serde_json::to_string(&fitted).expect("serialize");
let back: Policy = serde_json::from_str(&json).expect("deserialize");
assert_eq!(fitted, back);
}
#[test]
fn a_wrong_shaped_policy_is_invalid() {
let bad = Policy {
weights: vec![1.0; FEATURE_COUNT + 2],
bias: 0.0,
trained_on: 100,
trained_at: None,
};
assert!(!bad.is_valid());
let nan = Policy {
weights: vec![f32::NAN; FEATURE_COUNT],
bias: 0.0,
trained_on: 100,
trained_at: None,
};
assert!(!nan.is_valid());
}
#[test]
fn feature_names_line_up_with_the_vector() {
assert_eq!(Features::NAMES.len(), FEATURE_COUNT);
assert_eq!(features(0.5, false).to_vec().len(), FEATURE_COUNT);
let round = Features::from_slice(&features(0.42, true).to_vec()).expect("round trip");
assert_eq!(round, features(0.42, true));
assert!(Features::from_slice(&[0.1, 0.2]).is_none());
}
#[test]
fn sigmoid_is_stable_at_the_extremes() {
assert!(sigmoid(0.0) == 0.5);
assert!(sigmoid(200.0).is_finite() && sigmoid(200.0) > 0.999);
assert!(sigmoid(-200.0).is_finite() && sigmoid(-200.0) < 0.001);
}
fn surface_conn() -> Connection {
let conn = Connection::open_in_memory().expect("open");
crate::schema::initialize(&conn).expect("schema");
conn
}
fn log_injection(
conn: &Connection,
memory_id: &str,
surface: Option<&str>,
injected: bool,
ts: &str,
) {
let mut payload = serde_json::json!({
"memory_id": memory_id,
"features": features(0.6, false).to_vec().to_vec(),
"injected": injected,
});
if let Some(surface) = surface {
payload["surface"] = serde_json::json!(surface);
}
conn.execute(
"INSERT INTO events (event_id, run_id, ts, kind, schema_version, payload_json)
VALUES (?1, 'test-run', ?2, ?3, 1, ?4)",
rusqlite::params![
kimetsu_core::ids::new_id().to_string(),
ts,
INJECTED_EVENT,
payload.to_string()
],
)
.expect("insert event");
}
fn cite(conn: &Connection, memory_id: &str, ts: &str) {
conn.execute(
"INSERT INTO memory_citations (run_id, memory_id, turn, cited_at)
VALUES (?1, ?2, 1, ?3)",
rusqlite::params![format!("run-{memory_id}"), memory_id, ts],
)
.expect("insert citation");
}
#[test]
fn surfaces_are_scored_separately() {
let conn = surface_conn();
for id in ["post-a", "post-b"] {
log_injection(&conn, id, Some("posttool"), true, "2026-01-01T00:00:00Z");
cite(&conn, id, "2026-01-01T00:01:00Z");
}
for id in ["pre-a", "pre-b"] {
log_injection(
&conn,
id,
Some("pretool_prefetch"),
true,
"2026-01-01T00:00:00Z",
);
}
let stats = surface_acceptance(&conn).expect("stats");
let post = stats
.iter()
.find(|s| s.surface == Surface::PostTool.as_str())
.expect("posttool");
let pre = stats
.iter()
.find(|s| s.surface == Surface::PreToolPrefetch.as_str())
.expect("prefetch");
assert_eq!((post.injected, post.cited), (2, 2));
assert_eq!((pre.injected, pre.cited), (2, 0));
assert!((post.acceptance() - 1.0).abs() < f32::EPSILON);
assert!(pre.acceptance() == 0.0);
}
#[test]
fn an_unexercised_surface_is_omitted_rather_than_scored_zero() {
let conn = surface_conn();
log_injection(
&conn,
"post-a",
Some("posttool"),
true,
"2026-01-01T00:00:00Z",
);
let stats = surface_acceptance(&conn).expect("stats");
assert_eq!(stats.len(), 1, "got: {stats:?}");
assert_eq!(stats[0].surface, Surface::PostTool.as_str());
}
#[test]
fn suppressed_injections_do_not_count_against_a_surface() {
let conn = surface_conn();
log_injection(
&conn,
"post-a",
Some("posttool"),
false,
"2026-01-01T00:00:00Z",
);
assert!(surface_acceptance(&conn).expect("stats").is_empty());
}
#[test]
fn history_from_before_surfaces_is_dropped_not_defaulted() {
let conn = surface_conn();
log_injection(&conn, "old-a", None, true, "2026-01-01T00:00:00Z");
cite(&conn, "old-a", "2026-01-01T00:01:00Z");
assert!(surface_acceptance(&conn).expect("stats").is_empty());
}
#[test]
fn only_citations_after_the_injection_count() {
let conn = surface_conn();
cite(&conn, "post-a", "2025-12-01T00:00:00Z");
log_injection(
&conn,
"post-a",
Some("posttool"),
true,
"2026-01-01T00:00:00Z",
);
let stats = surface_acceptance(&conn).expect("stats");
assert_eq!(stats[0].cited, 0, "got: {stats:?}");
}
#[test]
fn surface_strings_round_trip() {
for surface in [
Surface::PreToolCommand,
Surface::PreToolPrefetch,
Surface::PostTool,
] {
assert_eq!(Surface::from_str(surface.as_str()), Some(surface));
}
assert!(Surface::from_str("something_else").is_none());
}
}