1use std::collections::{HashMap, VecDeque};
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11pub struct BetaPosterior {
12 pub alpha: f64,
13 pub beta: f64,
14}
15
16impl BetaPosterior {
17 pub fn new(alpha: f64, beta: f64) -> Self {
18 Self { alpha, beta }
19 }
20
21 pub fn mean(&self) -> f64 {
22 self.alpha / (self.alpha + self.beta)
23 }
24
25 pub fn variance(&self) -> f64 {
26 let n = self.alpha + self.beta;
27 (self.alpha * self.beta) / (n * n * (n + 1.0))
28 }
29
30 pub fn effective_sample_size(&self) -> f64 {
31 self.alpha + self.beta
32 }
33
34 pub fn update_success(&mut self) {
35 self.alpha += 1.0;
36 }
37
38 pub fn update_failure(&mut self) {
39 self.beta += 1.0;
40 }
41
42 pub fn merge(&self, other: &BetaPosterior, prior: &BetaPosterior) -> BetaPosterior {
46 BetaPosterior {
47 alpha: self.alpha + other.alpha - prior.alpha,
48 beta: self.beta + other.beta - prior.beta,
49 }
50 }
51}
52
53impl Default for BetaPosterior {
54 fn default() -> Self {
55 Self::new(1.0, 1.0)
56 }
57}
58
59pub struct EntityPosteriors {
64 map: HashMap<Uuid, BetaPosterior>,
65 order: VecDeque<Uuid>,
66 capacity: usize,
67}
68
69impl EntityPosteriors {
70 pub fn new(capacity: usize) -> Self {
71 Self {
72 map: HashMap::with_capacity(capacity),
73 order: VecDeque::with_capacity(capacity),
74 capacity,
75 }
76 }
77
78 pub fn get_or_insert(
79 &mut self,
80 id: Uuid,
81 default: impl FnOnce() -> BetaPosterior,
82 ) -> &mut BetaPosterior {
83 if !self.map.contains_key(&id) {
84 if self.map.len() >= self.capacity {
85 if let Some(evicted) = self.order.pop_front() {
86 self.map.remove(&evicted);
87 }
88 }
89 self.map.insert(id, default());
90 self.order.push_back(id);
91 }
92 self.map.get_mut(&id).unwrap()
93 }
94
95 pub fn get(&self, id: &Uuid) -> Option<&BetaPosterior> {
96 self.map.get(id)
97 }
98
99 pub fn len(&self) -> usize {
100 self.map.len()
101 }
102
103 pub fn is_empty(&self) -> bool {
104 self.map.is_empty()
105 }
106
107 pub fn clear(&mut self) {
108 self.map.clear();
109 self.order.clear();
110 }
111
112 pub fn to_snapshot(&self) -> HashMap<Uuid, BetaPosterior> {
113 self.map.clone()
114 }
115
116 pub fn from_snapshot(snapshot: HashMap<Uuid, BetaPosterior>, capacity: usize) -> Self {
117 let mut ep = Self::new(capacity);
118 for (id, posterior) in snapshot {
119 ep.map.insert(id, posterior);
120 ep.order.push_back(id);
121 }
122 ep
123 }
124}
125
126pub struct BalancedRecallState {
133 pub relevance: BetaPosterior,
135 pub importance: BetaPosterior,
137 pub temporal: BetaPosterior,
139 pub entity_posteriors: EntityPosteriors,
141 pub total_events: u64,
143 pub exploration_epoch: u64,
145}
146
147impl BalancedRecallState {
148 pub fn new(entity_capacity: usize) -> Self {
149 Self {
150 relevance: BetaPosterior::new(7.0, 3.0),
151 importance: BetaPosterior::new(2.0, 8.0),
152 temporal: BetaPosterior::new(1.0, 9.0),
153 entity_posteriors: EntityPosteriors::new(entity_capacity),
154 total_events: 0,
155 exploration_epoch: 0,
156 }
157 }
158
159 pub fn reset_posteriors(&mut self) {
160 self.relevance = BetaPosterior::new(7.0, 3.0);
161 self.importance = BetaPosterior::new(2.0, 8.0);
162 self.temporal = BetaPosterior::new(1.0, 9.0);
163 self.entity_posteriors.clear();
164 self.exploration_epoch += 1;
165 }
166
167 pub fn to_snapshot(&self) -> BalancedRecallSnapshot {
168 BalancedRecallSnapshot {
169 relevance: self.relevance.clone(),
170 importance: self.importance.clone(),
171 temporal: self.temporal.clone(),
172 entity_posteriors: self.entity_posteriors.to_snapshot(),
173 total_events: self.total_events,
174 exploration_epoch: self.exploration_epoch,
175 }
176 }
177
178 pub fn from_snapshot(snapshot: BalancedRecallSnapshot, entity_capacity: usize) -> Self {
179 Self {
180 relevance: snapshot.relevance,
181 importance: snapshot.importance,
182 temporal: snapshot.temporal,
183 entity_posteriors: EntityPosteriors::from_snapshot(
184 snapshot.entity_posteriors,
185 entity_capacity,
186 ),
187 total_events: snapshot.total_events,
188 exploration_epoch: snapshot.exploration_epoch,
189 }
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct BalancedRecallSnapshot {
196 pub relevance: BetaPosterior,
197 pub importance: BetaPosterior,
198 pub temporal: BetaPosterior,
199 pub entity_posteriors: HashMap<Uuid, BetaPosterior>,
200 pub total_events: u64,
201 pub exploration_epoch: u64,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(rename_all = "snake_case")]
209pub enum ProfileLifecycle {
210 Defined,
212 Registered,
214 Active,
216 Inactive,
218 Archived,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct ProfileRecord {
227 pub id: String,
228 pub description: String,
229 pub consumer_kind: String,
230 pub state_class: String,
231 pub lifecycle: ProfileLifecycle,
232 pub created_at: DateTime<Utc>,
233 pub state_snapshot: Option<serde_json::Value>,
235 pub total_events: u64,
236 pub exploration_epoch: u64,
237}
238
239impl ProfileRecord {
240 pub fn new_balanced_recall(entity_capacity: usize) -> Self {
241 let state = BalancedRecallState::new(entity_capacity);
242 let snapshot = state.to_snapshot();
243 Self {
244 id: "balanced-recall-v1".into(),
245 description: "Default recall profile: three-scalar Beta posteriors (ADR-032 §5a)"
246 .into(),
247 consumer_kind: "recall".into(),
248 state_class: "Bayesian".into(),
249 lifecycle: ProfileLifecycle::Active,
250 created_at: Utc::now(),
251 state_snapshot: serde_json::to_value(snapshot).ok(),
252 total_events: 0,
253 exploration_epoch: 0,
254 }
255 }
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct ProfileBinding {
265 pub actor: String,
266 pub namespace: String,
267 pub consumer_kind: String,
268 pub profile_id: String,
269 pub priority: i32,
270 pub created_at: DateTime<Utc>,
271}
272
273pub struct BrainState {
280 pub profiles: HashMap<String, ProfileRecord>,
282 pub balanced_recall: BalancedRecallState,
284 pub bindings: Vec<ProfileBinding>,
286}
287
288impl BrainState {
289 pub fn new(entity_capacity: usize) -> Self {
290 let mut profiles = HashMap::new();
291 let record = ProfileRecord::new_balanced_recall(entity_capacity);
292 profiles.insert(record.id.clone(), record);
293 Self {
294 profiles,
295 balanced_recall: BalancedRecallState::new(entity_capacity),
296 bindings: Vec::new(),
297 }
298 }
299
300 pub fn to_snapshot(&self) -> BrainStateSnapshot {
301 BrainStateSnapshot {
302 profiles: self.profiles.clone(),
303 balanced_recall: self.balanced_recall.to_snapshot(),
304 bindings: self.bindings.clone(),
305 }
306 }
307
308 pub fn from_snapshot(snapshot: BrainStateSnapshot, entity_capacity: usize) -> Self {
309 Self {
310 profiles: snapshot.profiles,
311 balanced_recall: BalancedRecallState::from_snapshot(
312 snapshot.balanced_recall,
313 entity_capacity,
314 ),
315 bindings: snapshot.bindings,
316 }
317 }
318
319 pub fn reset_posteriors(&mut self) {
321 self.balanced_recall.reset_posteriors();
322 if let Some(record) = self.profiles.get_mut("balanced-recall-v1") {
323 record.exploration_epoch = self.balanced_recall.exploration_epoch;
324 record.state_snapshot = serde_json::to_value(self.balanced_recall.to_snapshot()).ok();
325 }
326 }
327
328 pub fn resolve(
334 &self,
335 actor: Option<&str>,
336 namespace: Option<&str>,
337 consumer_kind: &str,
338 ) -> Option<&ProfileRecord> {
339 let actor_val = actor.unwrap_or("*");
340 let namespace_val = namespace.unwrap_or("*");
341
342 let best = self
343 .bindings
344 .iter()
345 .filter(|b| {
346 (b.actor == "*" || b.actor == actor_val)
347 && (b.namespace == "*" || b.namespace == namespace_val)
348 && (b.consumer_kind == "*" || b.consumer_kind == consumer_kind)
349 })
350 .max_by_key(|b| {
351 let actor_score = if b.actor != "*" { 4 } else { 0 };
352 let ns_score = if b.namespace != "*" { 2 } else { 0 };
353 let kind_score = if b.consumer_kind != "*" { 1 } else { 0 };
354 (
355 actor_score + ns_score + kind_score,
356 b.priority,
357 -(b.created_at.timestamp()),
358 )
359 });
360
361 if let Some(binding) = best {
362 return self.profiles.get(&binding.profile_id);
363 }
364
365 if let Some(default) = self.profiles.get("balanced-recall-v1") {
369 if default.lifecycle == ProfileLifecycle::Active
370 && (default.consumer_kind == consumer_kind
371 || consumer_kind == "*"
372 || default.consumer_kind == "*")
373 {
374 return Some(default);
375 }
376 }
377
378 self.profiles
380 .values()
381 .find(|p| p.consumer_kind == consumer_kind && p.lifecycle == ProfileLifecycle::Active)
382 }
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct BrainStateSnapshot {
388 pub profiles: HashMap<String, ProfileRecord>,
389 pub balanced_recall: BalancedRecallSnapshot,
390 pub bindings: Vec<ProfileBinding>,
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn beta_posterior_mean() {
399 let p = BetaPosterior::new(7.0, 3.0);
400 assert!((p.mean() - 0.7).abs() < 1e-12);
401 }
402
403 #[test]
404 fn beta_posterior_variance() {
405 let p = BetaPosterior::new(7.0, 3.0);
406 let expected = 21.0 / 1100.0;
407 assert!((p.variance() - expected).abs() < 1e-12);
408 }
409
410 #[test]
411 fn beta_posterior_ess() {
412 let p = BetaPosterior::new(7.0, 3.0);
413 assert!((p.effective_sample_size() - 10.0).abs() < 1e-12);
414 }
415
416 #[test]
417 fn beta_posterior_update() {
418 let mut p = BetaPosterior::new(1.0, 1.0);
419 p.update_success();
420 p.update_success();
421 p.update_failure();
422 assert!((p.alpha - 3.0).abs() < 1e-12);
423 assert!((p.beta - 2.0).abs() < 1e-12);
424 assert!((p.mean() - 0.6).abs() < 1e-12);
425 }
426
427 #[test]
428 fn beta_posterior_merge() {
429 let prior = BetaPosterior::new(2.0, 8.0);
430 let a = BetaPosterior::new(5.0, 9.0); let b = BetaPosterior::new(4.0, 10.0); let merged = a.merge(&b, &prior);
433 assert!((merged.alpha - 7.0).abs() < 1e-12);
435 assert!((merged.beta - 11.0).abs() < 1e-12);
436 }
437
438 #[test]
439 fn entity_posteriors_eviction() {
440 let mut ep = EntityPosteriors::new(3);
441 let ids: Vec<Uuid> = (0..5).map(|_| Uuid::new_v4()).collect();
442 for id in &ids {
443 ep.get_or_insert(*id, BetaPosterior::default);
444 }
445 assert_eq!(ep.len(), 3);
446 assert!(ep.get(&ids[0]).is_none());
447 assert!(ep.get(&ids[1]).is_none());
448 assert!(ep.get(&ids[2]).is_some());
449 assert!(ep.get(&ids[3]).is_some());
450 assert!(ep.get(&ids[4]).is_some());
451 }
452
453 #[test]
454 fn entity_posteriors_get_or_insert_existing() {
455 let mut ep = EntityPosteriors::new(10);
456 let id = Uuid::new_v4();
457 ep.get_or_insert(id, BetaPosterior::default)
458 .update_success();
459 let p = ep.get_or_insert(id, BetaPosterior::default);
460 assert!((p.alpha - 2.0).abs() < 1e-12);
461 }
462
463 #[test]
464 fn balanced_recall_state_snapshot_roundtrip() {
465 let mut state = BalancedRecallState::new(100);
466 state.relevance.update_success();
467 state.total_events = 42;
468 let id = Uuid::new_v4();
469 state
470 .entity_posteriors
471 .get_or_insert(id, BetaPosterior::default)
472 .update_success();
473
474 let snapshot = state.to_snapshot();
475 let json = serde_json::to_string(&snapshot).unwrap();
476 let back: BalancedRecallSnapshot = serde_json::from_str(&json).unwrap();
477 assert_eq!(back.total_events, 42);
478 assert!((back.relevance.alpha - 8.0).abs() < 1e-12);
479 assert!(back.entity_posteriors.contains_key(&id));
480 }
481
482 #[test]
483 fn balanced_recall_state_reset_preserves_epoch_increment() {
484 let mut state = BalancedRecallState::new(10);
485 state.total_events = 100;
486 state.reset_posteriors();
487 assert_eq!(state.total_events, 100);
488 assert_eq!(state.exploration_epoch, 1);
489 assert!((state.relevance.alpha - 7.0).abs() < 1e-12);
490 assert!((state.relevance.beta - 3.0).abs() < 1e-12);
491 }
492
493 #[test]
494 fn brain_state_has_balanced_recall_profile_by_default() {
495 let state = BrainState::new(100);
496 assert!(state.profiles.contains_key("balanced-recall-v1"));
497 let record = &state.profiles["balanced-recall-v1"];
498 assert_eq!(record.lifecycle, ProfileLifecycle::Active);
499 assert_eq!(record.consumer_kind, "recall");
500 assert_eq!(record.state_class, "Bayesian");
501 }
502
503 #[test]
504 fn brain_state_reset_posteriors_updates_record() {
505 let mut state = BrainState::new(10);
506 state.balanced_recall.relevance.update_success();
507 state.balanced_recall.total_events = 50;
508 state.reset_posteriors();
509 assert_eq!(state.balanced_recall.exploration_epoch, 1);
510 let record = &state.profiles["balanced-recall-v1"];
511 assert_eq!(record.exploration_epoch, 1);
512 }
513
514 #[test]
515 fn brain_state_resolve_falls_back_to_default() {
516 let state = BrainState::new(100);
517 let resolved = state.resolve(None, None, "recall");
518 assert!(resolved.is_some());
519 assert_eq!(resolved.unwrap().id, "balanced-recall-v1");
520 }
521
522 #[test]
523 fn brain_state_resolve_uses_explicit_binding() {
524 let mut state = BrainState::new(100);
525 let mut alt = ProfileRecord::new_balanced_recall(100);
527 alt.id = "alt-profile".into();
528 state.profiles.insert("alt-profile".into(), alt);
529
530 state.bindings.push(ProfileBinding {
532 actor: "agent-1".into(),
533 namespace: "*".into(),
534 consumer_kind: "recall".into(),
535 profile_id: "alt-profile".into(),
536 priority: 0,
537 created_at: Utc::now(),
538 });
539
540 let resolved = state.resolve(Some("agent-1"), None, "recall");
541 assert!(resolved.is_some());
542 assert_eq!(resolved.unwrap().id, "alt-profile");
543
544 let resolved_other = state.resolve(Some("agent-2"), None, "recall");
546 assert_eq!(resolved_other.unwrap().id, "balanced-recall-v1");
547 }
548
549 #[test]
552 fn brain_state_resolve_skips_archived_default() {
553 let mut state = BrainState::new(100);
554
555 state
557 .profiles
558 .get_mut("balanced-recall-v1")
559 .expect("default profile always exists")
560 .lifecycle = ProfileLifecycle::Archived;
561
562 let resolved = state.resolve(None, None, "recall");
564 assert!(
565 resolved.is_none(),
566 "archived default profile must not be returned by resolve"
567 );
568 }
569
570 #[test]
571 fn entity_posteriors_from_snapshot_rebuilds_map() {
572 let id1 = Uuid::new_v4();
573 let id2 = Uuid::new_v4();
574 let mut snapshot = HashMap::new();
575 snapshot.insert(id1, BetaPosterior::new(3.0, 2.0));
576 snapshot.insert(id2, BetaPosterior::new(5.0, 1.0));
577
578 let ep = EntityPosteriors::from_snapshot(snapshot, 100);
579 assert_eq!(ep.len(), 2);
580 let p1 = ep.get(&id1).unwrap();
581 assert!((p1.alpha - 3.0).abs() < 1e-12);
582 let p2 = ep.get(&id2).unwrap();
583 assert!((p2.alpha - 5.0).abs() < 1e-12);
584 }
585
586 #[test]
587 fn brain_state_snapshot_roundtrip() {
588 let mut state = BrainState::new(100);
589 state.balanced_recall.relevance.update_success();
590 state.balanced_recall.total_events = 55;
591 state.balanced_recall.exploration_epoch = 2;
592 let id = Uuid::new_v4();
593 state
594 .balanced_recall
595 .entity_posteriors
596 .get_or_insert(id, || BetaPosterior::new(4.0, 6.0))
597 .update_success();
598
599 let snap1 = state.to_snapshot();
600 let restored = BrainState::from_snapshot(snap1, 100);
601 let snap2 = restored.to_snapshot();
602
603 assert_eq!(snap2.balanced_recall.total_events, 55);
604 assert_eq!(snap2.balanced_recall.exploration_epoch, 2);
605 assert!((snap2.balanced_recall.relevance.alpha - 8.0).abs() < 1e-12);
606 let ep = snap2.balanced_recall.entity_posteriors.get(&id).unwrap();
607 assert!((ep.alpha - 5.0).abs() < 1e-12);
608 assert!((ep.beta - 6.0).abs() < 1e-12);
609 }
610
611 #[test]
612 fn profile_lifecycle_serde_roundtrip() {
613 let lc = ProfileLifecycle::Active;
614 let json = serde_json::to_string(&lc).unwrap();
615 let back: ProfileLifecycle = serde_json::from_str(&json).unwrap();
616 assert_eq!(back, ProfileLifecycle::Active);
617 }
618
619 #[test]
620 fn beta_posterior_default_has_uniform_prior() {
621 let p = BetaPosterior::default();
622 assert!((p.alpha - 1.0).abs() < 1e-12);
623 assert!((p.beta - 1.0).abs() < 1e-12);
624 assert!((p.mean() - 0.5).abs() < 1e-12);
625 }
626}