1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//! JSON wrapper for utility-aware memory retrieval (U-Mem, arXiv 2602.22406).
//!
//! Stateless helper, binding-only — see
//! `docs/proposals/autonomous-memory-agents.md`. Ranks retrieval candidates by
//! blending semantic relevance with a learned per-memory utility posterior,
//! using the deterministic UCB variant (reproducible; no RNG in the hot path).
use car_memgine::utility::{rank_ucb, Candidate, UtilityPosterior};
use serde::Deserialize;
/// One retrieval candidate as supplied over FFI: an id, its semantic
/// `relevance` in `[0,1]`, and the observed outcome counts that form its utility
/// posterior (`Beta(success+1, fail+1)`).
#[derive(Deserialize)]
struct CandidateInput {
id: String,
relevance: f64,
#[serde(default)]
success: u64,
#[serde(default)]
fail: u64,
}
/// Rank memory-retrieval candidates by utility-aware UCB (U-Mem SA-CTS,
/// deterministic variant). `candidates_json` is a JSON array of `{ id,
/// relevance, success, fail }`. `exploration` is the cold-start/uncertainty
/// bonus weight; `utility_weight` (`0..1`) blends utility vs. raw relevance
/// (`0` = pure relevance, unchanged). Returns the ranked JSON array
/// `[{ id, score, relevance, utility }]`, highest score first.
pub fn rank(
candidates_json: &str,
exploration: f64,
utility_weight: f64,
) -> Result<String, String> {
let inputs: Vec<CandidateInput> = crate::from_json("candidates", candidates_json)?;
let candidates: Vec<Candidate> = inputs
.into_iter()
.map(|c| Candidate {
id: c.id,
relevance: c.relevance,
posterior: UtilityPosterior::from_counts(c.success, c.fail),
})
.collect();
let ranked = rank_ucb(&candidates, exploration, utility_weight);
crate::to_json(&ranked)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
#[test]
fn ranks_proven_over_relevant_when_weighted() {
let cands = r#"[
{"id":"a","relevance":0.5,"success":50,"fail":0},
{"id":"b","relevance":0.7,"success":0,"fail":50}
]"#;
let out = rank(cands, 0.0, 0.8).unwrap();
let v: Value = serde_json::from_str(&out).unwrap();
assert_eq!(v[0]["id"], "a"); // proven-useful beats more-relevant-useless
}
#[test]
fn weight_zero_keeps_relevance_order() {
let cands = r#"[
{"id":"a","relevance":0.4,"success":50,"fail":0},
{"id":"b","relevance":0.9,"success":0,"fail":50}
]"#;
let out = rank(cands, 0.0, 0.0).unwrap();
let v: Value = serde_json::from_str(&out).unwrap();
assert_eq!(v[0]["id"], "b");
}
#[test]
fn missing_counts_default_to_cold_start() {
let out = rank(r#"[{"id":"x","relevance":0.5}]"#, 1.0, 0.5).unwrap();
let v: Value = serde_json::from_str(&out).unwrap();
assert_eq!(v[0]["id"], "x");
}
}