1use chrono::{DateTime, Local};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11pub trait EventFilterFn: Send + Sync {
16 fn matches(&self, event: &dyn TracerEvent) -> bool;
18}
19
20impl<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
30pub trait TracerEvent: Send + Sync {
35 fn timestamp(&self) -> f64;
37
38 fn correlation_id(&self) -> &str;
40
41 fn source(&self) -> &str;
43
44 fn printable_summary(&self) -> String;
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct LlmCallTracerEvent {
51 pub timestamp: f64,
53 pub correlation_id: String,
55 pub source: String,
57 pub model: String,
59 pub messages: Vec<HashMap<String, serde_json::Value>>,
61 pub temperature: f64,
63 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#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct LlmResponseTracerEvent {
118 pub timestamp: f64,
120 pub correlation_id: String,
122 pub source: String,
124 pub model: String,
126 pub content: String,
128 pub tool_calls: Option<Vec<HashMap<String, serde_json::Value>>>,
130 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#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct ToolCallTracerEvent {
184 pub timestamp: f64,
186 pub correlation_id: String,
188 pub source: String,
190 pub tool_name: String,
192 pub arguments: HashMap<String, serde_json::Value>,
194 pub result: serde_json::Value,
196 pub caller: Option<String>,
198 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#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct AgentInteractionTracerEvent {
253 pub timestamp: f64,
255 pub correlation_id: String,
257 pub source: String,
259 pub from_agent: String,
261 pub to_agent: String,
263 pub event_type: String,
265 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}