pe-core 0.1.0

Core types for Potential Expectations — messages, channels, state, traits
Documentation
//! Lobe registry — manages a collection of cognitive lobes.
//!
//! The registry holds all lobes available to a cognitive graph.
//! It provides filtering by activation state and sorting by priority
//! for the graph builder to wire lobes into the Pregel engine.

use std::collections::HashMap;
use std::sync::Arc;

use crate::cognitive::CognitiveState;
use crate::lobe::{Lobe, LobeContext};

/// Registry of cognitive lobes, keyed by name.
///
/// # Example
///
/// ```ignore
/// let mut registry = LobeRegistry::new();
/// registry.register(Arc::new(AnalystLobe::new(provider.clone())));
/// registry.register(Arc::new(CriticLobe::new(provider.clone())));
/// assert_eq!(registry.len(), 2);
/// ```
pub struct LobeRegistry {
    lobes: HashMap<String, Arc<dyn Lobe>>,
}

// Compile-time assertion: LobeRegistry must be Send + Sync for Arc sharing
const _: () = {
    fn _assert_send_sync<T: Send + Sync>() {}
    fn _check() {
        _assert_send_sync::<LobeRegistry>();
    }
};

impl LobeRegistry {
    /// Create an empty registry.
    pub fn new() -> Self {
        Self {
            lobes: HashMap::new(),
        }
    }

    /// Register a lobe. Replaces any existing lobe with the same name.
    pub fn register(&mut self, lobe: Arc<dyn Lobe>) -> &mut Self {
        self.lobes.insert(lobe.name().to_string(), lobe);
        self
    }

    /// Remove a lobe by name. Returns the removed lobe if it existed.
    pub fn unregister(&mut self, name: &str) -> Option<Arc<dyn Lobe>> {
        self.lobes.remove(name)
    }

    /// Get a lobe by name.
    pub fn get(&self, name: &str) -> Option<&Arc<dyn Lobe>> {
        self.lobes.get(name)
    }

    /// Whether a lobe with this name is registered.
    pub fn contains(&self, name: &str) -> bool {
        self.lobes.contains_key(name)
    }

    /// Number of registered lobes.
    pub fn len(&self) -> usize {
        self.lobes.len()
    }

    /// Whether the registry is empty.
    pub fn is_empty(&self) -> bool {
        self.lobes.is_empty()
    }

    /// All registered lobes, in no particular order.
    pub fn all(&self) -> Vec<&Arc<dyn Lobe>> {
        self.lobes.values().collect()
    }

    /// All registered lobes sorted by priority (lower = first).
    pub fn sorted_by_priority(&self) -> Vec<&Arc<dyn Lobe>> {
        let mut lobes: Vec<_> = self.lobes.values().collect();
        lobes.sort_by_key(|l| l.priority());
        lobes
    }

    /// Lobes that would activate for the given state.
    ///
    /// Builds a [`LobeContext`] from state, then filters by `should_activate()`.
    /// Returns in priority order (lower = first).
    pub fn active_lobes(&self, state: &CognitiveState) -> Vec<&Arc<dyn Lobe>> {
        let context = LobeContext::from_cognitive_state(state);
        self.active_lobes_for_context(&context)
    }

    /// Lobes that would activate for the given context.
    ///
    /// Returns in priority order (lower = first).
    pub fn active_lobes_for_context(&self, context: &LobeContext) -> Vec<&Arc<dyn Lobe>> {
        let mut active: Vec<_> = self
            .lobes
            .values()
            .filter(|l| l.should_activate(context))
            .collect();
        active.sort_by_key(|l| l.priority());
        active
    }

    /// Consume the registry into a vec of (name, lobe) pairs.
    pub fn into_lobes(self) -> Vec<(String, Arc<dyn Lobe>)> {
        self.lobes.into_iter().collect()
    }
}

impl Default for LobeRegistry {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::lobe::{LobeBudget, LobeFuture, LobeInput, LobeOutput, LobeOutputFormat};

    struct TestLobe {
        name: &'static str,
        priority: u32,
        active: bool,
    }

