Skip to main content

luft_core/scheduler/
registry.rs

1//! Backend registry (§2.2): backend id → implementation. Registered before a
2//! run, read-only during it.
3
4use super::error::SchedulerError;
5use crate::contract::backend::AgentBackend;
6use std::collections::HashMap;
7use std::sync::Arc;
8
9#[derive(Clone, Default)]
10pub struct BackendRegistry {
11    backends: HashMap<&'static str, Arc<dyn AgentBackend>>,
12    /// The backend used when an agent omits `backend`. Set automatically to
13    /// the first-registered backend, or explicitly via [`with_default`] /
14    /// [`set_default`].
15    ///
16    /// [`with_default`]: Self::with_default
17    /// [`set_default`]: Self::set_default
18    default_id: Option<&'static str>,
19}
20
21impl std::fmt::Debug for BackendRegistry {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        f.debug_struct("BackendRegistry")
24            .field("backend_ids", &self.backends.keys().collect::<Vec<_>>())
25            .field("default_id", &self.default_id)
26            .finish()
27    }
28}
29
30impl BackendRegistry {
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    pub fn register(&mut self, backend: Arc<dyn AgentBackend>) {
36        let id = backend.id();
37        tracing::debug!(id, "registering backend");
38        // First-registered becomes the default so single-backend callers
39        // using `with()` keep working without an explicit `with_default()`.
40        if self.default_id.is_none() {
41            self.default_id = Some(id);
42        }
43        self.backends.insert(id, backend);
44    }
45
46    /// Builder-style registration. The first registered backend becomes the
47    /// default unless [`with_default`](Self::with_default) is used.
48    pub fn with(mut self, backend: Arc<dyn AgentBackend>) -> Self {
49        self.register(backend);
50        self
51    }
52
53    /// Register a backend AND mark it as the default (the backend used when an
54    /// agent omits `backend`). Overrides any previously-set default.
55    pub fn with_default(mut self, backend: Arc<dyn AgentBackend>) -> Self {
56        let id = backend.id();
57        self.backends.insert(id, backend);
58        self.default_id = Some(id);
59        self
60    }
61
62    /// Mark an already-registered backend id as the default.
63    pub fn set_default(&mut self, id: &str) -> Result<(), SchedulerError> {
64        match self.backends.get_key_value(id) {
65            Some((key, _)) => {
66                self.default_id = Some(*key);
67                Ok(())
68            }
69            None => Err(SchedulerError::UnknownBackend(id.to_owned())),
70        }
71    }
72
73    pub fn get(&self, id: &str) -> Result<Arc<dyn AgentBackend>, SchedulerError> {
74        self.backends.get(id).cloned().ok_or_else(|| {
75            tracing::error!(id, "backend not found in registry");
76            SchedulerError::UnknownBackend(id.to_owned())
77        })
78    }
79
80    /// The default backend (used when an agent omits `backend`). Deterministic:
81    /// returns the backend marked as default (first-registered, or whichever
82    /// was set via [`with_default`](Self::with_default) /
83    /// [`set_default`](Self::set_default)).
84    pub fn default_backend(&self) -> Result<Arc<dyn AgentBackend>, SchedulerError> {
85        match self.default_id {
86            Some(id) => self.backends.get(id).cloned().ok_or_else(|| {
87                tracing::error!(default_id = id, "default backend not registered");
88                SchedulerError::NoBackendRegistered
89            }),
90            None => Err(SchedulerError::NoBackendRegistered),
91        }
92    }
93
94    /// The default backend id, if any.
95    pub fn default_id(&self) -> Option<&'static str> {
96        self.default_id
97    }
98
99    /// Sorted list of all registered backend ids.
100    pub fn available_ids(&self) -> Vec<&'static str> {
101        let mut ids: Vec<&'static str> = self.backends.keys().copied().collect();
102        ids.sort_unstable();
103        ids
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::contract::*;
111    use async_trait::async_trait;
112
113    /// Minimal stub backend for registry tests.
114    struct TestBackend {
115        id: &'static str,
116    }
117
118    #[async_trait]
119    impl AgentBackend for TestBackend {
120        fn id(&self) -> &'static str {
121            self.id
122        }
123        fn capabilities(&self) -> AgentCapabilities {
124            AgentCapabilities::default()
125        }
126        fn as_any(&self) -> &dyn std::any::Any {
127            self
128        }
129        async fn run(
130            &self,
131            _task: AgentTask,
132            _ctx: RunContext,
133        ) -> Result<AgentResult, BackendError> {
134            unimplemented!("registry tests never invoke run()")
135        }
136    }
137
138    fn make_backend(id: &'static str) -> Arc<dyn AgentBackend> {
139        Arc::new(TestBackend { id })
140    }
141
142    // ── construction ────────────────────────────────────────────
143
144    #[test]
145    fn test_new_is_empty() {
146        let reg = BackendRegistry::new();
147        assert!(matches!(
148            reg.default_backend(),
149            Err(SchedulerError::NoBackendRegistered)
150        ));
151    }
152
153    #[test]
154    fn test_default_is_empty() {
155        let reg = BackendRegistry::default();
156        assert!(matches!(
157            reg.default_backend(),
158            Err(SchedulerError::NoBackendRegistered)
159        ));
160    }
161
162    // ── register / with ────────────────────────────────────────
163
164    #[test]
165    fn test_register_adds_backend() {
166        let mut reg = BackendRegistry::new();
167        reg.register(make_backend("alpha"));
168        assert_eq!(reg.get("alpha").unwrap().id(), "alpha");
169    }
170
171    #[test]
172    fn test_with_builder_pattern() {
173        let reg = BackendRegistry::new()
174            .with(make_backend("a"))
175            .with(make_backend("b"))
176            .with(make_backend("c"));
177        assert_eq!(reg.get("a").unwrap().id(), "a");
178        assert_eq!(reg.get("b").unwrap().id(), "b");
179        assert_eq!(reg.get("c").unwrap().id(), "c");
180    }
181
182    #[test]
183    fn test_register_overwrites_existing_id() {
184        let mut reg = BackendRegistry::new();
185        reg.register(make_backend("dup"));
186        reg.register(make_backend("dup"));
187        assert_eq!(reg.get("dup").unwrap().id(), "dup");
188        // Still only one entry.
189        assert!(reg.get("other").is_err());
190    }
191
192    // ── get ────────────────────────────────────────────────────
193
194    #[test]
195    fn test_get_unknown_backend() {
196        let reg = BackendRegistry::new();
197        assert!(matches!(
198            reg.get("nonexistent"),
199            Err(SchedulerError::UnknownBackend(_))
200        ));
201    }
202
203    #[test]
204    fn test_get_after_register() {
205        let mut reg = BackendRegistry::new();
206        reg.register(make_backend("x"));
207        assert_eq!(reg.get("x").unwrap().id(), "x");
208    }
209
210    #[test]
211    fn test_get_multiple_backends() {
212        let mut reg = BackendRegistry::new();
213        reg.register(make_backend("first"));
214        reg.register(make_backend("second"));
215        reg.register(make_backend("third"));
216        assert_eq!(reg.get("first").unwrap().id(), "first");
217        assert_eq!(reg.get("second").unwrap().id(), "second");
218        assert_eq!(reg.get("third").unwrap().id(), "third");
219        assert!(reg.get("fourth").is_err());
220    }
221
222    // ── default_backend ────────────────────────────────────────
223
224    #[test]
225    fn test_default_backend_empty() {
226        let reg = BackendRegistry::new();
227        assert!(matches!(
228            reg.default_backend(),
229            Err(SchedulerError::NoBackendRegistered)
230        ));
231    }
232
233    #[test]
234    fn test_default_backend_returns_one_of_registered() {
235        let mut reg = BackendRegistry::new();
236        reg.register(make_backend("alpha"));
237        reg.register(make_backend("beta"));
238        // First-registered is the deterministic default.
239        assert_eq!(reg.default_backend().unwrap().id(), "alpha");
240    }
241
242    #[test]
243    fn test_default_backend_single_entry() {
244        let mut reg = BackendRegistry::new();
245        reg.register(make_backend("sole"));
246        assert_eq!(reg.default_backend().unwrap().id(), "sole");
247    }
248
249    #[test]
250    fn test_with_default_overrides_first_registered() {
251        let reg = BackendRegistry::new()
252            .with(make_backend("a"))
253            .with_default(make_backend("b"))
254            .with(make_backend("c"));
255        assert_eq!(reg.default_backend().unwrap().id(), "b");
256        assert_eq!(reg.default_id(), Some("b"));
257    }
258
259    #[test]
260    fn test_set_default_marks_existing_backend() {
261        let mut reg = BackendRegistry::new();
262        reg.register(make_backend("a"));
263        reg.register(make_backend("b"));
264        assert_eq!(reg.default_id(), Some("a"));
265        reg.set_default("b").unwrap();
266        assert_eq!(reg.default_backend().unwrap().id(), "b");
267    }
268
269    #[test]
270    fn test_set_default_rejects_unknown_id() {
271        let mut reg = BackendRegistry::new();
272        reg.register(make_backend("a"));
273        assert!(matches!(
274            reg.set_default("nope"),
275            Err(SchedulerError::UnknownBackend(_))
276        ));
277    }
278
279    // ── Clone ──────────────────────────────────────────────────
280
281    #[test]
282    fn test_clone_empty() {
283        let reg = BackendRegistry::new();
284        let cloned = reg.clone();
285        assert!(matches!(
286            cloned.default_backend(),
287            Err(SchedulerError::NoBackendRegistered)
288        ));
289    }
290
291    #[test]
292    fn test_clone_is_independent() {
293        let mut reg = BackendRegistry::new();
294        reg.register(make_backend("original"));
295        let mut cloned = reg.clone();
296        cloned.register(make_backend("new-only"));
297        // Original must not see the new backend.
298        assert!(reg.get("new-only").is_err());
299        assert_eq!(cloned.get("new-only").unwrap().id(), "new-only");
300        // Both still have "original".
301        assert_eq!(reg.get("original").unwrap().id(), "original");
302        assert_eq!(cloned.get("original").unwrap().id(), "original");
303    }
304
305    // ── Debug ──────────────────────────────────────────────────
306
307    #[test]
308    fn test_debug_empty() {
309        let reg = BackendRegistry::new();
310        let s = format!("{:?}", reg);
311        assert!(s.starts_with("BackendRegistry"));
312    }
313
314    #[test]
315    fn test_debug_with_backends() {
316        let mut reg = BackendRegistry::new();
317        reg.register(make_backend("dbg-a"));
318        reg.register(make_backend("dbg-b"));
319        let s = format!("{:?}", reg);
320        assert!(s.starts_with("BackendRegistry"));
321        assert!(s.contains("dbg-a") || s.contains("dbg-b"));
322    }
323}