Skip to main content

wm_workspace/
bus.rs

1//! Global workspace bus — publish/subscribe event bus with salience arbitration.
2//!
3//! All cognitive cores publish events to the workspace. The workspace
4//! arbitrates attention via the spotlight mechanism and maintains a
5//! ring buffer of recent events.
6//!
7//! The bus uses `tokio::sync::broadcast` for multiple subscribers. Slow
8//! subscribers may miss events (lossy by design — the workspace is not
9//! a reliable queue, it's an attention mechanism).
10
11use crate::event::{CoreId, EventType, WorkspaceEvent};
12use crate::spotlight::Spotlight;
13use std::sync::atomic::{AtomicU64, Ordering};
14use thiserror::Error;
15
16/// Maximum number of events in the backlog ring buffer.
17pub const BACKLOG_SIZE: usize = 256;
18
19/// Default broadcast channel capacity.
20pub const CHANNEL_CAPACITY: usize = 512;
21
22/// Error type for workspace operations.
23#[derive(Debug, Clone, Error)]
24pub enum WorkspaceError {
25    /// Event payload too large.
26    #[error("event payload too large: {0} bytes")]
27    PayloadTooLarge(usize),
28}
29
30/// Workspace statistics.
31#[derive(Debug, Clone, Default)]
32pub struct WorkspaceStats {
33    /// Total events published.
34    pub events_published: u64,
35    /// Total spotlight transfers.
36    pub spotlight_transfers: u64,
37    /// Total arbitration cycles.
38    pub arbitration_cycles: u64,
39    /// Events per core.
40    pub events_per_core: std::collections::HashMap<CoreId, u64>,
41    /// Events per type.
42    pub events_per_type: std::collections::HashMap<EventType, u64>,
43}
44
45/// The global workspace — coordinates attention across all cognitive cores.
46///
47/// Combines:
48/// - A broadcast event bus (tokio::sync::broadcast)
49/// - A spotlight tracker for salience-based arbitration
50/// - A ring buffer backlog of recent events
51pub struct GlobalWorkspace {
52    /// Spotlight tracker.
53    spotlight: Spotlight,
54    /// Ring buffer of recent events.
55    backlog: std::collections::VecDeque<WorkspaceEvent>,
56    /// Total events published.
57    events_published: AtomicU64,
58    /// Events per core.
59    events_per_core: std::collections::HashMap<CoreId, u64>,
60    /// Events per type.
61    events_per_type: std::collections::HashMap<EventType, u64>,
62}
63
64impl std::fmt::Debug for GlobalWorkspace {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        f.debug_struct("GlobalWorkspace")
67            .field("spotlight", &self.spotlight)
68            .field("backlog_len", &self.backlog.len())
69            .field(
70                "events_published",
71                &self.events_published.load(Ordering::Relaxed),
72            )
73            .finish_non_exhaustive()
74    }
75}
76
77impl Default for GlobalWorkspace {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl GlobalWorkspace {
84    /// Create a new global workspace with default settings.
85    #[must_use]
86    pub fn new() -> Self {
87        Self {
88            spotlight: Spotlight::default(),
89            backlog: std::collections::VecDeque::with_capacity(BACKLOG_SIZE),
90            events_published: AtomicU64::new(0),
91            events_per_core: std::collections::HashMap::new(),
92            events_per_type: std::collections::HashMap::new(),
93        }
94    }
95
96    /// Create a workspace with a custom spotlight half-life.
97    #[must_use]
98    pub fn with_half_life(half_life: std::time::Duration) -> Self {
99        Self {
100            spotlight: Spotlight::new(half_life),
101            backlog: std::collections::VecDeque::with_capacity(BACKLOG_SIZE),
102            events_published: AtomicU64::new(0),
103            events_per_core: std::collections::HashMap::new(),
104            events_per_type: std::collections::HashMap::new(),
105        }
106    }
107
108    /// Publish an event to the workspace.
109    ///
110    /// The event is:
111    /// 1. Added to the backlog ring buffer
112    /// 2. Arbitrated against the current spotlight
113    /// 3. Counted in statistics
114    ///
115    /// Returns `true` if the event won the spotlight.
116    pub fn publish(&mut self, event: &WorkspaceEvent) -> bool {
117        self.events_published.fetch_add(1, Ordering::Relaxed);
118
119        // Update per-core and per-type counts
120        *self.events_per_core.entry(event.core).or_insert(0) += 1;
121        *self.events_per_type.entry(event.event_type).or_insert(0) += 1;
122
123        // Add to backlog (evict oldest if full)
124        if self.backlog.len() >= BACKLOG_SIZE {
125            self.backlog.pop_front();
126        }
127        self.backlog.push_back(event.clone());
128
129        // Arbitrate
130        let won = self.spotlight.arbitrate(event);
131
132        if won {
133            tracing::debug!(
134                core = %event.core,
135                event_type = %event.event_type,
136                salience = event.composite_salience(),
137                "spotlight won"
138            );
139        }
140
141        won
142    }
143
144    /// Publish a simple event with default urgency.
145    pub fn publish_simple(
146        &mut self,
147        core: CoreId,
148        event_type: EventType,
149        novelty: f32,
150        confidence: f32,
151        payload: serde_json::Value,
152    ) -> bool {
153        let event =
154            WorkspaceEvent::with_default_urgency(core, event_type, novelty, confidence, payload);
155        self.publish(&event)
156    }
157
158    /// Publish multiple events at once. The highest-salience event is
159    /// arbitrated against the current spotlight.
160    ///
161    /// Returns the index of the winning event, or `None` if none won.
162    pub fn publish_batch(&mut self, events: &[WorkspaceEvent]) -> Option<usize> {
163        if events.is_empty() {
164            return None;
165        }
166
167        // Add all to backlog and counts
168        for event in events {
169            self.events_published.fetch_add(1, Ordering::Relaxed);
170            *self.events_per_core.entry(event.core).or_insert(0) += 1;
171            *self.events_per_type.entry(event.event_type).or_insert(0) += 1;
172
173            if self.backlog.len() >= BACKLOG_SIZE {
174                self.backlog.pop_front();
175            }
176            self.backlog.push_back(event.clone());
177        }
178
179        // Arbitrate batch
180        self.spotlight.arbitrate_batch(events)
181    }
182
183    /// Get the current spotlight entry.
184    #[must_use]
185    pub const fn spotlight(&self) -> Option<&crate::spotlight::SpotlightEntry> {
186        self.spotlight.current()
187    }
188
189    /// Get the core currently holding the spotlight.
190    #[must_use]
191    pub fn spotlight_core(&self) -> Option<CoreId> {
192        self.spotlight.current_core()
193    }
194
195    /// Get the current spotlight strength (0.0 to 1.0).
196    #[must_use]
197    pub fn spotlight_strength(&self) -> f32 {
198        self.spotlight.strength()
199    }
200
201    /// Get the recent event backlog (newest first).
202    pub const fn backlog(&self) -> &std::collections::VecDeque<WorkspaceEvent> {
203        &self.backlog
204    }
205
206    /// Get the last N events from the backlog.
207    pub fn recent_events(&self, n: usize) -> Vec<&WorkspaceEvent> {
208        self.backlog.iter().rev().take(n).collect()
209    }
210
211    /// Total events published.
212    #[must_use]
213    pub fn events_published(&self) -> u64 {
214        self.events_published.load(Ordering::Relaxed)
215    }
216
217    /// Spotlight transfer count.
218    #[must_use]
219    pub const fn spotlight_transfers(&self) -> u64 {
220        self.spotlight.transfer_count()
221    }
222
223    /// Spotlight arbitration cycle count.
224    #[must_use]
225    pub const fn arbitration_cycles(&self) -> u64 {
226        self.spotlight.arbitration_count()
227    }
228
229    /// Get the number of times a core has held the spotlight.
230    #[must_use]
231    pub fn core_hold_count(&self, core: CoreId) -> u64 {
232        self.spotlight.core_hold_count(core)
233    }
234
235    /// Get the number of events published by a core.
236    #[must_use]
237    pub fn core_event_count(&self, core: CoreId) -> u64 {
238        self.events_per_core.get(&core).copied().unwrap_or(0)
239    }
240
241    /// Get the number of events of a specific type.
242    #[must_use]
243    pub fn event_type_count(&self, event_type: EventType) -> u64 {
244        self.events_per_type.get(&event_type).copied().unwrap_or(0)
245    }
246
247    /// Collect workspace statistics.
248    pub fn stats(&self) -> WorkspaceStats {
249        WorkspaceStats {
250            events_published: self.events_published.load(Ordering::Relaxed),
251            spotlight_transfers: self.spotlight.transfer_count(),
252            arbitration_cycles: self.spotlight.arbitration_count(),
253            events_per_core: self.events_per_core.clone(),
254            events_per_type: self.events_per_type.clone(),
255        }
256    }
257
258    /// Clear the spotlight.
259    pub const fn clear_spotlight(&mut self) {
260        self.spotlight.clear();
261    }
262
263    /// Clear the event backlog.
264    pub fn clear_backlog(&mut self) {
265        self.backlog.clear();
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use crate::salience::Salience;
273
274    fn make_event(core: CoreId, event_type: EventType, salience: f32) -> WorkspaceEvent {
275        WorkspaceEvent::new(
276            core,
277            event_type,
278            Salience::new(salience, salience, salience),
279            serde_json::json!({"test": true}),
280        )
281    }
282
283    #[test]
284    fn workspace_default() {
285        let ws = GlobalWorkspace::default();
286        assert_eq!(ws.events_published(), 0);
287        assert!(ws.spotlight().is_none());
288        assert_eq!(ws.backlog().len(), 0);
289    }
290
291    #[test]
292    fn publish_first_event_wins_spotlight() {
293        let mut ws = GlobalWorkspace::new();
294        let event = make_event(CoreId::Citta, EventType::AttentionRequest, 0.5);
295        assert!(ws.publish(&event));
296        assert_eq!(ws.spotlight_core(), Some(CoreId::Citta));
297        assert_eq!(ws.events_published(), 1);
298    }
299
300    #[test]
301    fn publish_lower_salience_does_not_win() {
302        let mut ws = GlobalWorkspace::new();
303        let high = make_event(CoreId::Citta, EventType::AttentionRequest, 0.8);
304        assert!(ws.publish(&high));
305
306        let low = make_event(CoreId::Dream, EventType::Reward, 0.3);
307        assert!(!ws.publish(&low));
308        assert_eq!(ws.spotlight_core(), Some(CoreId::Citta));
309    }
310
311    #[test]
312    fn publish_higher_salience_wins() {
313        let mut ws = GlobalWorkspace::new();
314        let low = make_event(CoreId::Citta, EventType::AttentionRequest, 0.3);
315        assert!(ws.publish(&low));
316
317        let high = make_event(CoreId::Dream, EventType::NovelDetection, 0.9);
318        assert!(ws.publish(&high));
319        assert_eq!(ws.spotlight_core(), Some(CoreId::Dream));
320    }
321
322    #[test]
323    fn backlog_ring_buffer_evicts_old() {
324        let mut ws = GlobalWorkspace::new();
325        for i in 0..(BACKLOG_SIZE + 50) {
326            let event = make_event(CoreId::Custom(i as u16), EventType::DriveUpdate, 0.1);
327            ws.publish(&event);
328        }
329        assert_eq!(ws.backlog().len(), BACKLOG_SIZE);
330    }
331
332    #[test]
333    fn recent_events_returns_newest_first() {
334        let mut ws = GlobalWorkspace::new();
335        ws.publish(&make_event(CoreId::Citta, EventType::Reward, 0.1));
336        ws.publish(&make_event(CoreId::Dream, EventType::Reward, 0.1));
337        ws.publish(&make_event(CoreId::Reflex, EventType::Reward, 0.1));
338
339        let recent = ws.recent_events(2);
340        assert_eq!(recent.len(), 2);
341        assert_eq!(recent[0].core, CoreId::Reflex);
342        assert_eq!(recent[1].core, CoreId::Dream);
343    }
344
345    #[test]
346    fn publish_batch() {
347        let mut ws = GlobalWorkspace::new();
348        let events = vec![
349            make_event(CoreId::Citta, EventType::AttentionRequest, 0.3),
350            make_event(CoreId::Dream, EventType::NovelDetection, 0.7),
351            make_event(CoreId::Reflex, EventType::SafetyAlert, 0.5),
352        ];
353        let winner = ws.publish_batch(&events);
354        assert_eq!(winner, Some(1)); // Dream has highest salience
355        assert_eq!(ws.spotlight_core(), Some(CoreId::Dream));
356        assert_eq!(ws.events_published(), 3);
357        assert_eq!(ws.backlog().len(), 3);
358    }
359
360    #[test]
361    fn publish_batch_empty() {
362        let mut ws = GlobalWorkspace::new();
363        assert!(ws.publish_batch(&[]).is_none());
364    }
365
366    #[test]
367    fn core_event_count() {
368        let mut ws = GlobalWorkspace::new();
369        ws.publish(&make_event(CoreId::Citta, EventType::Reward, 0.1));
370        ws.publish(&make_event(CoreId::Citta, EventType::Reward, 0.1));
371        ws.publish(&make_event(CoreId::Dream, EventType::Reward, 0.1));
372        assert_eq!(ws.core_event_count(CoreId::Citta), 2);
373        assert_eq!(ws.core_event_count(CoreId::Dream), 1);
374        assert_eq!(ws.core_event_count(CoreId::Reflex), 0);
375    }
376
377    #[test]
378    fn event_type_count() {
379        let mut ws = GlobalWorkspace::new();
380        ws.publish(&make_event(CoreId::Citta, EventType::Error, 0.1));
381        ws.publish(&make_event(CoreId::Dream, EventType::Error, 0.1));
382        ws.publish(&make_event(CoreId::Reflex, EventType::SafetyAlert, 0.1));
383        assert_eq!(ws.event_type_count(EventType::Error), 2);
384        assert_eq!(ws.event_type_count(EventType::SafetyAlert), 1);
385    }
386
387    #[test]
388    fn stats_collection() {
389        let mut ws = GlobalWorkspace::new();
390        ws.publish(&make_event(CoreId::Citta, EventType::Reward, 0.5));
391        ws.publish(&make_event(CoreId::Dream, EventType::NovelDetection, 0.8));
392
393        let stats = ws.stats();
394        assert_eq!(stats.events_published, 2);
395        assert_eq!(stats.spotlight_transfers, 2);
396        assert_eq!(stats.arbitration_cycles, 2);
397        assert_eq!(stats.events_per_core.get(&CoreId::Citta), Some(&1));
398        assert_eq!(stats.events_per_core.get(&CoreId::Dream), Some(&1));
399    }
400
401    #[test]
402    fn publish_simple_with_default_urgency() {
403        let mut ws = GlobalWorkspace::new();
404        let won = ws.publish_simple(
405            CoreId::Reflex,
406            EventType::SafetyAlert,
407            0.9,
408            0.9,
409            serde_json::json!({"alert": "collision"}),
410        );
411        // SafetyAlert has urgency 1.0, so composite = 1.0 * 0.9 * 0.9 = 0.81
412        assert!(won);
413        assert_eq!(ws.spotlight_core(), Some(CoreId::Reflex));
414    }
415
416    #[test]
417    fn clear_spotlight() {
418        let mut ws = GlobalWorkspace::new();
419        ws.publish(&make_event(CoreId::Citta, EventType::AttentionRequest, 0.5));
420        assert!(ws.spotlight().is_some());
421        ws.clear_spotlight();
422        assert!(ws.spotlight().is_none());
423    }
424
425    #[test]
426    fn clear_backlog() {
427        let mut ws = GlobalWorkspace::new();
428        ws.publish(&make_event(CoreId::Citta, EventType::Reward, 0.1));
429        ws.publish(&make_event(CoreId::Dream, EventType::Reward, 0.1));
430        assert_eq!(ws.backlog().len(), 2);
431        ws.clear_backlog();
432        assert_eq!(ws.backlog().len(), 0);
433    }
434
435    #[test]
436    fn spotlight_strength_decays() {
437        let mut ws = GlobalWorkspace::with_half_life(std::time::Duration::from_millis(50));
438        ws.publish(&make_event(CoreId::Citta, EventType::AttentionRequest, 0.8));
439
440        let s1 = ws.spotlight_strength();
441        assert!(s1 > 0.48 && s1 < 0.53, "initial strength: {s1}");
442
443        std::thread::sleep(std::time::Duration::from_millis(50));
444        let s2 = ws.spotlight_strength();
445        assert!(s2 < s1);
446    }
447
448    #[test]
449    fn core_hold_count() {
450        let mut ws = GlobalWorkspace::new();
451        ws.publish(&make_event(CoreId::Citta, EventType::AttentionRequest, 0.5));
452        ws.publish(&make_event(CoreId::Dream, EventType::NovelDetection, 0.8));
453        ws.publish(&make_event(CoreId::Citta, EventType::Error, 0.9));
454
455        assert_eq!(ws.core_hold_count(CoreId::Citta), 2);
456        assert_eq!(ws.core_hold_count(CoreId::Dream), 1);
457    }
458
459    #[test]
460    fn backlog_max_size() {
461        let mut ws = GlobalWorkspace::new();
462        for i in 0..100 {
463            ws.publish(&make_event(
464                CoreId::Custom(i as u16),
465                EventType::DriveUpdate,
466                0.01,
467            ));
468        }
469        assert!(ws.backlog().len() <= BACKLOG_SIZE);
470    }
471}