khive_pack_memory/
tunable.rs1use khive_brain_core::BalancedRecallState;
5use khive_brain_core::{PackTunable, ParameterDef, ParameterSpace};
6use khive_runtime::RuntimeError;
7use serde_json::Value;
8
9use crate::config::RecallConfig;
10use crate::MemoryPack;
11
12impl PackTunable for MemoryPack {
17 fn parameter_space(&self) -> ParameterSpace {
18 ParameterSpace {
19 parameters: vec![
20 ParameterDef {
21 name: "memory::relevance_weight".into(),
22 prior_alpha: 7.0,
25 prior_beta: 3.0,
26 bounds: (0.0, 1.0),
27 },
28 ParameterDef {
29 name: "memory::salience_weight".into(),
30 prior_alpha: 2.0,
32 prior_beta: 8.0,
33 bounds: (0.0, 1.0),
34 },
35 ParameterDef {
36 name: "memory::temporal_weight".into(),
37 prior_alpha: 1.0,
39 prior_beta: 9.0,
40 bounds: (0.0, 1.0),
41 },
42 ],
43 }
44 }
45
46 fn project_config(&self, state: &BalancedRecallState) -> Value {
48 let current = self.active_config();
49
50 let relevance = state.relevance.mean();
51 let salience = state.salience.mean();
52 let temporal = state.temporal.mean();
53
54 let projected = RecallConfig {
55 relevance_weight: relevance,
56 salience_weight: salience,
57 temporal_weight: temporal,
58 ..current
59 };
60
61 serde_json::to_value(projected).unwrap_or_else(|_| serde_json::json!({}))
62 }
63
64 fn apply_config(&self, config: Value) -> Result<(), RuntimeError> {
66 let new_cfg: RecallConfig = serde_json::from_value(config)
67 .map_err(|e| RuntimeError::InvalidInput(format!("invalid RecallConfig: {e}")))?;
68 new_cfg.validate()?;
69 *self.config.lock().unwrap() = new_cfg;
70 Ok(())
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use khive_brain_core::{BalancedRecallState, BetaPosterior};
78 use khive_runtime::KhiveRuntime;
79
80 fn make_pack() -> MemoryPack {
81 let rt = KhiveRuntime::memory().expect("in-memory runtime");
82 MemoryPack::new(rt)
83 }
84
85 fn balanced_state_with_means(
86 relevance_mean: f64,
87 salience_mean: f64,
88 temporal_mean: f64,
89 ) -> BalancedRecallState {
90 let to_posterior =
93 |mean: f64| -> BetaPosterior { BetaPosterior::new(mean * 10.0, (1.0 - mean) * 10.0) };
94 let mut state = BalancedRecallState::new(100);
95 state.relevance = to_posterior(relevance_mean);
96 state.salience = to_posterior(salience_mean);
97 state.temporal = to_posterior(temporal_mean);
98 state
99 }
100
101 #[test]
102 fn parameter_space_has_three_params() {
103 let pack = make_pack();
104 let space = pack.parameter_space();
105 assert_eq!(space.parameters.len(), 3);
106 let names: Vec<&str> = space.parameters.iter().map(|p| p.name.as_str()).collect();
107 assert!(names.contains(&"memory::relevance_weight"));
108 assert!(names.contains(&"memory::salience_weight"));
109 assert!(names.contains(&"memory::temporal_weight"));
110 }
111
112 #[test]
113 fn project_config_reads_posterior_means() {
114 let pack = make_pack();
115 let state = balanced_state_with_means(0.6, 0.3, 0.1);
116 let projected = pack.project_config(&state);
117
118 let cfg: RecallConfig = serde_json::from_value(projected).unwrap();
119 assert!((cfg.relevance_weight - 0.6).abs() < 1e-10);
120 assert!((cfg.salience_weight - 0.3).abs() < 1e-10);
121 assert!((cfg.temporal_weight - 0.1).abs() < 1e-10);
122 }
123
124 #[test]
125 fn project_config_with_default_priors_matches_expected_defaults() {
126 let pack = make_pack();
128 let state = BalancedRecallState::new(100);
129 let projected = pack.project_config(&state);
130
131 let cfg: RecallConfig = serde_json::from_value(projected).unwrap();
132 assert!((cfg.relevance_weight - 0.70).abs() < 1e-10);
133 assert!((cfg.salience_weight - 0.20).abs() < 1e-10);
134 assert!((cfg.temporal_weight - 0.10).abs() < 1e-10);
135 }
136
137 #[test]
138 fn apply_config_updates_active_config() {
139 let pack = make_pack();
140 let new_cfg = RecallConfig {
141 relevance_weight: 0.5,
142 salience_weight: 0.3,
143 temporal_weight: 0.2,
144 ..RecallConfig::default()
145 };
146 let config_value = serde_json::to_value(&new_cfg).unwrap();
147 pack.apply_config(config_value)
148 .expect("apply_config succeeds");
149
150 let active = pack.active_config();
151 assert!((active.relevance_weight - 0.5).abs() < 1e-10);
152 assert!((active.salience_weight - 0.3).abs() < 1e-10);
153 assert!((active.temporal_weight - 0.2).abs() < 1e-10);
154 }
155
156 #[test]
157 fn apply_config_rejects_all_zero_weights() {
158 let pack = make_pack();
159 let bad_cfg = RecallConfig {
160 relevance_weight: 0.0,
161 salience_weight: 0.0,
162 temporal_weight: 0.0,
163 ..RecallConfig::default()
164 };
165 let config_value = serde_json::to_value(&bad_cfg).unwrap();
166 assert!(pack.apply_config(config_value).is_err());
167 }
168
169 #[test]
170 fn apply_config_rejects_malformed_json() {
171 let pack = make_pack();
172 let bad = serde_json::json!({ "relevance_weight": "not_a_number" });
173 assert!(pack.apply_config(bad).is_err());
174 }
175
176 #[test]
177 fn prior_for_relevance_weight_matches_balanced_recall_state_prior() {
178 let pack = make_pack();
180 let space = pack.parameter_space();
181 let def = space
182 .parameters
183 .iter()
184 .find(|p| p.name == "memory::relevance_weight")
185 .unwrap();
186 assert!((def.prior_alpha - 7.0).abs() < 1e-12);
187 assert!((def.prior_beta - 3.0).abs() < 1e-12);
188 }
189}