Skip to main content

mnemo_core/model/
event.rs

1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5pub struct AgentEvent {
6    pub id: Uuid,
7    pub agent_id: String,
8    pub thread_id: Option<String>,
9    pub run_id: Option<String>,
10    pub parent_event_id: Option<Uuid>,
11    pub event_type: EventType,
12    pub payload: serde_json::Value,
13    // OTel fields
14    pub trace_id: Option<String>,
15    pub span_id: Option<String>,
16    pub model: Option<String>,
17    pub tokens_input: Option<i64>,
18    pub tokens_output: Option<i64>,
19    pub latency_ms: Option<i64>,
20    pub cost_usd: Option<f64>,
21    // Temporal
22    pub timestamp: String,
23    pub logical_clock: i64,
24    // Integrity
25    pub content_hash: Vec<u8>,
26    pub prev_hash: Option<Vec<u8>>,
27    // Optional embedding of the event payload
28    pub embedding: Option<Vec<f32>>,
29}
30
31impl AgentEvent {
32    /// Create a new `AgentEvent` with required fields; all optional fields default to `None`.
33    pub fn new(
34        agent_id: String,
35        event_type: EventType,
36        payload: serde_json::Value,
37        timestamp: String,
38        content_hash: Vec<u8>,
39    ) -> Self {
40        Self {
41            id: Uuid::now_v7(),
42            agent_id,
43            thread_id: None,
44            run_id: None,
45            parent_event_id: None,
46            event_type,
47            payload,
48            trace_id: None,
49            span_id: None,
50            model: None,
51            tokens_input: None,
52            tokens_output: None,
53            latency_ms: None,
54            cost_usd: None,
55            timestamp,
56            logical_clock: 0,
57            content_hash,
58            prev_hash: None,
59            embedding: None,
60        }
61    }
62
63    /// Create an `AgentEvent` with all fields specified.
64    /// Intended for storage backends that reconstruct events from database rows.
65    #[allow(clippy::too_many_arguments)]
66    pub fn from_parts(
67        id: Uuid,
68        agent_id: String,
69        thread_id: Option<String>,
70        run_id: Option<String>,
71        parent_event_id: Option<Uuid>,
72        event_type: EventType,
73        payload: serde_json::Value,
74        trace_id: Option<String>,
75        span_id: Option<String>,
76        model: Option<String>,
77        tokens_input: Option<i64>,
78        tokens_output: Option<i64>,
79        latency_ms: Option<i64>,
80        cost_usd: Option<f64>,
81        timestamp: String,
82        logical_clock: i64,
83        content_hash: Vec<u8>,
84        prev_hash: Option<Vec<u8>>,
85        embedding: Option<Vec<f32>>,
86    ) -> Self {
87        Self {
88            id,
89            agent_id,
90            thread_id,
91            run_id,
92            parent_event_id,
93            event_type,
94            payload,
95            trace_id,
96            span_id,
97            model,
98            tokens_input,
99            tokens_output,
100            latency_ms,
101            cost_usd,
102            timestamp,
103            logical_clock,
104            content_hash,
105            prev_hash,
106            embedding,
107        }
108    }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum EventType {
114    MemoryWrite,
115    MemoryRead,
116    MemoryDelete,
117    MemoryShare,
118    MemoryExpired,
119    MemoryRedact,
120    Checkpoint,
121    Branch,
122    Merge,
123    UserMessage,
124    AssistantMessage,
125    ToolCall,
126    ToolResult,
127    Error,
128    RetrievalQuery,
129    RetrievalResult,
130    Decision,
131    /// `run_reflection_pass` finished successfully. Payload carries the
132    /// `ReflectionReport` so cadence-aware callers can skip when
133    /// `last_reflection_at` is too recent.
134    ReflectionCompleted,
135    /// An Auto Dream organization-report trailer was parsed and
136    /// ingested. Payload carries merged/removed/re-indexed counts.
137    DreamReportIngested,
138    /// v0.4.0 (P0-1) — emitted by `mnemo mcp-server` whenever the
139    /// catalog of MCP tools the engine is about to advertise diverges
140    /// from the operator-pinned baseline. Payload carries the verdict
141    /// (`Match` / `Drift` / `Reject`) plus added/removed/mutated
142    /// fingerprints. Direct response to arXiv 2604.20994
143    /// (function-hijacking via tool-catalog poisoning).
144    McpToolCatalogDrift,
145}
146
147impl std::fmt::Display for EventType {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        match self {
150            EventType::MemoryWrite => write!(f, "memory_write"),
151            EventType::MemoryRead => write!(f, "memory_read"),
152            EventType::MemoryDelete => write!(f, "memory_delete"),
153            EventType::MemoryShare => write!(f, "memory_share"),
154            EventType::MemoryExpired => write!(f, "memory_expired"),
155            EventType::MemoryRedact => write!(f, "memory_redact"),
156            EventType::Checkpoint => write!(f, "checkpoint"),
157            EventType::Branch => write!(f, "branch"),
158            EventType::Merge => write!(f, "merge"),
159            EventType::UserMessage => write!(f, "user_message"),
160            EventType::AssistantMessage => write!(f, "assistant_message"),
161            EventType::ToolCall => write!(f, "tool_call"),
162            EventType::ToolResult => write!(f, "tool_result"),
163            EventType::Error => write!(f, "error"),
164            EventType::RetrievalQuery => write!(f, "retrieval_query"),
165            EventType::RetrievalResult => write!(f, "retrieval_result"),
166            EventType::Decision => write!(f, "decision"),
167            EventType::ReflectionCompleted => write!(f, "reflection_completed"),
168            EventType::DreamReportIngested => write!(f, "dream_report_ingested"),
169            EventType::McpToolCatalogDrift => write!(f, "mcp_tool_catalog_drift"),
170        }
171    }
172}
173
174impl std::str::FromStr for EventType {
175    type Err = crate::error::Error;
176    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
177        match s {
178            "memory_write" => Ok(EventType::MemoryWrite),
179            "memory_read" => Ok(EventType::MemoryRead),
180            "memory_delete" => Ok(EventType::MemoryDelete),
181            "memory_share" => Ok(EventType::MemoryShare),
182            "memory_expired" => Ok(EventType::MemoryExpired),
183            "memory_redact" => Ok(EventType::MemoryRedact),
184            "checkpoint" => Ok(EventType::Checkpoint),
185            "branch" => Ok(EventType::Branch),
186            "merge" => Ok(EventType::Merge),
187            "user_message" => Ok(EventType::UserMessage),
188            "assistant_message" => Ok(EventType::AssistantMessage),
189            "tool_call" => Ok(EventType::ToolCall),
190            "tool_result" => Ok(EventType::ToolResult),
191            "error" => Ok(EventType::Error),
192            "retrieval_query" => Ok(EventType::RetrievalQuery),
193            "retrieval_result" => Ok(EventType::RetrievalResult),
194            "decision" => Ok(EventType::Decision),
195            "reflection_completed" => Ok(EventType::ReflectionCompleted),
196            "dream_report_ingested" => Ok(EventType::DreamReportIngested),
197            "mcp_tool_catalog_drift" => Ok(EventType::McpToolCatalogDrift),
198            _ => Err(crate::error::Error::Validation(format!(
199                "invalid event type: {s}"
200            ))),
201        }
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_agent_event_serde() {
211        let event = AgentEvent {
212            id: Uuid::now_v7(),
213            agent_id: "agent-1".to_string(),
214            thread_id: Some("thread-1".to_string()),
215            run_id: None,
216            parent_event_id: None,
217            event_type: EventType::MemoryWrite,
218            payload: serde_json::json!({"memory_id": "abc"}),
219            trace_id: None,
220            span_id: None,
221            model: None,
222            tokens_input: None,
223            tokens_output: None,
224            latency_ms: None,
225            cost_usd: None,
226            timestamp: "2025-01-01T00:00:00Z".to_string(),
227            logical_clock: 1,
228            content_hash: vec![1, 2, 3],
229            prev_hash: None,
230            embedding: None,
231        };
232        let json = serde_json::to_string(&event).unwrap();
233        let deserialized: AgentEvent = serde_json::from_str(&json).unwrap();
234        assert_eq!(event, deserialized);
235    }
236
237    #[test]
238    fn test_event_type_display_fromstr() {
239        assert_eq!(EventType::MemoryWrite.to_string(), "memory_write");
240        assert_eq!(
241            "memory_read".parse::<EventType>().unwrap(),
242            EventType::MemoryRead
243        );
244        assert_eq!(
245            "checkpoint".parse::<EventType>().unwrap(),
246            EventType::Checkpoint
247        );
248        assert_eq!("error".parse::<EventType>().unwrap(), EventType::Error);
249        assert!("invalid".parse::<EventType>().is_err());
250    }
251}