Skip to main content

matrixcode_core/
event.rs

1//! MatrixCode Event Protocol
2
3use serde::{Deserialize, Serialize};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6/// Agent event
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub struct AgentEvent {
9    pub event_type: EventType,
10    pub timestamp: u64,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub data: Option<EventData>,
13}
14
15/// Event types
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17#[serde(rename_all = "snake_case")]
18pub enum EventType {
19    TextStart,
20    TextDelta,
21    TextEnd,
22    ThinkingStart,
23    ThinkingDelta,
24    ThinkingEnd,
25    ToolUseStart,
26    ToolUseInputDelta,
27    ToolUseInputEnd,
28    ToolResult,
29    SessionStarted,
30    SessionEnded,
31    SessionRestored, // Session loaded from file with token stats
32    NewSession,
33    CompressionTriggered,
34    CompressionCompleted,
35    MemoryLoaded,
36    MemoryDetected,    // Memory extracted from conversation
37    KeywordsExtracted, // Keywords extracted from context (for debug)
38    Error,
39    Usage,
40    Progress,
41    AskQuestion, // Ask tool: waiting for user input
42    DebugLog,    // Debug log entry for TUI debug panel
43}
44
45/// Event data
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47#[serde(rename_all = "snake_case")]
48pub enum EventData {
49    Text {
50        delta: String,
51    },
52    Thinking {
53        delta: String,
54        signature: Option<String>,
55    },
56    ToolUse {
57        id: String,
58        name: String,
59        input: Option<serde_json::Value>,
60    },
61    ToolUseInput {
62        id: String,
63        delta: String,
64    },
65    ToolResult {
66        tool_use_id: String,
67        name: String,
68        detail: Option<String>,
69        content: String,
70        is_error: bool,
71    },
72    Error {
73        message: String,
74        code: Option<String>,
75        source: Option<String>,
76    },
77    Usage {
78        input_tokens: u64,
79        output_tokens: u64,
80        cache_creation_input_tokens: Option<u64>,
81        cache_read_input_tokens: Option<u64>,
82    },
83    SessionRestore {
84        input_tokens: u64,
85        total_output_tokens: u64,
86        message_count: usize,
87    },
88    Progress {
89        message: String,
90        percentage: Option<u8>,
91    },
92    Compression {
93        original_tokens: u64,
94        compressed_tokens: u64,
95        ratio: f32,
96    },
97    Memory {
98        summary: String,
99        entries_count: usize,
100    },
101    Keywords {
102        keywords: Vec<String>,
103        source: String,
104    }, // Extracted keywords
105    AskQuestion {
106        question: String,
107        options: Option<serde_json::Value>,
108    },
109    DebugLog {
110        category: String,
111        message: String,
112    }, // Debug log entry
113}
114
115impl AgentEvent {
116    pub fn new(event_type: EventType) -> Self {
117        Self {
118            event_type,
119            timestamp: current_timestamp(),
120            data: None,
121        }
122    }
123
124    pub fn with_data(event_type: EventType, data: EventData) -> Self {
125        Self {
126            event_type,
127            timestamp: current_timestamp(),
128            data: Some(data),
129        }
130    }
131
132    pub fn text_delta(delta: impl Into<String>) -> Self {
133        Self::with_data(
134            EventType::TextDelta,
135            EventData::Text {
136                delta: delta.into(),
137            },
138        )
139    }
140
141    pub fn text_start() -> Self {
142        Self::new(EventType::TextStart)
143    }
144    pub fn text_end() -> Self {
145        Self::new(EventType::TextEnd)
146    }
147    pub fn thinking_start() -> Self {
148        Self::new(EventType::ThinkingStart)
149    }
150    pub fn thinking_end() -> Self {
151        Self::new(EventType::ThinkingEnd)
152    }
153    pub fn session_started() -> Self {
154        Self::new(EventType::SessionStarted)
155    }
156    pub fn session_ended() -> Self {
157        Self::new(EventType::SessionEnded)
158    }
159    pub fn session_restored(
160        input_tokens: u64,
161        total_output_tokens: u64,
162        message_count: usize,
163    ) -> Self {
164        Self::with_data(
165            EventType::SessionRestored,
166            EventData::SessionRestore {
167                input_tokens,
168                total_output_tokens,
169                message_count,
170            },
171        )
172    }
173
174    pub fn thinking_delta(delta: impl Into<String>, signature: Option<String>) -> Self {
175        Self::with_data(
176            EventType::ThinkingDelta,
177            EventData::Thinking {
178                delta: delta.into(),
179                signature,
180            },
181        )
182    }
183
184    pub fn tool_use_start(
185        id: impl Into<String>,
186        name: impl Into<String>,
187        input: Option<serde_json::Value>,
188    ) -> Self {
189        Self::with_data(
190            EventType::ToolUseStart,
191            EventData::ToolUse {
192                id: id.into(),
193                name: name.into(),
194                input,
195            },
196        )
197    }
198
199    pub fn tool_result(
200        tool_use_id: impl Into<String>,
201        name: impl Into<String>,
202        detail: Option<String>,
203        content: impl Into<String>,
204        is_error: bool,
205    ) -> Self {
206        Self::with_data(
207            EventType::ToolResult,
208            EventData::ToolResult {
209                tool_use_id: tool_use_id.into(),
210                name: name.into(),
211                detail,
212                content: content.into(),
213                is_error,
214            },
215        )
216    }
217
218    pub fn error(message: impl Into<String>, code: Option<String>, source: Option<String>) -> Self {
219        Self::with_data(
220            EventType::Error,
221            EventData::Error {
222                message: message.into(),
223                code,
224                source,
225            },
226        )
227    }
228
229    pub fn progress(message: impl Into<String>, percentage: Option<u8>) -> Self {
230        Self::with_data(
231            EventType::Progress,
232            EventData::Progress {
233                message: message.into(),
234                percentage,
235            },
236        )
237    }
238
239    pub fn usage(input_tokens: u64, output_tokens: u64) -> Self {
240        Self::with_data(
241            EventType::Usage,
242            EventData::Usage {
243                input_tokens,
244                output_tokens,
245                cache_creation_input_tokens: None,
246                cache_read_input_tokens: None,
247            },
248        )
249    }
250
251    pub fn usage_with_cache(
252        input_tokens: u64,
253        output_tokens: u64,
254        cache_read: u64,
255        cache_created: u64,
256    ) -> Self {
257        Self::with_data(
258            EventType::Usage,
259            EventData::Usage {
260                input_tokens,
261                output_tokens,
262                cache_creation_input_tokens: if cache_created > 0 {
263                    Some(cache_created)
264                } else {
265                    None
266                },
267                cache_read_input_tokens: if cache_read > 0 {
268                    Some(cache_read)
269                } else {
270                    None
271                },
272            },
273        )
274    }
275
276    pub fn debug_log(category: impl Into<String>, message: impl Into<String>) -> Self {
277        Self::with_data(
278            EventType::DebugLog,
279            EventData::DebugLog {
280                category: category.into(),
281                message: message.into(),
282            },
283        )
284    }
285
286    pub fn to_json(&self) -> Result<String, serde_json::Error> {
287        serde_json::to_string(self)
288    }
289    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
290        serde_json::from_str(json)
291    }
292}
293
294fn current_timestamp() -> u64 {
295    SystemTime::now()
296        .duration_since(UNIX_EPOCH)
297        .unwrap_or_default()
298        .as_millis() as u64
299}
300
301#[derive(Debug, Default)]
302pub struct EventCollector {
303    events: Vec<AgentEvent>,
304}
305
306impl EventCollector {
307    pub fn new() -> Self {
308        Self::default()
309    }
310    pub fn push(&mut self, event: AgentEvent) {
311        self.events.push(event);
312    }
313    pub fn events(&self) -> &[AgentEvent] {
314        &self.events
315    }
316    pub fn len(&self) -> usize {
317        self.events.len()
318    }
319    pub fn is_empty(&self) -> bool {
320        self.events.is_empty()
321    }
322    pub fn clear(&mut self) {
323        self.events.clear();
324    }
325    pub fn to_json_lines(&self) -> Result<Vec<String>, serde_json::Error> {
326        self.events.iter().map(|e| e.to_json()).collect()
327    }
328    pub fn output_json_lines(&self) -> Result<String, serde_json::Error> {
329        Ok(self.to_json_lines()?.join("\n"))
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    #[test]
337    fn test_event() {
338        let e = AgentEvent::text_delta("Hello");
339        assert!(e.to_json().unwrap().contains("Hello"));
340    }
341}