Skip to main content

wm_workspace/
event.rs

1//! Workspace event types — published by cognitive cores.
2
3use crate::salience::Salience;
4use serde::{Deserialize, Serialize};
5use std::time::Instant;
6
7/// Identifier for a cognitive core (the source of an event).
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub enum CoreId {
10    /// Citta consciousness cycle.
11    Citta,
12    /// Dream cycle.
13    Dream,
14    /// Brain-wave state manager.
15    BrainWave,
16    /// Autonomous cycle runner.
17    Autonomous,
18    /// Tool dispatch pipeline.
19    Dispatch,
20    /// Reflex tier.
21    Reflex,
22    /// Self-model / predictive introspection.
23    SelfModel,
24    /// Drive core (emotion/motivation).
25    Drive,
26    /// Homeostasis monitor.
27    Homeostasis,
28    /// External sensor / embodiment layer.
29    Sensor,
30    /// User-defined core (custom ID).
31    Custom(u16),
32}
33
34impl CoreId {
35    /// Human-readable name.
36    #[must_use]
37    pub const fn name(&self) -> &str {
38        match self {
39            Self::Citta => "citta",
40            Self::Dream => "dream",
41            Self::BrainWave => "brain_wave",
42            Self::Autonomous => "autonomous",
43            Self::Dispatch => "dispatch",
44            Self::Reflex => "reflex",
45            Self::SelfModel => "self_model",
46            Self::Drive => "drive",
47            Self::Homeostasis => "homeostasis",
48            Self::Sensor => "sensor",
49            Self::Custom(_) => "custom",
50        }
51    }
52}
53
54impl std::fmt::Display for CoreId {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        match self {
57            Self::Custom(id) => write!(f, "custom_{id}"),
58            _ => write!(f, "{}", self.name()),
59        }
60    }
61}
62
63/// Type of workspace event.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
65pub enum EventType {
66    /// An error occurred in a core.
67    Error,
68    /// A reward signal was produced.
69    Reward,
70    /// A core is requesting attention (wants the spotlight).
71    AttentionRequest,
72    /// A novel detection was made (unexpected pattern, anomaly).
73    NovelDetection,
74    /// A metric crossed a threshold (from self-model or homeostasis).
75    ThresholdCrossing,
76    /// A drive state changed (curiosity, caution, etc.).
77    DriveUpdate,
78    /// A safety alert was triggered (from reflex tier or dharma).
79    SafetyAlert,
80}
81
82impl EventType {
83    /// Human-readable name.
84    #[must_use]
85    pub const fn name(&self) -> &'static str {
86        match self {
87            Self::Error => "error",
88            Self::Reward => "reward",
89            Self::AttentionRequest => "attention_request",
90            Self::NovelDetection => "novel_detection",
91            Self::ThresholdCrossing => "threshold_crossing",
92            Self::DriveUpdate => "drive_update",
93            Self::SafetyAlert => "safety_alert",
94        }
95    }
96}
97
98impl std::fmt::Display for EventType {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        write!(f, "{}", self.name())
101    }
102}
103
104/// A workspace event published by a cognitive core.
105///
106/// Each event carries a salience score that determines its priority in
107/// the spotlight arbitration. The payload is a JSON value containing
108/// core-specific data.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct WorkspaceEvent {
111    /// The core that produced this event.
112    pub core: CoreId,
113    /// The type of event.
114    pub event_type: EventType,
115    /// Salience score (urgency × novelty × confidence).
116    pub salience: Salience,
117    /// Core-specific payload data.
118    pub payload: serde_json::Value,
119    /// When the event was created.
120    #[serde(skip, default = "Instant::now")]
121    pub timestamp: Instant,
122}
123
124impl WorkspaceEvent {
125    /// Create a new workspace event with the given salience.
126    #[must_use]
127    pub fn new(
128        core: CoreId,
129        event_type: EventType,
130        salience: Salience,
131        payload: serde_json::Value,
132    ) -> Self {
133        Self {
134            core,
135            event_type,
136            salience,
137            payload,
138            timestamp: Instant::now(),
139        }
140    }
141
142    /// Create a new event with default urgency based on event type.
143    #[must_use]
144    pub fn with_default_urgency(
145        core: CoreId,
146        event_type: EventType,
147        novelty: f32,
148        confidence: f32,
149        payload: serde_json::Value,
150    ) -> Self {
151        let urgency = crate::salience::default_urgency(&event_type);
152        Self::new(
153            core,
154            event_type,
155            Salience::new(urgency, novelty, confidence),
156            payload,
157        )
158    }
159
160    /// Get the composite salience score.
161    #[must_use]
162    pub fn composite_salience(&self) -> f32 {
163        self.salience.composite()
164    }
165
166    /// Check if this event should preempt the current spotlight.
167    #[must_use]
168    pub fn should_preempt(&self) -> bool {
169        self.salience.is_high_salience()
170    }
171
172    /// Age of this event (time since creation).
173    #[must_use]
174    pub fn age(&self) -> std::time::Duration {
175        self.timestamp.elapsed()
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn core_id_names() {
185        assert_eq!(CoreId::Citta.name(), "citta");
186        assert_eq!(CoreId::Dream.name(), "dream");
187        assert_eq!(CoreId::Reflex.name(), "reflex");
188        assert_eq!(CoreId::Custom(42).name(), "custom");
189    }
190
191    #[test]
192    fn core_id_display_custom() {
193        assert_eq!(format!("{}", CoreId::Custom(7)), "custom_7");
194        assert_eq!(format!("{}", CoreId::Citta), "citta");
195    }
196
197    #[test]
198    fn event_type_names() {
199        assert_eq!(EventType::Error.name(), "error");
200        assert_eq!(EventType::SafetyAlert.name(), "safety_alert");
201        assert_eq!(EventType::NovelDetection.name(), "novel_detection");
202    }
203
204    #[test]
205    fn event_new() {
206        let event = WorkspaceEvent::new(
207            CoreId::Reflex,
208            EventType::SafetyAlert,
209            Salience::new(1.0, 0.8, 0.9),
210            serde_json::json!({"sensor": "imu_1", "value": 42.0}),
211        );
212        assert_eq!(event.core, CoreId::Reflex);
213        assert_eq!(event.event_type, EventType::SafetyAlert);
214        assert!((event.composite_salience() - 0.72).abs() < 0.001);
215    }
216
217    #[test]
218    fn event_with_default_urgency() {
219        let event = WorkspaceEvent::with_default_urgency(
220            CoreId::Homeostasis,
221            EventType::ThresholdCrossing,
222            0.5,
223            0.9,
224            serde_json::json!({"metric": "cpu", "value": 95.0}),
225        );
226        // ThresholdCrossing has default urgency 0.8
227        assert!((event.salience.urgency - 0.8).abs() < 0.001);
228        assert!((event.salience.novelty - 0.5).abs() < 0.001);
229        assert!((event.salience.confidence - 0.9).abs() < 0.001);
230    }
231
232    #[test]
233    fn event_should_preempt() {
234        let high = WorkspaceEvent::new(
235            CoreId::Reflex,
236            EventType::SafetyAlert,
237            Salience::new(0.95, 0.95, 0.95),
238            serde_json::json!({}),
239        );
240        assert!(high.should_preempt());
241
242        let low = WorkspaceEvent::new(
243            CoreId::Drive,
244            EventType::DriveUpdate,
245            Salience::new(0.2, 0.3, 0.5),
246            serde_json::json!({}),
247        );
248        assert!(!low.should_preempt());
249    }
250
251    #[test]
252    fn event_age_grows() {
253        let event = WorkspaceEvent::new(
254            CoreId::Citta,
255            EventType::AttentionRequest,
256            Salience::new(0.5, 0.5, 0.5),
257            serde_json::json!({}),
258        );
259        std::thread::sleep(std::time::Duration::from_millis(10));
260        assert!(event.age().as_millis() >= 10);
261    }
262}