1pub mod event;
2pub mod fold;
3pub mod state;
4pub mod tunable;
5
6use std::sync::Mutex;
7
8use async_trait::async_trait;
9use chrono::Utc;
10use serde::Deserialize;
11use serde_json::{json, Value};
12
13use khive_fold::{Fold, FoldContext};
14use khive_runtime::pack::PackRuntime;
15use khive_runtime::{
16 DispatchHook, EventView, KhiveRuntime, NamespaceToken, RuntimeError, VerbRegistry,
17};
18use khive_storage::event::{Event, EventFilter};
19use khive_storage::types::PageRequest;
20use khive_types::{HandlerDef, Pack, VerbCategory, Visibility};
21
22use crate::fold::BalancedRecallFold;
23use crate::state::{BrainState, ProfileBinding, ProfileLifecycle, ProfileRecord};
24
25const ENTITY_CACHE_CAPACITY: usize = 10_000;
26
27static BRAIN_HANDLERS: &[HandlerDef] = &[
36 HandlerDef {
38 name: "brain.state",
39 description: "Return current BrainState snapshot for inspection",
40 visibility: Visibility::Subhandler,
41 category: VerbCategory::Assertive,
42 },
43 HandlerDef {
44 name: "brain.config",
45 description: "Return projected config for a named pack parameter",
46 visibility: Visibility::Subhandler,
47 category: VerbCategory::Assertive,
48 },
49 HandlerDef {
50 name: "brain.events",
51 description: "List recent brain-relevant events for debugging",
52 visibility: Visibility::Subhandler,
53 category: VerbCategory::Assertive,
54 },
55 HandlerDef {
56 name: "brain.profiles",
57 description: "List profiles, optionally filtered by lifecycle",
58 visibility: Visibility::Verb,
59 category: VerbCategory::Assertive,
60 },
61 HandlerDef {
62 name: "brain.profile",
63 description: "Profile metadata, latest snapshot, current state summary",
64 visibility: Visibility::Verb,
65 category: VerbCategory::Assertive,
66 },
67 HandlerDef {
68 name: "brain.resolve",
69 description: "Show which profile would serve a caller context",
70 visibility: Visibility::Verb,
71 category: VerbCategory::Assertive,
72 },
73 HandlerDef {
75 name: "brain.activate",
76 description: "Move a profile to Active (start live update loop)",
77 visibility: Visibility::Verb,
78 category: VerbCategory::Commissive,
79 },
80 HandlerDef {
81 name: "brain.deactivate",
82 description: "Move a profile to Inactive (stop live updates, retain state)",
83 visibility: Visibility::Verb,
84 category: VerbCategory::Commissive,
85 },
86 HandlerDef {
87 name: "brain.archive",
88 description: "Move a profile to Archived (read-only, audit-retained)",
89 visibility: Visibility::Verb,
90 category: VerbCategory::Declaration,
91 },
92 HandlerDef {
93 name: "brain.reset",
94 description: "Reset posteriors to priors (preserves event history)",
95 visibility: Visibility::Verb,
96 category: VerbCategory::Declaration,
97 },
98 HandlerDef {
99 name: "brain.feedback",
100 description: "Emit a FeedbackExplicit event into the shared log",
101 visibility: Visibility::Verb,
102 category: VerbCategory::Commissive,
103 },
104 HandlerDef {
106 name: "brain.bind",
107 description: "Write a row in the profile resolution table",
108 visibility: Visibility::Verb,
109 category: VerbCategory::Declaration,
110 },
111 HandlerDef {
112 name: "brain.unbind",
113 description: "Remove rows from the profile resolution table",
114 visibility: Visibility::Verb,
115 category: VerbCategory::Declaration,
116 },
117 HandlerDef {
119 name: "brain.emit",
120 description: "Manually emit a feedback event (deprecated; use brain.feedback)",
121 visibility: Visibility::Subhandler,
122 category: VerbCategory::Commissive,
123 },
124];
125
126pub struct BrainPack {
134 runtime: KhiveRuntime,
135 state: Mutex<BrainState>,
137 fold: BalancedRecallFold,
139}
140
141impl Pack for BrainPack {
142 const NAME: &'static str = "brain";
143 const NOTE_KINDS: &'static [&'static str] = &[];
144 const ENTITY_KINDS: &'static [&'static str] = &[];
145 const HANDLERS: &'static [HandlerDef] = BRAIN_HANDLERS;
146 const REQUIRES: &'static [&'static str] = &["kg"];
147}
148
149impl BrainPack {
150 pub fn new(runtime: KhiveRuntime) -> Self {
151 let fold = BalancedRecallFold::new(ENTITY_CACHE_CAPACITY);
152 let state = BrainState::new(ENTITY_CACHE_CAPACITY);
153 Self {
154 runtime,
155 state: Mutex::new(state),
156 fold,
157 }
158 }
159
160 pub fn snapshot(&self) -> crate::state::BrainStateSnapshot {
162 self.state.lock().unwrap().to_snapshot()
163 }
164
165 async fn handle_state(&self, _params: Value) -> Result<Value, RuntimeError> {
168 let state = self.state.lock().unwrap();
169 let snapshot = state.to_snapshot();
170 serde_json::to_value(&snapshot).map_err(|e| RuntimeError::InvalidInput(e.to_string()))
171 }
172
173 async fn handle_config(&self, params: Value) -> Result<Value, RuntimeError> {
176 #[derive(Deserialize)]
177 struct ConfigParams {
178 parameter: Option<String>,
179 }
180 let p: ConfigParams = serde_json::from_value(params)
181 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
182
183 let state = self.state.lock().unwrap();
184 let br = &state.balanced_recall;
185
186 let param_map = [
187 ("recall::relevance_weight", &br.relevance),
188 ("recall::importance_weight", &br.importance),
189 ("recall::temporal_weight", &br.temporal),
190 ];
191
192 match p.parameter {
193 Some(key) => {
194 let posterior = param_map
195 .iter()
196 .find(|(k, _)| *k == key)
197 .map(|(_, p)| *p)
198 .ok_or_else(|| {
199 RuntimeError::NotFound(format!(
200 "parameter {key:?}; valid: {}",
201 param_map
202 .iter()
203 .map(|(k, _)| *k)
204 .collect::<Vec<_>>()
205 .join(", ")
206 ))
207 })?;
208 Ok(json!({
209 "parameter": key,
210 "mean": posterior.mean(),
211 "variance": posterior.variance(),
212 "ess": posterior.effective_sample_size(),
213 "alpha": posterior.alpha,
214 "beta": posterior.beta,
215 }))
216 }
217 None => {
218 let configs: serde_json::Map<String, Value> = param_map
219 .iter()
220 .map(|(k, p)| {
221 (
222 (*k).to_owned(),
223 json!({
224 "mean": p.mean(),
225 "variance": p.variance(),
226 "ess": p.effective_sample_size(),
227 }),
228 )
229 })
230 .collect();
231 Ok(Value::Object(configs))
232 }
233 }
234 }
235
236 async fn handle_events(
239 &self,
240 token: &NamespaceToken,
241 params: Value,
242 ) -> Result<Value, RuntimeError> {
243 #[derive(Deserialize)]
244 struct EventsParams {
245 limit: Option<u32>,
246 }
247 let p: EventsParams = serde_json::from_value(params)
248 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
249
250 let limit = p.limit.unwrap_or(20).min(100);
251 let ns = token.namespace().as_str().to_string();
252
253 let store = self.runtime.events(token)?;
254 let filter = EventFilter {
255 verbs: vec![
256 "recall".into(),
257 "search".into(),
258 "brain.feedback".into(),
259 "brain.emit".into(), "get".into(),
261 "remember".into(),
262 ],
263 ..EventFilter::default()
264 };
265 let _ = ns;
266 let page = store
267 .query_events(filter, PageRequest { offset: 0, limit })
268 .await
269 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
270
271 let events: Vec<Value> = page
272 .items
273 .iter()
274 .map(|e| {
275 json!({
276 "id": e.id.to_string(),
277 "verb": e.verb,
278 "outcome": e.outcome,
279 "target_id": e.target_id.map(|t| t.to_string()),
280 "duration_us": e.duration_us,
281 "created_at": e.created_at,
282 "payload": e.payload,
283 })
284 })
285 .collect();
286
287 Ok(json!({
288 "count": events.len(),
289 "events": events,
290 }))
291 }
292
293 async fn handle_profiles(&self, params: Value) -> Result<Value, RuntimeError> {
296 #[derive(Deserialize)]
297 struct ProfilesParams {
298 lifecycle: Option<String>,
299 }
300 let p: ProfilesParams = serde_json::from_value(params)
301 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
302
303 let state = self.state.lock().unwrap();
304 let filter_lc: Option<ProfileLifecycle> = p
305 .lifecycle
306 .as_deref()
307 .map(|s| serde_json::from_value(Value::String(s.to_owned())))
308 .transpose()
309 .map_err(|e| RuntimeError::InvalidInput(format!("invalid lifecycle: {e}")))?;
310
311 let profiles: Vec<&ProfileRecord> = state
312 .profiles
313 .values()
314 .filter(|r| filter_lc.as_ref().is_none_or(|lc| &r.lifecycle == lc))
315 .collect();
316
317 let items: Vec<Value> = profiles
318 .iter()
319 .map(|r| {
320 json!({
321 "id": r.id,
322 "description": r.description,
323 "consumer_kind": r.consumer_kind,
324 "state_class": r.state_class,
325 "lifecycle": r.lifecycle,
326 "total_events": r.total_events,
327 "exploration_epoch": r.exploration_epoch,
328 "created_at": r.created_at,
329 })
330 })
331 .collect();
332
333 Ok(json!({ "count": items.len(), "profiles": items }))
334 }
335
336 async fn handle_profile(&self, params: Value) -> Result<Value, RuntimeError> {
339 #[derive(Deserialize)]
340 struct ProfileParams {
341 id: String,
342 }
343 let p: ProfileParams = serde_json::from_value(params)
344 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
345
346 let state = self.state.lock().unwrap();
347 let record = state
348 .profiles
349 .get(&p.id)
350 .ok_or_else(|| RuntimeError::NotFound(format!("profile {:?}", p.id)))?;
351
352 Ok(json!({
353 "id": record.id,
354 "description": record.description,
355 "consumer_kind": record.consumer_kind,
356 "state_class": record.state_class,
357 "lifecycle": record.lifecycle,
358 "total_events": record.total_events,
359 "exploration_epoch": record.exploration_epoch,
360 "created_at": record.created_at,
361 "state_snapshot": record.state_snapshot,
362 }))
363 }
364
365 async fn handle_resolve(&self, params: Value) -> Result<Value, RuntimeError> {
368 #[derive(Deserialize)]
369 struct ResolveParams {
370 actor: Option<String>,
371 namespace: Option<String>,
372 consumer_kind: String,
373 }
374 let p: ResolveParams = serde_json::from_value(params)
375 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
376
377 let state = self.state.lock().unwrap();
378 match state.resolve(p.actor.as_deref(), p.namespace.as_deref(), &p.consumer_kind) {
379 Some(record) => Ok(json!({
380 "resolved_profile_id": record.id,
381 "lifecycle": record.lifecycle,
382 "consumer_kind": record.consumer_kind,
383 })),
384 None => Err(RuntimeError::NotFound(format!(
385 "no profile resolved for consumer_kind={:?}",
386 p.consumer_kind
387 ))),
388 }
389 }
390
391 async fn handle_activate(&self, params: Value) -> Result<Value, RuntimeError> {
394 self.set_lifecycle(params, ProfileLifecycle::Active).await
395 }
396
397 async fn handle_deactivate(&self, params: Value) -> Result<Value, RuntimeError> {
398 self.set_lifecycle(params, ProfileLifecycle::Inactive).await
399 }
400
401 async fn handle_archive(&self, params: Value) -> Result<Value, RuntimeError> {
402 self.set_lifecycle(params, ProfileLifecycle::Archived).await
403 }
404
405 async fn set_lifecycle(
406 &self,
407 params: Value,
408 lifecycle: ProfileLifecycle,
409 ) -> Result<Value, RuntimeError> {
410 #[derive(Deserialize)]
411 struct LifecycleParams {
412 profile_id: String,
413 }
414 let p: LifecycleParams = serde_json::from_value(params)
415 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
416
417 let mut state = self.state.lock().unwrap();
418 let record = state
419 .profiles
420 .get_mut(&p.profile_id)
421 .ok_or_else(|| RuntimeError::NotFound(format!("profile {:?}", p.profile_id)))?;
422
423 record.lifecycle = lifecycle.clone();
424 Ok(json!({
425 "profile_id": p.profile_id,
426 "lifecycle": lifecycle,
427 }))
428 }
429
430 async fn handle_reset(&self, _params: Value) -> Result<Value, RuntimeError> {
433 let mut state = self.state.lock().unwrap();
434 state.reset_posteriors();
435 Ok(json!({
436 "reset": true,
437 "exploration_epoch": state.balanced_recall.exploration_epoch,
438 }))
439 }
440
441 async fn handle_feedback(
444 &self,
445 token: &NamespaceToken,
446 params: Value,
447 ) -> Result<Value, RuntimeError> {
448 #[derive(Deserialize)]
449 struct FeedbackParams {
450 target_id: String,
451 signal: String,
452 served_by_profile_id: Option<String>,
453 }
454 let p: FeedbackParams = serde_json::from_value(params)
455 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
456
457 let target: uuid::Uuid = p
458 .target_id
459 .parse()
460 .map_err(|e| RuntimeError::InvalidInput(format!("invalid target_id: {e}")))?;
461
462 let signal = match p.signal.as_str() {
463 "useful" => "useful",
464 "not_useful" => "not_useful",
465 "wrong" => "wrong",
466 other => {
467 return Err(RuntimeError::InvalidInput(format!(
468 "unknown signal {other:?}; valid: useful | not_useful | wrong"
469 )))
470 }
471 };
472
473 let mut data = json!({"signal": signal});
474 if let Some(ref profile_id) = p.served_by_profile_id {
475 data["served_by_profile_id"] = json!(profile_id);
476 }
477
478 let event = Event::new(
479 token.namespace().as_str().to_string(),
480 "brain.feedback",
481 khive_types::EventKind::FeedbackExplicit,
482 khive_types::SubstrateKind::Event,
483 "brain",
484 )
485 .with_target(target)
486 .with_payload(data);
487
488 let store = self.runtime.events(token)?;
489 store
490 .append_event(event.clone())
491 .await
492 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
493
494 let ctx = FoldContext::new();
496 let mut state = self.state.lock().unwrap();
497 let current_recall = std::mem::replace(
498 &mut state.balanced_recall,
499 crate::state::BalancedRecallState::new(0),
500 );
501 let updated = self.fold.reduce(current_recall, &event, &ctx);
502 state.balanced_recall = updated;
503
504 let total_ev = state.balanced_recall.total_events;
506 let snap_val = serde_json::to_value(state.balanced_recall.to_snapshot()).ok();
507 if let Some(record) = state.profiles.get_mut("balanced-recall-v1") {
508 record.total_events = total_ev;
509 record.state_snapshot = snap_val;
510 }
511
512 Ok(json!({
513 "emitted": true,
514 "event_id": event.id.to_string(),
515 "verb": "brain.feedback",
516 "signal": signal,
517 "target_id": target.to_string(),
518 }))
519 }
520
521 async fn handle_emit(
526 &self,
527 token: &NamespaceToken,
528 params: Value,
529 ) -> Result<Value, RuntimeError> {
530 self.handle_feedback(token, params).await
531 }
532
533 async fn handle_bind(&self, params: Value) -> Result<Value, RuntimeError> {
536 #[derive(Deserialize)]
537 struct BindParams {
538 profile_id: String,
539 actor: Option<String>,
540 namespace: Option<String>,
541 consumer_kind: Option<String>,
542 priority: Option<i32>,
543 }
544 let p: BindParams = serde_json::from_value(params)
545 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
546
547 let mut state = self.state.lock().unwrap();
548
549 if !state.profiles.contains_key(&p.profile_id) {
551 return Err(RuntimeError::NotFound(format!(
552 "profile {:?}",
553 p.profile_id
554 )));
555 }
556
557 let actor = p.actor.unwrap_or_else(|| "*".into());
558 let namespace = p.namespace.unwrap_or_else(|| "*".into());
559 let consumer_kind = p.consumer_kind.unwrap_or_else(|| "*".into());
560
561 for (field, val) in [
563 ("actor", &actor),
564 ("namespace", &namespace),
565 ("consumer_kind", &consumer_kind),
566 ] {
567 if val.as_str() != "*" && val.contains('*') {
568 return Err(RuntimeError::InvalidInput(format!(
569 "{field}: '*' is reserved as the wildcard sentinel and cannot appear inside a real value"
570 )));
571 }
572 }
573
574 state.bindings.retain(|b| {
576 !(b.actor == actor && b.namespace == namespace && b.consumer_kind == consumer_kind)
577 });
578
579 state.bindings.push(ProfileBinding {
580 actor: actor.clone(),
581 namespace: namespace.clone(),
582 consumer_kind: consumer_kind.clone(),
583 profile_id: p.profile_id.clone(),
584 priority: p.priority.unwrap_or(0),
585 created_at: Utc::now(),
586 });
587
588 Ok(json!({
589 "bound": true,
590 "profile_id": p.profile_id,
591 "actor": actor,
592 "namespace": namespace,
593 "consumer_kind": consumer_kind,
594 }))
595 }
596
597 async fn handle_unbind(&self, params: Value) -> Result<Value, RuntimeError> {
600 #[derive(Deserialize)]
601 struct UnbindParams {
602 profile_id: Option<String>,
603 actor: Option<String>,
604 namespace: Option<String>,
605 consumer_kind: Option<String>,
606 }
607 let p: UnbindParams = serde_json::from_value(params)
608 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
609
610 let mut state = self.state.lock().unwrap();
611 let before = state.bindings.len();
612
613 state.bindings.retain(|b| {
614 let pid_match = p.profile_id.as_ref().is_none_or(|id| &b.profile_id == id);
615 let actor_match = p.actor.as_ref().is_none_or(|a| &b.actor == a);
616 let ns_match = p.namespace.as_ref().is_none_or(|n| &b.namespace == n);
617 let kind_match = p
618 .consumer_kind
619 .as_ref()
620 .is_none_or(|k| &b.consumer_kind == k);
621 !(pid_match && actor_match && ns_match && kind_match)
625 });
626
627 let removed = before - state.bindings.len();
628 Ok(json!({ "unbound": removed }))
629 }
630}
631
632struct BrainPackFactory;
635
636impl khive_runtime::PackFactory for BrainPackFactory {
637 fn name(&self) -> &'static str {
638 "brain"
639 }
640
641 fn requires(&self) -> &'static [&'static str] {
642 &["kg"]
643 }
644
645 fn create(&self, runtime: KhiveRuntime) -> Box<dyn PackRuntime> {
646 Box::new(BrainPack::new(runtime))
647 }
648}
649
650inventory::submit! { khive_runtime::PackRegistration(&BrainPackFactory) }
651
652#[async_trait]
655impl PackRuntime for BrainPack {
656 fn name(&self) -> &str {
657 <BrainPack as Pack>::NAME
658 }
659
660 fn note_kinds(&self) -> &'static [&'static str] {
661 <BrainPack as Pack>::NOTE_KINDS
662 }
663
664 fn entity_kinds(&self) -> &'static [&'static str] {
665 <BrainPack as Pack>::ENTITY_KINDS
666 }
667
668 fn handlers(&self) -> &'static [HandlerDef] {
669 BRAIN_HANDLERS
670 }
671
672 fn requires(&self) -> &'static [&'static str] {
673 <BrainPack as Pack>::REQUIRES
674 }
675
676 async fn dispatch(
677 &self,
678 verb: &str,
679 params: Value,
680 _registry: &VerbRegistry,
681 token: &NamespaceToken,
682 ) -> Result<Value, RuntimeError> {
683 match verb {
684 "brain.state" => self.handle_state(params).await,
686 "brain.config" => self.handle_config(params).await,
687 "brain.events" => self.handle_events(token, params).await,
688 "brain.profiles" => self.handle_profiles(params).await,
689 "brain.profile" => self.handle_profile(params).await,
690 "brain.resolve" => self.handle_resolve(params).await,
691 "brain.activate" => self.handle_activate(params).await,
693 "brain.deactivate" => self.handle_deactivate(params).await,
694 "brain.archive" => self.handle_archive(params).await,
695 "brain.reset" => self.handle_reset(params).await,
696 "brain.feedback" => self.handle_feedback(token, params).await,
697 "brain.bind" => self.handle_bind(params).await,
699 "brain.unbind" => self.handle_unbind(params).await,
700 "brain.emit" => self.handle_emit(token, params).await,
702 _ => Err(RuntimeError::InvalidInput(format!(
703 "brain pack does not handle verb {verb:?}"
704 ))),
705 }
706 }
707}
708
709#[async_trait]
718impl DispatchHook for BrainPack {
719 async fn on_dispatch(&self, view: &EventView) {
720 let ctx = FoldContext::new();
721 let mut state = self.state.lock().unwrap();
722 let current = std::mem::replace(
723 &mut state.balanced_recall,
724 crate::state::BalancedRecallState::new(0),
725 );
726 let updated = self.fold.reduce(current, &view.event, &ctx);
727 state.balanced_recall = updated;
728 }
729}
730
731#[cfg(test)]
734mod tests {
735 use super::*;
736 use khive_runtime::{Namespace, VerbRegistryBuilder};
737 use serde_json::json;
738
739 fn make_pack() -> (BrainPack, KhiveRuntime) {
740 let rt = KhiveRuntime::memory().expect("in-memory runtime");
741 let pack = BrainPack::new(rt.clone());
742 (pack, rt)
743 }
744
745 fn empty_registry() -> VerbRegistry {
746 VerbRegistryBuilder::new()
747 .build()
748 .expect("empty registry builds successfully")
749 }
750
751 #[tokio::test]
752 async fn dispatch_unknown_verb_returns_invalid_input() {
753 let (pack, rt) = make_pack();
754 let registry = empty_registry();
755 let err = pack
756 .dispatch(
757 "brain.unknown",
758 json!({}),
759 ®istry,
760 &rt.authorize(Namespace::local()),
761 )
762 .await
763 .unwrap_err();
764 if let RuntimeError::InvalidInput(msg) = &err {
765 assert!(
766 msg.contains("brain.unknown"),
767 "expected verb name in error: {msg}"
768 );
769 } else {
770 panic!("expected InvalidInput, got {err:?}");
771 }
772 }
773
774 #[tokio::test]
775 async fn dispatch_reset_returns_true_and_increments_epoch() {
776 let (pack, rt) = make_pack();
777 let registry = empty_registry();
778 let result = pack
779 .dispatch(
780 "brain.reset",
781 json!({}),
782 ®istry,
783 &rt.authorize(Namespace::local()),
784 )
785 .await
786 .unwrap();
787 assert_eq!(result["reset"], json!(true));
788 assert_eq!(result["exploration_epoch"], json!(1u64));
789 }
790
791 #[tokio::test]
792 async fn dispatch_feedback_invalid_signal_returns_invalid_input() {
793 let (pack, rt) = make_pack();
794 let registry = empty_registry();
795 let target = "00000000-0000-0000-0000-000000000001";
796 let err = pack
797 .dispatch(
798 "brain.feedback",
799 json!({"target_id": target, "signal": "bad_signal"}),
800 ®istry,
801 &rt.authorize(Namespace::local()),
802 )
803 .await
804 .unwrap_err();
805 if let RuntimeError::InvalidInput(msg) = &err {
806 assert!(
807 msg.contains("bad_signal"),
808 "expected signal name in error: {msg}"
809 );
810 assert!(
811 msg.contains("valid"),
812 "expected hint about valid values: {msg}"
813 );
814 } else {
815 panic!("expected InvalidInput, got {err:?}");
816 }
817 }
818
819 #[tokio::test]
820 async fn dispatch_state_returns_snapshot_fields() {
821 let (pack, rt) = make_pack();
822 let registry = empty_registry();
823 let result = pack
824 .dispatch(
825 "brain.state",
826 json!({}),
827 ®istry,
828 &rt.authorize(Namespace::local()),
829 )
830 .await
831 .unwrap();
832 assert!(result.get("profiles").is_some(), "missing profiles");
833 assert!(
834 result.get("balanced_recall").is_some(),
835 "missing balanced_recall"
836 );
837 assert!(result.get("bindings").is_some(), "missing bindings");
838 }
839
840 #[tokio::test]
841 async fn dispatch_profiles_returns_default_profile() {
842 let (pack, rt) = make_pack();
843 let registry = empty_registry();
844 let result = pack
845 .dispatch(
846 "brain.profiles",
847 json!({}),
848 ®istry,
849 &rt.authorize(Namespace::local()),
850 )
851 .await
852 .unwrap();
853 let profiles = result["profiles"].as_array().unwrap();
854 assert!(!profiles.is_empty(), "expected at least one profile");
855 assert_eq!(profiles[0]["id"], json!("balanced-recall-v1"));
856 }
857
858 #[tokio::test]
859 async fn dispatch_profiles_filtered_by_lifecycle() {
860 let (pack, rt) = make_pack();
861 let registry = empty_registry();
862 let result = pack
863 .dispatch(
864 "brain.profiles",
865 json!({"lifecycle": "active"}),
866 ®istry,
867 &rt.authorize(Namespace::local()),
868 )
869 .await
870 .unwrap();
871 let profiles = result["profiles"].as_array().unwrap();
872 for p in profiles {
873 assert_eq!(p["lifecycle"], json!("active"));
874 }
875 }
876
877 #[tokio::test]
878 async fn dispatch_profile_returns_profile_details() {
879 let (pack, rt) = make_pack();
880 let registry = empty_registry();
881 let result = pack
882 .dispatch(
883 "brain.profile",
884 json!({"id": "balanced-recall-v1"}),
885 ®istry,
886 &rt.authorize(Namespace::local()),
887 )
888 .await
889 .unwrap();
890 assert_eq!(result["id"], json!("balanced-recall-v1"));
891 assert_eq!(result["state_class"], json!("Bayesian"));
892 assert_eq!(result["consumer_kind"], json!("recall"));
893 }
894
895 #[tokio::test]
896 async fn dispatch_profile_not_found_returns_not_found() {
897 let (pack, rt) = make_pack();
898 let registry = empty_registry();
899 let err = pack
900 .dispatch(
901 "brain.profile",
902 json!({"id": "nonexistent"}),
903 ®istry,
904 &rt.authorize(Namespace::local()),
905 )
906 .await
907 .unwrap_err();
908 assert!(matches!(err, RuntimeError::NotFound(_)));
909 }
910
911 #[tokio::test]
912 async fn dispatch_resolve_returns_default_profile_for_recall() {
913 let (pack, rt) = make_pack();
914 let registry = empty_registry();
915 let result = pack
916 .dispatch(
917 "brain.resolve",
918 json!({"consumer_kind": "recall"}),
919 ®istry,
920 &rt.authorize(Namespace::local()),
921 )
922 .await
923 .unwrap();
924 assert_eq!(result["resolved_profile_id"], json!("balanced-recall-v1"));
925 }
926
927 #[tokio::test]
928 async fn dispatch_activate_and_deactivate_profile() {
929 let (pack, rt) = make_pack();
930 let registry = empty_registry();
931 let token = rt.authorize(Namespace::local());
932
933 let result = pack
935 .dispatch(
936 "brain.deactivate",
937 json!({"profile_id": "balanced-recall-v1"}),
938 ®istry,
939 &token,
940 )
941 .await
942 .unwrap();
943 assert_eq!(result["lifecycle"], json!("inactive"));
944
945 let state = pack
947 .dispatch(
948 "brain.profile",
949 json!({"id": "balanced-recall-v1"}),
950 ®istry,
951 &token,
952 )
953 .await
954 .unwrap();
955 assert_eq!(state["lifecycle"], json!("inactive"));
956
957 let result = pack
959 .dispatch(
960 "brain.activate",
961 json!({"profile_id": "balanced-recall-v1"}),
962 ®istry,
963 &token,
964 )
965 .await
966 .unwrap();
967 assert_eq!(result["lifecycle"], json!("active"));
968 }
969
970 #[tokio::test]
971 async fn dispatch_archive_profile() {
972 let (pack, rt) = make_pack();
973 let registry = empty_registry();
974 let result = pack
975 .dispatch(
976 "brain.archive",
977 json!({"profile_id": "balanced-recall-v1"}),
978 ®istry,
979 &rt.authorize(Namespace::local()),
980 )
981 .await
982 .unwrap();
983 assert_eq!(result["lifecycle"], json!("archived"));
984 }
985
986 #[tokio::test]
987 async fn dispatch_activate_nonexistent_profile_returns_not_found() {
988 let (pack, rt) = make_pack();
989 let registry = empty_registry();
990 let err = pack
991 .dispatch(
992 "brain.activate",
993 json!({"profile_id": "ghost-profile"}),
994 ®istry,
995 &rt.authorize(Namespace::local()),
996 )
997 .await
998 .unwrap_err();
999 assert!(matches!(err, RuntimeError::NotFound(_)));
1000 }
1001
1002 #[tokio::test]
1003 async fn dispatch_bind_and_resolve_explicit_binding() {
1004 let (pack, rt) = make_pack();
1005 let registry = empty_registry();
1006 let token = rt.authorize(Namespace::local());
1007
1008 let result = pack
1010 .dispatch(
1011 "brain.bind",
1012 json!({
1013 "profile_id": "balanced-recall-v1",
1014 "actor": "agent-x",
1015 "consumer_kind": "recall"
1016 }),
1017 ®istry,
1018 &token,
1019 )
1020 .await
1021 .unwrap();
1022 assert_eq!(result["bound"], json!(true));
1023 assert_eq!(result["actor"], json!("agent-x"));
1024
1025 let resolved = pack
1027 .dispatch(
1028 "brain.resolve",
1029 json!({"actor": "agent-x", "consumer_kind": "recall"}),
1030 ®istry,
1031 &token,
1032 )
1033 .await
1034 .unwrap();
1035 assert_eq!(resolved["resolved_profile_id"], json!("balanced-recall-v1"));
1036 }
1037
1038 #[tokio::test]
1039 async fn dispatch_bind_nonexistent_profile_returns_not_found() {
1040 let (pack, rt) = make_pack();
1041 let registry = empty_registry();
1042 let err = pack
1043 .dispatch(
1044 "brain.bind",
1045 json!({"profile_id": "ghost", "consumer_kind": "recall"}),
1046 ®istry,
1047 &rt.authorize(Namespace::local()),
1048 )
1049 .await
1050 .unwrap_err();
1051 assert!(matches!(err, RuntimeError::NotFound(_)));
1052 }
1053
1054 #[tokio::test]
1055 async fn dispatch_unbind_removes_binding() {
1056 let (pack, rt) = make_pack();
1057 let registry = empty_registry();
1058 let token = rt.authorize(Namespace::local());
1059
1060 pack.dispatch(
1062 "brain.bind",
1063 json!({"profile_id": "balanced-recall-v1", "actor": "agent-y", "consumer_kind": "recall"}),
1064 ®istry,
1065 &token,
1066 )
1067 .await
1068 .unwrap();
1069
1070 let result = pack
1072 .dispatch(
1073 "brain.unbind",
1074 json!({"actor": "agent-y"}),
1075 ®istry,
1076 &token,
1077 )
1078 .await
1079 .unwrap();
1080 assert_eq!(result["unbound"], json!(1u64));
1081 }
1082
1083 #[tokio::test]
1086 async fn dispatch_unbind_uses_and_not_or() {
1087 let (pack, rt) = make_pack();
1088 let registry = empty_registry();
1089 let token = rt.authorize(Namespace::local());
1090
1091 pack.dispatch(
1093 "brain.bind",
1094 json!({"profile_id": "balanced-recall-v1", "namespace": "ns-a", "consumer_kind": "recall"}),
1095 ®istry,
1096 &token,
1097 )
1098 .await
1099 .unwrap();
1100
1101 pack.dispatch(
1103 "brain.bind",
1104 json!({"profile_id": "balanced-recall-v1", "namespace": "ns-b", "consumer_kind": "recall"}),
1105 ®istry,
1106 &token,
1107 )
1108 .await
1109 .unwrap();
1110
1111 let result = pack
1113 .dispatch(
1114 "brain.unbind",
1115 json!({"namespace": "ns-a", "profile_id": "balanced-recall-v1"}),
1116 ®istry,
1117 &token,
1118 )
1119 .await
1120 .unwrap();
1121 assert_eq!(
1122 result["unbound"],
1123 json!(1u64),
1124 "should remove exactly one binding"
1125 );
1126
1127 let state = pack.state.lock().unwrap();
1129 let remaining: Vec<_> = state
1130 .bindings
1131 .iter()
1132 .filter(|b| b.namespace == "ns-b")
1133 .collect();
1134 assert_eq!(remaining.len(), 1, "ns-b binding must survive the unbind");
1135 }
1136
1137 #[tokio::test]
1138 async fn dispatch_config_all_parameters() {
1139 let (pack, rt) = make_pack();
1140 let registry = empty_registry();
1141 let result = pack
1142 .dispatch(
1143 "brain.config",
1144 json!({}),
1145 ®istry,
1146 &rt.authorize(Namespace::local()),
1147 )
1148 .await
1149 .unwrap();
1150 let obj = result.as_object().unwrap();
1151 assert!(obj.contains_key("recall::relevance_weight"));
1152 assert!(obj.contains_key("recall::importance_weight"));
1153 assert!(obj.contains_key("recall::temporal_weight"));
1154 }
1155
1156 #[tokio::test]
1157 async fn dispatch_config_single_parameter() {
1158 let (pack, rt) = make_pack();
1159 let registry = empty_registry();
1160 let result = pack
1161 .dispatch(
1162 "brain.config",
1163 json!({"parameter": "recall::relevance_weight"}),
1164 ®istry,
1165 &rt.authorize(Namespace::local()),
1166 )
1167 .await
1168 .unwrap();
1169 assert_eq!(result["parameter"], json!("recall::relevance_weight"));
1170 let mean = result["mean"].as_f64().unwrap();
1172 assert!((mean - 0.7).abs() < 1e-6);
1173 }
1174}