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    /// v0.5.0 — a caller-driven topic-document consolidation
146    /// (`MnemoEngine::consolidate`) grouped a set of member memories into
147    /// one revisable topic document. Payload carries the topic name, the
148    /// `consolidated_from` member ids, the new `topic_document_id`, and
149    /// the document `version`. Anchored on Infini-Memory (arXiv:2606.10677).
150    MemoryConsolidated,
151    /// v0.5.0 — a topic document superseded an earlier one during a
152    /// consolidation revision. Payload carries `superseded_id` (the prior
153    /// document, retained in history), `superseded_by` (the new current
154    /// view), and the topic name. Provenance/hash-chain history is never
155    /// dropped.
156    MemoryRevised,
157}
158
159impl std::fmt::Display for EventType {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        match self {
162            EventType::MemoryWrite => write!(f, "memory_write"),
163            EventType::MemoryRead => write!(f, "memory_read"),
164            EventType::MemoryDelete => write!(f, "memory_delete"),
165            EventType::MemoryShare => write!(f, "memory_share"),
166            EventType::MemoryExpired => write!(f, "memory_expired"),
167            EventType::MemoryRedact => write!(f, "memory_redact"),
168            EventType::Checkpoint => write!(f, "checkpoint"),
169            EventType::Branch => write!(f, "branch"),
170            EventType::Merge => write!(f, "merge"),
171            EventType::UserMessage => write!(f, "user_message"),
172            EventType::AssistantMessage => write!(f, "assistant_message"),
173            EventType::ToolCall => write!(f, "tool_call"),
174            EventType::ToolResult => write!(f, "tool_result"),
175            EventType::Error => write!(f, "error"),
176            EventType::RetrievalQuery => write!(f, "retrieval_query"),
177            EventType::RetrievalResult => write!(f, "retrieval_result"),
178            EventType::Decision => write!(f, "decision"),
179            EventType::ReflectionCompleted => write!(f, "reflection_completed"),
180            EventType::DreamReportIngested => write!(f, "dream_report_ingested"),
181            EventType::McpToolCatalogDrift => write!(f, "mcp_tool_catalog_drift"),
182            EventType::MemoryConsolidated => write!(f, "memory_consolidated"),
183            EventType::MemoryRevised => write!(f, "memory_revised"),
184        }
185    }
186}
187
188impl std::str::FromStr for EventType {
189    type Err = crate::error::Error;
190    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
191        match s {
192            "memory_write" => Ok(EventType::MemoryWrite),
193            "memory_read" => Ok(EventType::MemoryRead),
194            "memory_delete" => Ok(EventType::MemoryDelete),
195            "memory_share" => Ok(EventType::MemoryShare),
196            "memory_expired" => Ok(EventType::MemoryExpired),
197            "memory_redact" => Ok(EventType::MemoryRedact),
198            "checkpoint" => Ok(EventType::Checkpoint),
199            "branch" => Ok(EventType::Branch),
200            "merge" => Ok(EventType::Merge),
201            "user_message" => Ok(EventType::UserMessage),
202            "assistant_message" => Ok(EventType::AssistantMessage),
203            "tool_call" => Ok(EventType::ToolCall),
204            "tool_result" => Ok(EventType::ToolResult),
205            "error" => Ok(EventType::Error),
206            "retrieval_query" => Ok(EventType::RetrievalQuery),
207            "retrieval_result" => Ok(EventType::RetrievalResult),
208            "decision" => Ok(EventType::Decision),
209            "reflection_completed" => Ok(EventType::ReflectionCompleted),
210            "dream_report_ingested" => Ok(EventType::DreamReportIngested),
211            "mcp_tool_catalog_drift" => Ok(EventType::McpToolCatalogDrift),
212            "memory_consolidated" => Ok(EventType::MemoryConsolidated),
213            "memory_revised" => Ok(EventType::MemoryRevised),
214            _ => Err(crate::error::Error::Validation(format!(
215                "invalid event type: {s}"
216            ))),
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_agent_event_serde() {
227        let event = AgentEvent {
228            id: Uuid::now_v7(),
229            agent_id: "agent-1".to_string(),
230            thread_id: Some("thread-1".to_string()),
231            run_id: None,
232            parent_event_id: None,
233            event_type: EventType::MemoryWrite,
234            payload: serde_json::json!({"memory_id": "abc"}),
235            trace_id: None,
236            span_id: None,
237            model: None,
238            tokens_input: None,
239            tokens_output: None,
240            latency_ms: None,
241            cost_usd: None,
242            timestamp: "2025-01-01T00:00:00Z".to_string(),
243            logical_clock: 1,
244            content_hash: vec![1, 2, 3],
245            prev_hash: None,
246            embedding: None,
247        };
248        let json = serde_json::to_string(&event).unwrap();
249        let deserialized: AgentEvent = serde_json::from_str(&json).unwrap();
250        assert_eq!(event, deserialized);
251    }
252
253    #[test]
254    fn test_event_type_display_fromstr() {
255        assert_eq!(EventType::MemoryWrite.to_string(), "memory_write");
256        assert_eq!(
257            "memory_read".parse::<EventType>().unwrap(),
258            EventType::MemoryRead
259        );
260        assert_eq!(
261            "checkpoint".parse::<EventType>().unwrap(),
262            EventType::Checkpoint
263        );
264        assert_eq!("error".parse::<EventType>().unwrap(), EventType::Error);
265        assert!("invalid".parse::<EventType>().is_err());
266    }
267}