Skip to main content

el_core/
events.rs

1//! Content-free domain events (ADR-007).
2//!
3//! **Enforcement:** [`DomainEvent`] and [`EventEnvelope`] derive `Copy`. A
4//! `String`, `Vec<u8>`, or any heap-owning field is not `Copy`, so adding one
5//! would fail to compile. That makes "no prompt/response content on an event" a
6//! *compile-time* guarantee, not a code-review convention. Fixed-point integers
7//! (e.g. `*_milli`) stand in for ratios/scores so the type stays `Copy` + `Eq`.
8
9use crate::ids::{ModelId, ModelVersion, SessionId};
10use crate::value_objects::{
11    DeviceTarget, ModelFormat, RuntimeKind, SafetyMode, SpeculationMode, StopReason,
12};
13
14/// Why an optional pipeline stage was degraded (PRD risk policy made
15/// observable).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum DegradeReason {
18    Disabled,
19    MemoryPressure,
20    MidRangeProfile,
21}
22
23/// Every fact the pipeline emits. Carries only ids, counts, enums, and
24/// fixed-point numbers — never content.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum DomainEvent {
27    // --- 1. Inference Runtime ---
28    SessionInitialized {
29        runtime: RuntimeKind,
30        device: DeviceTarget,
31        safety: SafetyMode,
32        speculation: SpeculationMode,
33    },
34    ModelLoaded {
35        model: ModelId,
36        version: ModelVersion,
37        format: ModelFormat,
38    },
39    PrefillCompleted {
40        prompt_tokens: u32,
41        kv_len: u32,
42        prefill_tps: u32,
43    },
44    TokenGenerated {
45        sampled: bool,
46    },
47    TokenCommitted {
48        kv_len: u32,
49    },
50    GenerationCompleted {
51        total_tokens: u32,
52        stop: StopReason,
53    },
54    SessionReset,
55    HybridRelayConsulted,
56
57    // --- 2. Prompt Compression ---
58    PromptCompressed {
59        input_tokens: u32,
60        output_tokens: u32,
61        /// Compression ratio × 1000 (e.g. 250 = 0.25 = 4× shorter).
62        ratio_milli: u32,
63    },
64    CompressionSkipped {
65        reason: DegradeReason,
66    },
67
68    // --- 3. Speculative Decoding ---
69    DraftProposed {
70        draft_len: u8,
71    },
72    DraftVerified {
73        accepted: u8,
74        first_reject: u8,
75    },
76    SpeculationDisabled {
77        reason: DegradeReason,
78    },
79
80    // --- 4. Grammar Constraint ---
81    GrammarSwitched {
82        from_state: u32,
83        to_state: u32,
84    },
85    TokenMaskApplied {
86        allowed: u32,
87    },
88    GrammarViolationBlocked,
89
90    // --- 5. Safety ---
91    SafetyModeSelected {
92        mode: SafetyMode,
93    },
94    LogitsSteered {
95        adjustment_norm_milli: u32,
96    },
97    SafetyViolationDetected {
98        score_milli: u16,
99        threshold_milli: u16,
100    },
101    ClaimBacktracked {
102        claim_index: u32,
103    },
104    SafetyDisabled {
105        reason: DegradeReason,
106    },
107
108    // --- 6. Memory Management ---
109    MemoryPlanCreated {
110        total_bytes: u64,
111        sram_bytes: u64,
112        dram_bytes: u64,
113    },
114    KvCacheCompacted {
115        reclaimed: u32,
116    },
117    MemoryBudgetExceeded {
118        requested_bytes: u64,
119        budget_bytes: u64,
120    },
121
122    // --- 7. Hardware & Delegate ---
123    DeviceProfiled {
124        profile: DeviceTarget,
125        npu_tops: u16,
126        bandwidth_gbs: u16,
127    },
128    DelegateSelected {
129        partitions: u8,
130    },
131    DelegateFellBack,
132
133    // --- 8. Model Provenance ---
134    ModelSignatureVerified {
135        model: ModelId,
136        version: ModelVersion,
137    },
138    ModelSignatureRejected {
139        model: ModelId,
140    },
141
142    // --- 9. Telemetry ---
143    MetricsSampled {
144        decode_tps: u32,
145        ttft_ms: u32,
146        peak_bytes: u64,
147    },
148
149    // --- 10. Frontier LLM (ADR-010 opt-in cloud egress) ---
150    /// Emitted when the opt-in cloud backend is consulted (parallel to
151    /// `HybridRelayConsulted`). `provider_hash` is a CRC32 of the provider
152    /// prefix — not the API key or any content.
153    FrontierLlmConsulted {
154        provider_hash: u32,
155        prompt_tokens: u32,
156        completion_tokens: u32,
157    },
158}
159
160/// Standard envelope (`docs/ddd/domain-events.md`): every event is correlated by
161/// `SessionId` and ordered by a logical step index.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub struct EventEnvelope {
164    pub session: SessionId,
165    pub step: u32,
166    pub event: DomainEvent,
167}
168
169impl EventEnvelope {
170    pub fn new(session: SessionId, step: u32, event: DomainEvent) -> Self {
171        Self {
172            session,
173            step,
174            event,
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    // Compile-time proof that events are content-free: this only compiles if
184    // `DomainEvent: Copy`, which is impossible if any field owns heap memory.
185    fn _assert_copy<T: Copy>() {}
186    #[test]
187    fn events_are_content_free_by_construction() {
188        _assert_copy::<DomainEvent>();
189        _assert_copy::<EventEnvelope>();
190    }
191
192    #[test]
193    fn envelope_carries_step_and_session() {
194        let e = EventEnvelope::new(
195            SessionId(1),
196            4,
197            DomainEvent::TokenGenerated { sampled: true },
198        );
199        assert_eq!(e.step, 4);
200        assert_eq!(e.session, SessionId(1));
201    }
202}