Skip to main content

ringkernel_core/
registry.rs

1//! Named Actor Registry — FR-002
2//!
3//! Global actor registry with symbolic names for service discovery.
4//! Actors register by name; callers look up actors by name or wildcard pattern.
5//!
6//! # Usage
7//!
8//! ```ignore
9//! use ringkernel_core::registry::ActorRegistry;
10//! use ringkernel_core::runtime::KernelId;
11//!
12//! let mut registry = ActorRegistry::new();
13//!
14//! // Register actors by name
15//! registry.register("isa_ontology", KernelId::new("kernel_1"));
16//! registry.register("pcaob_rules", KernelId::new("kernel_2"));
17//! registry.register("standards/isa/500", KernelId::new("kernel_3"));
18//!
19//! // Lookup by exact name
20//! let actor = registry.lookup("isa_ontology"); // Some(KernelId("kernel_1"))
21//!
22//! // Lookup by wildcard
23//! let actors = registry.lookup_pattern("standards/*"); // ["standards/isa/500"]
24//! ```
25
26use std::collections::HashMap;
27use std::time::Instant;
28
29use crate::runtime::KernelId;
30
31/// A named actor registration entry.
32#[derive(Debug, Clone)]
33pub struct RegistryEntry {
34    /// Symbolic name.
35    pub name: String,
36    /// The kernel ID this name resolves to.
37    pub kernel_id: KernelId,
38    /// When the registration was created.
39    pub registered_at: Instant,
40    /// Metadata tags.
41    pub tags: HashMap<String, String>,
42}
43
44/// Event emitted when the registry changes.
45#[derive(Debug, Clone)]
46pub enum RegistryEvent {
47    /// A new actor was registered.
48    Registered {
49        /// Symbolic name of the registered actor.
50        name: String,
51        /// Kernel ID assigned to the actor.
52        kernel_id: KernelId,
53    },
54    /// An actor was deregistered.
55    Deregistered {
56        /// Symbolic name of the deregistered actor.
57        name: String,
58        /// Kernel ID that was removed.
59        kernel_id: KernelId,
60    },
61    /// An actor's registration was updated.
62    Updated {
63        /// Symbolic name of the updated actor.
64        name: String,
65        /// Previous kernel ID.
66        old_kernel_id: KernelId,
67        /// New kernel ID.
68        new_kernel_id: KernelId,
69    },
70}
71
72/// Global actor registry with symbolic names.
73///
74/// Provides service discovery for the actor system: LLM tool calls,
75/// external APIs, and inter-actor routing can find actors by name
76/// instead of opaque kernel IDs.
77pub struct ActorRegistry {
78    /// Name → Entry mapping.
79    entries: HashMap<String, RegistryEntry>,
80    /// Reverse mapping: KernelId → names (one kernel can have multiple names).
81    reverse: HashMap<KernelId, Vec<String>>,
82    /// Watchers: pattern → callback channels.
83    watchers: Vec<(String, Vec<RegistryEvent>)>,
84}
85
86impl ActorRegistry {
87    /// Create an empty registry.
88    pub fn new() -> Self {
89        Self {
90            entries: HashMap::new(),
91            reverse: HashMap::new(),
92            watchers: Vec::new(),
93        }
94    }
95
96    /// Register an actor by name.
97    ///
98    /// If the name is already registered, updates the mapping and
99    /// emits an `Updated` event.
100    pub fn register(&mut self, name: impl Into<String>, kernel_id: KernelId) -> RegistryEvent {
101        let name = name.into();
102
103        let event = if let Some(existing) = self.entries.get(&name) {
104            let old_id = existing.kernel_id.clone();
105            // Remove from old reverse mapping
106            if let Some(names) = self.reverse.get_mut(&old_id) {
107                names.retain(|n| n != &name);
108            }
109            RegistryEvent::Updated {
110                name: name.clone(),
111                old_kernel_id: old_id,
112                new_kernel_id: kernel_id.clone(),
113            }
114        } else {
115            RegistryEvent::Registered {
116                name: name.clone(),
117                kernel_id: kernel_id.clone(),
118            }
119        };
120
121        // Add to reverse mapping
122        self.reverse
123            .entry(kernel_id.clone())
124            .or_default()
125            .push(name.clone());
126
127        self.entries.insert(
128            name.clone(),
129            RegistryEntry {
130                name,
131                kernel_id,
132                registered_at: Instant::now(),
133                tags: HashMap::new(),
134            },
135        );
136
137        self.notify_watchers(&event);
138        event
139    }
140
141    /// Register with metadata tags.
142    pub fn register_with_tags(
143        &mut self,
144        name: impl Into<String>,
145        kernel_id: KernelId,
146        tags: HashMap<String, String>,
147    ) -> RegistryEvent {
148        let event = self.register(name, kernel_id);
149        if let RegistryEvent::Registered { ref name, .. }
150        | RegistryEvent::Updated { ref name, .. } = event
151        {
152            if let Some(entry) = self.entries.get_mut(name) {
153                entry.tags = tags;
154            }
155        }
156        event
157    }
158
159    /// Deregister an actor by name.
160    pub fn deregister(&mut self, name: &str) -> Option<RegistryEvent> {
161        if let Some(entry) = self.entries.remove(name) {
162            // Remove from reverse mapping
163            if let Some(names) = self.reverse.get_mut(&entry.kernel_id) {
164                names.retain(|n| n != name);
165                if names.is_empty() {
166                    self.reverse.remove(&entry.kernel_id);
167                }
168            }
169
170            let event = RegistryEvent::Deregistered {
171                name: name.to_string(),
172                kernel_id: entry.kernel_id,
173            };
174            self.notify_watchers(&event);
175            Some(event)
176        } else {
177            None
178        }
179    }
180
181    /// Deregister all names associated with a kernel ID.
182    pub fn deregister_kernel(&mut self, kernel_id: &KernelId) -> Vec<RegistryEvent> {
183        let mut events = Vec::new();
184        if let Some(names) = self.reverse.remove(kernel_id) {
185            for name in names {
186                if let Some(entry) = self.entries.remove(&name) {
187                    events.push(RegistryEvent::Deregistered {
188                        name,
189                        kernel_id: entry.kernel_id,
190                    });
191                }
192            }
193        }
194        for event in &events {
195            self.notify_watchers(event);
196        }
197        events
198    }
199
200    /// Lookup an actor by exact name.
201    pub fn lookup(&self, name: &str) -> Option<&KernelId> {
202        self.entries.get(name).map(|e| &e.kernel_id)
203    }
204
205    /// Lookup the full registry entry by name.
206    pub fn lookup_entry(&self, name: &str) -> Option<&RegistryEntry> {
207        self.entries.get(name)
208    }
209
210    /// Lookup all names registered to a kernel ID.
211    pub fn names_for(&self, kernel_id: &KernelId) -> Vec<&str> {
212        self.reverse
213            .get(kernel_id)
214            .map(|names| names.iter().map(String::as_str).collect())
215            .unwrap_or_default()
216    }
217
218    /// Lookup actors by wildcard pattern.
219    ///
220    /// Supports:
221    /// - `*` matches any sequence of characters (excluding `/`)
222    /// - `**` matches any sequence including `/`
223    /// - `?` matches a single character
224    ///
225    /// Examples:
226    /// - `"standards/*"` matches `"standards/isa"` but not `"standards/isa/500"`
227    /// - `"standards/**"` matches `"standards/isa/500"`
228    /// - `"isa_*"` matches `"isa_ontology"`, `"isa_rules"`
229    pub fn lookup_pattern(&self, pattern: &str) -> Vec<(&str, &KernelId)> {
230        self.entries
231            .iter()
232            .filter(|(name, _)| wildcard_match(pattern, name))
233            .map(|(name, entry)| (name.as_str(), &entry.kernel_id))
234            .collect()
235    }
236
237    /// List all registered names.
238    pub fn list_names(&self) -> Vec<&str> {
239        self.entries.keys().map(String::as_str).collect()
240    }
241
242    /// Number of registered entries.
243    pub fn len(&self) -> usize {
244        self.entries.len()
245    }
246
247    /// Check if registry is empty.
248    pub fn is_empty(&self) -> bool {
249        self.entries.is_empty()
250    }
251
252    /// Add a watcher for registry changes.
253    ///
254    /// Returns a watcher ID that can be used to retrieve events.
255    pub fn watch(&mut self, pattern: impl Into<String>) -> usize {
256        let id = self.watchers.len();
257        self.watchers.push((pattern.into(), Vec::new()));
258        id
259    }
260
261    /// Drain events for a watcher.
262    pub fn drain_events(&mut self, watcher_id: usize) -> Vec<RegistryEvent> {
263        if let Some((_, events)) = self.watchers.get_mut(watcher_id) {
264            std::mem::take(events)
265        } else {
266            Vec::new()
267        }
268    }
269
270    fn notify_watchers(&mut self, event: &RegistryEvent) {
271        let name = match event {
272            RegistryEvent::Registered { name, .. } => name,
273            RegistryEvent::Deregistered { name, .. } => name,
274            RegistryEvent::Updated { name, .. } => name,
275        };
276
277        for (pattern, events) in &mut self.watchers {
278            if wildcard_match(pattern, name) {
279                events.push(event.clone());
280            }
281        }
282    }
283}
284
285impl Default for ActorRegistry {
286    fn default() -> Self {
287        Self::new()
288    }
289}
290
291/// Simple wildcard pattern matching.
292///
293/// `*` matches any characters except `/`.
294/// `**` matches any characters including `/`.
295/// `?` matches a single character.
296fn wildcard_match(pattern: &str, text: &str) -> bool {
297    let p = pattern.chars().collect::<Vec<_>>();
298    let t = text.chars().collect::<Vec<_>>();
299
300    wildcard_match_recursive(&p, &t, 0, 0)
301}
302
303fn wildcard_match_recursive(pattern: &[char], text: &[char], pi: usize, ti: usize) -> bool {
304    if pi == pattern.len() && ti == text.len() {
305        return true;
306    }
307    if pi == pattern.len() {
308        return false;
309    }
310
311    // Check for ** (matches everything including /)
312    if pi + 1 < pattern.len() && pattern[pi] == '*' && pattern[pi + 1] == '*' {
313        // Try matching ** against 0 or more characters
314        for i in ti..=text.len() {
315            if wildcard_match_recursive(pattern, text, pi + 2, i) {
316                return true;
317            }
318        }
319        return false;
320    }
321
322    // Check for * (matches everything except /)
323    if pattern[pi] == '*' {
324        for i in ti..=text.len() {
325            if i > ti && i <= text.len() && text[i - 1] == '/' {
326                // * doesn't cross /
327                break;
328            }
329            if wildcard_match_recursive(pattern, text, pi + 1, i) {
330                return true;
331            }
332        }
333        return false;
334    }
335
336    // Check for ? (matches any single char)
337    if pattern[pi] == '?' && ti < text.len() {
338        return wildcard_match_recursive(pattern, text, pi + 1, ti + 1);
339    }
340
341    // Exact character match
342    if ti < text.len() && pattern[pi] == text[ti] {
343        return wildcard_match_recursive(pattern, text, pi + 1, ti + 1);
344    }
345
346    false
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn test_register_and_lookup() {
355        let mut reg = ActorRegistry::new();
356
357        reg.register("isa_ontology", KernelId::new("k1"));
358        reg.register("pcaob_rules", KernelId::new("k2"));
359
360        assert_eq!(reg.lookup("isa_ontology"), Some(&KernelId::new("k1")));
361        assert_eq!(reg.lookup("pcaob_rules"), Some(&KernelId::new("k2")));
362        assert_eq!(reg.lookup("nonexistent"), None);
363    }
364
365    #[test]
366    fn test_deregister() {
367        let mut reg = ActorRegistry::new();
368        reg.register("actor_a", KernelId::new("k1"));
369        assert_eq!(reg.len(), 1);
370
371        reg.deregister("actor_a");
372        assert_eq!(reg.len(), 0);
373        assert_eq!(reg.lookup("actor_a"), None);
374    }
375
376    #[test]
377    fn test_update_registration() {
378        let mut reg = ActorRegistry::new();
379        reg.register("my_actor", KernelId::new("k1"));
380        let event = reg.register("my_actor", KernelId::new("k2"));
381
382        assert!(matches!(event, RegistryEvent::Updated { .. }));
383        assert_eq!(reg.lookup("my_actor"), Some(&KernelId::new("k2")));
384    }
385
386    #[test]
387    fn test_reverse_lookup() {
388        let mut reg = ActorRegistry::new();
389        reg.register("name_a", KernelId::new("k1"));
390        reg.register("name_b", KernelId::new("k1"));
391
392        let names = reg.names_for(&KernelId::new("k1"));
393        assert_eq!(names.len(), 2);
394        assert!(names.contains(&"name_a"));
395        assert!(names.contains(&"name_b"));
396    }
397
398    #[test]
399    fn test_wildcard_exact() {
400        assert!(wildcard_match("hello", "hello"));
401        assert!(!wildcard_match("hello", "world"));
402    }
403
404    #[test]
405    fn test_wildcard_star() {
406        assert!(wildcard_match("isa_*", "isa_ontology"));
407        assert!(wildcard_match("isa_*", "isa_rules"));
408        assert!(!wildcard_match("isa_*", "pcaob_rules"));
409    }
410
411    #[test]
412    fn test_wildcard_star_no_slash() {
413        assert!(wildcard_match("standards/*", "standards/isa"));
414        assert!(!wildcard_match("standards/*", "standards/isa/500"));
415    }
416
417    #[test]
418    fn test_wildcard_double_star() {
419        assert!(wildcard_match("standards/**", "standards/isa/500"));
420        assert!(wildcard_match("standards/**", "standards/isa"));
421        assert!(!wildcard_match("standards/**", "other/isa"));
422    }
423
424    #[test]
425    fn test_wildcard_question() {
426        assert!(wildcard_match("actor_?", "actor_a"));
427        assert!(wildcard_match("actor_?", "actor_b"));
428        assert!(!wildcard_match("actor_?", "actor_ab"));
429    }
430
431    #[test]
432    fn test_pattern_lookup() {
433        let mut reg = ActorRegistry::new();
434        reg.register("standards/isa/500", KernelId::new("k1"));
435        reg.register("standards/isa/700", KernelId::new("k2"));
436        reg.register("standards/pcaob/101", KernelId::new("k3"));
437        reg.register("other/thing", KernelId::new("k4"));
438
439        let isa = reg.lookup_pattern("standards/isa/*");
440        assert_eq!(isa.len(), 2);
441
442        let all_standards = reg.lookup_pattern("standards/**");
443        assert_eq!(all_standards.len(), 3);
444    }
445
446    #[test]
447    fn test_watcher() {
448        let mut reg = ActorRegistry::new();
449        let watcher = reg.watch("isa_*");
450
451        reg.register("isa_ontology", KernelId::new("k1"));
452        reg.register("pcaob_rules", KernelId::new("k2")); // Shouldn't trigger
453        reg.register("isa_rules", KernelId::new("k3"));
454
455        let events = reg.drain_events(watcher);
456        assert_eq!(events.len(), 2); // Only isa_* matches
457    }
458
459    #[test]
460    fn test_deregister_kernel() {
461        let mut reg = ActorRegistry::new();
462        reg.register("name_a", KernelId::new("k1"));
463        reg.register("name_b", KernelId::new("k1"));
464        reg.register("name_c", KernelId::new("k2"));
465
466        let events = reg.deregister_kernel(&KernelId::new("k1"));
467        assert_eq!(events.len(), 2);
468        assert_eq!(reg.len(), 1);
469        assert_eq!(reg.lookup("name_c"), Some(&KernelId::new("k2")));
470    }
471
472    #[test]
473    fn test_register_with_tags() {
474        let mut reg = ActorRegistry::new();
475        let mut tags = HashMap::new();
476        tags.insert("domain".to_string(), "audit".to_string());
477        tags.insert("version".to_string(), "2.0".to_string());
478
479        reg.register_with_tags("isa_ontology", KernelId::new("k1"), tags);
480
481        let entry = reg.lookup_entry("isa_ontology").unwrap();
482        assert_eq!(entry.tags.get("domain").unwrap(), "audit");
483    }
484}