    impl Lobe for TestLobe {
        fn name(&self) -> &str {
            self.name
        }
        fn should_activate(&self, _: &LobeContext) -> bool {
            self.active
        }
        fn priority(&self) -> u32 {
            self.priority
        }
        fn budget(&self) -> LobeBudget {
            LobeBudget::default()
        }
        fn output_format(&self) -> LobeOutputFormat {
            LobeOutputFormat::FreeText
        }
        fn process(&self, _: &LobeInput) -> LobeFuture {
            Box::pin(async { Ok(LobeOutput::new("test", 0.5)) })
        }
    }

    #[test]
    fn test_register_and_get() {
        let mut reg = LobeRegistry::new();
        reg.register(Arc::new(TestLobe {
            name: "analyst",
            priority: 10,
            active: true,
        }));
        assert_eq!(reg.len(), 1);
        assert!(reg.contains("analyst"));
        assert!(reg.get("analyst").is_some());
        assert!(reg.get("nonexistent").is_none());
    }

    #[test]
    fn test_register_replaces_duplicate() {
        let mut reg = LobeRegistry::new();
        reg.register(Arc::new(TestLobe {
            name: "critic",
            priority: 10,
            active: true,
        }));
        reg.register(Arc::new(TestLobe {
            name: "critic",
            priority: 20,
            active: true,
        }));
        assert_eq!(reg.len(), 1);
        assert_eq!(reg.get("critic").unwrap().priority(), 20);
    }

    #[test]
    fn test_unregister() {
        let mut reg = LobeRegistry::new();
        reg.register(Arc::new(TestLobe {
            name: "analyst",
            priority: 10,
            active: true,
        }));
        let removed = reg.unregister("analyst");
        assert!(removed.is_some());
        assert!(reg.is_empty());
        assert!(reg.unregister("analyst").is_none());
    }

    #[test]
    fn test_sorted_by_priority() {
        let mut reg = LobeRegistry::new();
        reg.register(Arc::new(TestLobe {
            name: "low",
            priority: 30,
            active: true,
        }));
        reg.register(Arc::new(TestLobe {
            name: "high",
            priority: 5,
            active: true,
        }));
        reg.register(Arc::new(TestLobe {
            name: "mid",
            priority: 15,
            active: true,
        }));

        let sorted = reg.sorted_by_priority();
        assert_eq!(sorted[0].name(), "high");
        assert_eq!(sorted[1].name(), "mid");
        assert_eq!(sorted[2].name(), "low");
    }

    #[test]
    fn test_active_lobes_filters_inactive() {
        let mut reg = LobeRegistry::new();
        reg.register(Arc::new(TestLobe {
            name: "always_on",
            priority: 10,
            active: true,
        }));
        reg.register(Arc::new(TestLobe {
            name: "dormant",
            priority: 5,
            active: false,
        }));

        let state = CognitiveState::default();
        let active = reg.active_lobes(&state);
        assert_eq!(active.len(), 1);
        assert_eq!(active[0].name(), "always_on");
    }

    #[test]
    fn test_active_lobes_sorted_by_priority() {
        let mut reg = LobeRegistry::new();
        reg.register(Arc::new(TestLobe {
            name: "second",
            priority: 20,
            active: true,
        }));
        reg.register(Arc::new(TestLobe {
            name: "first",
            priority: 5,
            active: true,
        }));

        let state = CognitiveState::default();
        let active = reg.active_lobes(&state);
        assert_eq!(active[0].name(), "first");
        assert_eq!(active[1].name(), "second");
    }

    #[test]
    fn test_empty_registry() {
        let reg = LobeRegistry::new();
        assert!(reg.is_empty());
        assert_eq!(reg.len(), 0);
        assert!(reg.all().is_empty());
        assert!(reg.sorted_by_priority().is_empty());
    }

    #[test]
    fn test_into_lobes() {
        let mut reg = LobeRegistry::new();
        reg.register(Arc::new(TestLobe {
            name: "a",
            priority: 10,
            active: true,
        }));
        reg.register(Arc::new(TestLobe {
            name: "b",
            priority: 20,
            active: true,
        }));

        let pairs = reg.into_lobes();
        assert_eq!(pairs.len(), 2);
    }
}