agtrace_types/event/
event.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use uuid::Uuid;
5
6use super::payload::EventPayload;
7use super::stream::StreamId;
8
9#[cfg(test)]
10use super::payload::UserPayload;
11
12// NOTE: Schema Design Goals
13//
14// 1. Normalization: Abstract provider-specific quirks into unified time-series events
15//    - Gemini: Unfold nested batch records into sequential events
16//    - Codex: Align async token notifications and eliminate echo duplicates
17//    - Claude: Extract embedded usage into independent events
18//
19// 2. Observability: Enable accurate cost/performance tracking
20//    - Token: Sidecar pattern + incremental detection for precise billing (no double-counting)
21//    - Latency: Measure turnaround time (T_req → T_res) from user perspective
22//
23// 3. Replayability: Reconstruct full conversation context via parent_id chain
24//    - Linked-list structure ensures deterministic history recovery regardless of parallel execution
25//
26// 4. Separation: Distinguish time-series flow (parent_id) from logical relations (tool_call_id)
27//    - Enables both "conversation replay" and "request/response mapping"
28//
29// NOTE: Intentional Limitations (Not Goals)
30//
31// - OS-level execution timestamps: Unavailable in logs; command issue time ≒ execution start
32// - Tree/branch structure: Parallel tool calls are linearized in chronological/array order
33// - Real-time token sync: Codex-style delayed tokens handled via eventual consistency (sidecar)
34// - Gemini token breakdown: Total usage attached to final generation event (no speculation)
35
36/// Agent event
37/// Maps 1:1 to database table row
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct AgentEvent {
40    /// Unique event ID
41    pub id: Uuid,
42
43    /// Session/trace ID (groups entire conversation)
44    pub session_id: Uuid,
45
46    /// Parent event ID in time-series chain (Linked List structure)
47    /// None for root events (first User input)
48    pub parent_id: Option<Uuid>,
49
50    /// Event timestamp (UTC)
51    pub timestamp: DateTime<Utc>,
52
53    /// Stream identifier (main, sidechain, subagent)
54    /// Enables parallel conversation streams within same session
55    #[serde(default)]
56    pub stream_id: StreamId,
57
58    /// Event type and content (flattened enum)
59    #[serde(flatten)]
60    pub payload: EventPayload,
61
62    /// Provider-specific raw data and debug information
63    /// Examples: Codex "call_id", Gemini "finish_reason", etc.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub metadata: Option<Value>,
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_serialization() {
74        let event = AgentEvent {
75            id: Uuid::new_v4(),
76            session_id: Uuid::new_v4(),
77            parent_id: None,
78            timestamp: Utc::now(),
79            stream_id: StreamId::Main,
80            payload: EventPayload::User(UserPayload {
81                text: "Hello".to_string(),
82            }),
83            metadata: None,
84        };
85
86        let json = serde_json::to_string(&event).unwrap();
87        let deserialized: AgentEvent = serde_json::from_str(&json).unwrap();
88
89        match deserialized.payload {
90            EventPayload::User(payload) => assert_eq!(payload.text, "Hello"),
91            _ => panic!("Wrong payload type"),
92        }
93    }
94}