Skip to main content

eml_core/
events.rs

1//! Events emitted by EML models for chain logging.
2//!
3//! Each EML model accumulates events during its lifecycle (training,
4//! prediction, drift detection, save/load). The kernel-level code is
5//! responsible for draining these events and appending them to the
6//! ExoChain — the models themselves are chain-agnostic.
7
8use serde::{Deserialize, Serialize};
9
10// ---------------------------------------------------------------------------
11// EmlEvent
12// ---------------------------------------------------------------------------
13
14/// An event emitted by an EML model during its lifecycle.
15///
16/// These events are accumulated in a per-model event log and drained
17/// by the caller for chain persistence. The model itself never touches
18/// the ExoChain directly.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub enum EmlEvent {
21    /// Model training completed.
22    Trained {
23        model_name: String,
24        samples_used: usize,
25        mse_before: f64,
26        mse_after: f64,
27        converged: bool,
28        param_count: usize,
29    },
30    /// Significant model prediction recorded.
31    Prediction {
32        model_name: String,
33        /// BLAKE3 hash of input features (hex-encoded).
34        inputs_hash: String,
35        output: Vec<f64>,
36    },
37    /// Drift detected between prediction and actual value.
38    Drift {
39        model_name: String,
40        predicted: f64,
41        actual: f64,
42        drift_pct: f64,
43    },
44    /// Model state saved to disk.
45    Saved {
46        model_name: String,
47        path: String,
48        param_count: usize,
49    },
50    /// Model state loaded from disk.
51    Loaded {
52        model_name: String,
53        path: String,
54        trained: bool,
55        samples: usize,
56    },
57    /// Model was reset.
58    Reset {
59        model_name: String,
60        reason: String,
61    },
62}
63
64impl EmlEvent {
65    /// Return the canonical event type string for chain logging.
66    ///
67    /// Used as the `kind` field when appending to ExoChain.
68    pub fn event_type(&self) -> &'static str {
69        match self {
70            EmlEvent::Trained { .. } => "eml.trained",
71            EmlEvent::Prediction { .. } => "eml.prediction",
72            EmlEvent::Drift { .. } => "eml.drift",
73            EmlEvent::Saved { .. } => "eml.saved",
74            EmlEvent::Loaded { .. } => "eml.loaded",
75            EmlEvent::Reset { .. } => "eml.reset",
76        }
77    }
78
79    /// Return the model name embedded in this event.
80    pub fn model_name(&self) -> &str {
81        match self {
82            EmlEvent::Trained { model_name, .. }
83            | EmlEvent::Prediction { model_name, .. }
84            | EmlEvent::Drift { model_name, .. }
85            | EmlEvent::Saved { model_name, .. }
86            | EmlEvent::Loaded { model_name, .. }
87            | EmlEvent::Reset { model_name, .. } => model_name,
88        }
89    }
90}
91
92// ---------------------------------------------------------------------------
93// EmlEventLog
94// ---------------------------------------------------------------------------
95
96/// Accumulator for EML lifecycle events.
97///
98/// Models push events here during operations. Callers drain the log
99/// periodically and forward events to the ExoChain or other sinks.
100#[derive(Debug, Clone, Default)]
101pub struct EmlEventLog {
102    events: Vec<EmlEvent>,
103}
104
105impl EmlEventLog {
106    /// Create a new empty event log.
107    pub fn new() -> Self {
108        Self {
109            events: Vec::new(),
110        }
111    }
112
113    /// Push a new event.
114    pub fn push(&mut self, event: EmlEvent) {
115        self.events.push(event);
116    }
117
118    /// Drain all accumulated events, returning them and clearing the log.
119    pub fn drain(&mut self) -> Vec<EmlEvent> {
120        std::mem::take(&mut self.events)
121    }
122
123    /// Number of pending events.
124    pub fn len(&self) -> usize {
125        self.events.len()
126    }
127
128    /// Whether the log is empty.
129    pub fn is_empty(&self) -> bool {
130        self.events.is_empty()
131    }
132}
133
134// ---------------------------------------------------------------------------
135// Tests
136// ---------------------------------------------------------------------------
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn event_type_strings() {
144        let e = EmlEvent::Trained {
145            model_name: "test".into(),
146            samples_used: 100,
147            mse_before: 1.0,
148            mse_after: 0.01,
149            converged: true,
150            param_count: 50,
151        };
152        assert_eq!(e.event_type(), "eml.trained");
153
154        let e = EmlEvent::Drift {
155            model_name: "test".into(),
156            predicted: 1.0,
157            actual: 1.1,
158            drift_pct: 10.0,
159        };
160        assert_eq!(e.event_type(), "eml.drift");
161    }
162
163    #[test]
164    fn event_log_drain() {
165        let mut log = EmlEventLog::new();
166        assert!(log.is_empty());
167
168        log.push(EmlEvent::Reset {
169            model_name: "test".into(),
170            reason: "manual".into(),
171        });
172        assert_eq!(log.len(), 1);
173
174        let drained = log.drain();
175        assert_eq!(drained.len(), 1);
176        assert!(log.is_empty());
177    }
178
179    #[test]
180    fn model_name_accessor() {
181        let e = EmlEvent::Saved {
182            model_name: "coherence".into(),
183            path: "/tmp/test".into(),
184            param_count: 50,
185        };
186        assert_eq!(e.model_name(), "coherence");
187    }
188}