1use crate::event::{CoreId, EventType, WorkspaceEvent};
12use crate::spotlight::Spotlight;
13use std::sync::atomic::{AtomicU64, Ordering};
14use thiserror::Error;
15
16pub const BACKLOG_SIZE: usize = 256;
18
19pub const CHANNEL_CAPACITY: usize = 512;
21
22#[derive(Debug, Clone, Error)]
24pub enum WorkspaceError {
25 #[error("event payload too large: {0} bytes")]
27 PayloadTooLarge(usize),
28}
29
30#[derive(Debug, Clone, Default)]
32pub struct WorkspaceStats {
33 pub events_published: u64,
35 pub spotlight_transfers: u64,
37 pub arbitration_cycles: u64,
39 pub events_per_core: std::collections::HashMap<CoreId, u64>,
41 pub events_per_type: std::collections::HashMap<EventType, u64>,
43}
44
45pub struct GlobalWorkspace {
52 spotlight: Spotlight,
54 backlog: std::collections::VecDeque<WorkspaceEvent>,
56 events_published: AtomicU64,
58 events_per_core: std::collections::HashMap<CoreId, u64>,
60 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 #[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 #[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 pub fn publish(&mut self, event: &WorkspaceEvent) -> bool {
117 self.events_published.fetch_add(1, Ordering::Relaxed);
118
119 *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 if self.backlog.len() >= BACKLOG_SIZE {
125 self.backlog.pop_front();
126 }
127 self.backlog.push_back(event.clone());
128
129 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 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 pub fn publish_batch(&mut self, events: &[WorkspaceEvent]) -> Option<usize> {
163 if events.is_empty() {
164 return None;
165 }
166
167 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 self.spotlight.arbitrate_batch(events)
181 }
182
183 #[must_use]
185 pub const fn spotlight(&self) -> Option<&crate::spotlight::SpotlightEntry> {
186 self.spotlight.current()
187 }
188
189 #[must_use]
191 pub fn spotlight_core(&self) -> Option<CoreId> {
192 self.spotlight.current_core()
193 }
194
195 #[must_use]
197 pub fn spotlight_strength(&self) -> f32 {
198 self.spotlight.strength()
199 }
200
201 pub const fn backlog(&self) -> &std::collections::VecDeque<WorkspaceEvent> {
203 &self.backlog
204 }
205
206 pub fn recent_events(&self, n: usize) -> Vec<&WorkspaceEvent> {
208 self.backlog.iter().rev().take(n).collect()
209 }
210
211 #[must_use]
213 pub fn events_published(&self) -> u64 {
214 self.events_published.load(Ordering::Relaxed)
215 }
216
217 #[must_use]
219 pub const fn spotlight_transfers(&self) -> u64 {
220 self.spotlight.transfer_count()
221 }
222
223 #[must_use]
225 pub const fn arbitration_cycles(&self) -> u64 {
226 self.spotlight.arbitration_count()
227 }
228
229 #[must_use]
231 pub fn core_hold_count(&self, core: CoreId) -> u64 {
232 self.spotlight.core_hold_count(core)
233 }
234
235 #[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 #[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 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 pub const fn clear_spotlight(&mut self) {
260 self.spotlight.clear();
261 }
262
263 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)); 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 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}