mojentic/tracer/
tracer_events.rs

1//! Tracer event types for tracking system interactions
2//!
3//! This module defines the core event types used by the tracer system to record
4//! LLM calls, tool executions, and agent interactions. All events implement the
5//! `TracerEvent` trait which provides timestamps, correlation IDs, and printable summaries.
6
7use chrono::{DateTime, Local};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Trait for filtering tracer events
12///
13/// Implement this trait to create custom event filters. This trait is used
14/// instead of raw closure types to avoid type complexity warnings.
15pub trait EventFilterFn: Send + Sync {
16    /// Test whether an event passes the filter
17    fn matches(&self, event: &dyn TracerEvent) -> bool;
18}
19
20/// Implement EventFilterFn for any function that matches the signature
21impl<F> EventFilterFn for F
22where
23    F: Fn(&dyn TracerEvent) -> bool + Send + Sync,
24{
25    fn matches(&self, event: &dyn TracerEvent) -> bool {
26        self(event)
27    }
28}
29
30/// Base trait for all tracer events
31///
32/// Tracer events are used to track system interactions for observability purposes.
33/// They are distinct from regular events which are used for agent communication.
34pub trait TracerEvent: Send + Sync {
35    /// Get the timestamp when the event occurred
36    fn timestamp(&self) -> f64;
37
38    /// Get the correlation ID for tracing related events
39    fn correlation_id(&self) -> &str;
40
41    /// Get the source of the event
42    fn source(&self) -> &str;
43
44    /// Get a formatted string summary of the event
45    fn printable_summary(&self) -> String;
46}
47
48/// Records when an LLM is called with specific messages
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct LlmCallTracerEvent {
51    /// Timestamp when the event occurred (Unix timestamp)
52    pub timestamp: f64,
53    /// UUID string that is copied from cause-to-affect for tracing events
54    pub correlation_id: String,
55    /// Source of the event
56    pub source: String,
57    /// The LLM model that was used
58    pub model: String,
59    /// The messages sent to the LLM (simplified representation)
60    pub messages: Vec<HashMap<String, serde_json::Value>>,
61    /// The temperature setting used for the call
62    pub temperature: f64,
63    /// The tools available to the LLM, if any
64    pub tools: Option<Vec<HashMap<String, serde_json::Value>>>,
65}
66
67impl TracerEvent for LlmCallTracerEvent {
68    fn timestamp(&self) -> f64 {
69        self.timestamp
70    }
71
72    fn correlation_id(&self) -> &str {
73        &self.correlation_id
74    }
75
76    fn source(&self) -> &str {
77        &self.source
78    }
79
80    fn printable_summary(&self) -> String {
81        let dt = DateTime::from_timestamp(self.timestamp as i64, 0)
82            .unwrap_or_else(|| DateTime::from_timestamp(0, 0).unwrap())
83            .with_timezone(&Local);
84        let time_str = dt.format("%H:%M:%S%.3f").to_string();
85
86        let mut summary = format!(
87            "[{}] LlmCallTracerEvent (correlation_id: {})\n   Model: {}",
88            time_str, self.correlation_id, self.model
89        );
90
91        if !self.messages.is_empty() {
92            let msg_count = self.messages.len();
93            let plural = if msg_count != 1 { "s" } else { "" };
94            summary.push_str(&format!("\n   Messages: {} message{}", msg_count, plural));
95        }
96
97        if (self.temperature - 1.0).abs() > f64::EPSILON {
98            summary.push_str(&format!("\n   Temperature: {}", self.temperature));
99        }
100
101        if let Some(tools) = &self.tools {
102            let tool_names: Vec<String> = tools
103                .iter()
104                .filter_map(|t| t.get("name").and_then(|n| n.as_str()).map(|s| s.to_string()))
105                .collect();
106            if !tool_names.is_empty() {
107                summary.push_str(&format!("\n   Available Tools: {}", tool_names.join(", ")));
108            }
109        }
110
111        summary
112    }
113}
114
115/// Records when an LLM responds to a call
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct LlmResponseTracerEvent {
118    /// Timestamp when the event occurred (Unix timestamp)
119    pub timestamp: f64,
120    /// UUID string that is copied from cause-to-affect for tracing events
121    pub correlation_id: String,
122    /// Source of the event
123    pub source: String,
124    /// The LLM model that was used
125    pub model: String,
126    /// The content of the LLM response
127    pub content: String,
128    /// Any tool calls made by the LLM
129    pub tool_calls: Option<Vec<HashMap<String, serde_json::Value>>>,
130    /// Duration of the LLM call in milliseconds
131    pub call_duration_ms: Option<f64>,
132}
133
134impl TracerEvent for LlmResponseTracerEvent {
135    fn timestamp(&self) -> f64 {
136        self.timestamp
137    }
138
139    fn correlation_id(&self) -> &str {
140        &self.correlation_id
141    }
142
143    fn source(&self) -> &str {
144        &self.source
145    }
146
147    fn printable_summary(&self) -> String {
148        let dt = DateTime::from_timestamp(self.timestamp as i64, 0)
149            .unwrap_or_else(|| DateTime::from_timestamp(0, 0).unwrap())
150            .with_timezone(&Local);
151        let time_str = dt.format("%H:%M:%S%.3f").to_string();
152
153        let mut summary = format!(
154            "[{}] LlmResponseTracerEvent (correlation_id: {})\n   Model: {}",
155            time_str, self.correlation_id, self.model
156        );
157
158        if !self.content.is_empty() {
159            let content_preview = if self.content.len() > 100 {
160                format!("{}...", &self.content[..100])
161            } else {
162                self.content.clone()
163            };
164            summary.push_str(&format!("\n   Content: {}", content_preview));
165        }
166
167        if let Some(tool_calls) = &self.tool_calls {
168            let tool_count = tool_calls.len();
169            let plural = if tool_count != 1 { "s" } else { "" };
170            summary.push_str(&format!("\n   Tool Calls: {} call{}", tool_count, plural));
171        }
172
173        if let Some(duration) = self.call_duration_ms {
174            summary.push_str(&format!("\n   Duration: {:.2}ms", duration));
175        }
176
177        summary
178    }
179}
180
181/// Records when a tool is called during agent execution
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct ToolCallTracerEvent {
184    /// Timestamp when the event occurred (Unix timestamp)
185    pub timestamp: f64,
186    /// UUID string that is copied from cause-to-affect for tracing events
187    pub correlation_id: String,
188    /// Source of the event
189    pub source: String,
190    /// Name of the tool that was called
191    pub tool_name: String,
192    /// Arguments provided to the tool
193    pub arguments: HashMap<String, serde_json::Value>,
194    /// Result returned by the tool (as JSON value)
195    pub result: serde_json::Value,
196    /// Name of the agent or component that called the tool
197    pub caller: Option<String>,
198    /// Duration of the tool call in milliseconds
199    pub call_duration_ms: Option<f64>,
200}
201
202impl TracerEvent for ToolCallTracerEvent {
203    fn timestamp(&self) -> f64 {
204        self.timestamp
205    }
206
207    fn correlation_id(&self) -> &str {
208        &self.correlation_id
209    }
210
211    fn source(&self) -> &str {
212        &self.source
213    }
214
215    fn printable_summary(&self) -> String {
216        let dt = DateTime::from_timestamp(self.timestamp as i64, 0)
217            .unwrap_or_else(|| DateTime::from_timestamp(0, 0).unwrap())
218            .with_timezone(&Local);
219        let time_str = dt.format("%H:%M:%S%.3f").to_string();
220
221        let mut summary = format!(
222            "[{}] ToolCallTracerEvent (correlation_id: {})\n   Tool: {}",
223            time_str, self.correlation_id, self.tool_name
224        );
225
226        if !self.arguments.is_empty() {
227            summary.push_str(&format!("\n   Arguments: {:?}", self.arguments));
228        }
229
230        let result_str = self.result.to_string();
231        let result_preview = if result_str.len() > 100 {
232            format!("{}...", &result_str[..100])
233        } else {
234            result_str
235        };
236        summary.push_str(&format!("\n   Result: {}", result_preview));
237
238        if let Some(caller) = &self.caller {
239            summary.push_str(&format!("\n   Caller: {}", caller));
240        }
241
242        if let Some(duration) = self.call_duration_ms {
243            summary.push_str(&format!("\n   Duration: {:.2}ms", duration));
244        }
245
246        summary
247    }
248}
249
250/// Records interactions between agents
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct AgentInteractionTracerEvent {
253    /// Timestamp when the event occurred (Unix timestamp)
254    pub timestamp: f64,
255    /// UUID string that is copied from cause-to-affect for tracing events
256    pub correlation_id: String,
257    /// Source of the event
258    pub source: String,
259    /// Name of the agent sending the event
260    pub from_agent: String,
261    /// Name of the agent receiving the event
262    pub to_agent: String,
263    /// Type of event being processed
264    pub event_type: String,
265    /// Unique identifier for the event
266    pub event_id: Option<String>,
267}
268
269impl TracerEvent for AgentInteractionTracerEvent {
270    fn timestamp(&self) -> f64 {
271        self.timestamp
272    }
273
274    fn correlation_id(&self) -> &str {
275        &self.correlation_id
276    }
277
278    fn source(&self) -> &str {
279        &self.source
280    }
281
282    fn printable_summary(&self) -> String {
283        let dt = DateTime::from_timestamp(self.timestamp as i64, 0)
284            .unwrap_or_else(|| DateTime::from_timestamp(0, 0).unwrap())
285            .with_timezone(&Local);
286        let time_str = dt.format("%H:%M:%S%.3f").to_string();
287
288        let mut summary = format!(
289            "[{}] AgentInteractionTracerEvent (correlation_id: {})\n   From: {} → To: {}\n   Event Type: {}",
290            time_str, self.correlation_id, self.from_agent, self.to_agent, self.event_type
291        );
292
293        if let Some(event_id) = &self.event_id {
294            summary.push_str(&format!("\n   Event ID: {}", event_id));
295        }
296
297        summary
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use std::time::{SystemTime, UNIX_EPOCH};
305
306    fn current_timestamp() -> f64 {
307        SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs_f64()
308    }
309
310    #[test]
311    fn test_llm_call_event() {
312        let event = LlmCallTracerEvent {
313            timestamp: current_timestamp(),
314            correlation_id: "test-123".to_string(),
315            source: "test".to_string(),
316            model: "llama3.2".to_string(),
317            messages: vec![],
318            temperature: 0.7,
319            tools: None,
320        };
321
322        assert_eq!(event.correlation_id(), "test-123");
323        assert_eq!(event.model, "llama3.2");
324        assert!((event.temperature - 0.7).abs() < f64::EPSILON);
325
326        let summary = event.printable_summary();
327        assert!(summary.contains("LlmCallTracerEvent"));
328        assert!(summary.contains("test-123"));
329        assert!(summary.contains("llama3.2"));
330    }
331
332    #[test]
333    fn test_llm_response_event() {
334        let event = LlmResponseTracerEvent {
335            timestamp: current_timestamp(),
336            correlation_id: "test-456".to_string(),
337            source: "test".to_string(),
338            model: "llama3.2".to_string(),
339            content: "Hello, world!".to_string(),
340            tool_calls: None,
341            call_duration_ms: Some(150.5),
342        };
343
344        assert_eq!(event.content, "Hello, world!");
345        assert_eq!(event.call_duration_ms, Some(150.5));
346
347        let summary = event.printable_summary();
348        assert!(summary.contains("LlmResponseTracerEvent"));
349        assert!(summary.contains("Hello, world!"));
350        assert!(summary.contains("150.5"));
351    }
352
353    #[test]
354    fn test_tool_call_event() {
355        let mut args = HashMap::new();
356        args.insert("input".to_string(), serde_json::json!("test"));
357
358        let event = ToolCallTracerEvent {
359            timestamp: current_timestamp(),
360            correlation_id: "test-789".to_string(),
361            source: "test".to_string(),
362            tool_name: "example_tool".to_string(),
363            arguments: args,
364            result: serde_json::json!({"output": "result"}),
365            caller: Some("agent1".to_string()),
366            call_duration_ms: Some(25.0),
367        };
368
369        assert_eq!(event.tool_name, "example_tool");
370        assert_eq!(event.caller, Some("agent1".to_string()));
371
372        let summary = event.printable_summary();
373        assert!(summary.contains("ToolCallTracerEvent"));
374        assert!(summary.contains("example_tool"));
375    }
376
377    #[test]
378    fn test_agent_interaction_event() {
379        let event = AgentInteractionTracerEvent {
380            timestamp: current_timestamp(),
381            correlation_id: "test-abc".to_string(),
382            source: "test".to_string(),
383            from_agent: "agent1".to_string(),
384            to_agent: "agent2".to_string(),
385            event_type: "message".to_string(),
386            event_id: Some("evt-123".to_string()),
387        };
388
389        assert_eq!(event.from_agent, "agent1");
390        assert_eq!(event.to_agent, "agent2");
391
392        let summary = event.printable_summary();
393        assert!(summary.contains("AgentInteractionTracerEvent"));
394        assert!(summary.contains("agent1"));
395        assert!(summary.contains("agent2"));
396    }
397}