1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10#[serde(tag = "type", rename_all = "snake_case")]
11pub enum EventType {
12 #[serde(rename = "session.started")]
13 SessionStarted,
14
15 #[serde(rename = "session.closed")]
16 SessionClosed {
17 #[serde(skip_serializing_if = "Option::is_none")]
18 summary: Option<String>,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 duration_ms: Option<u64>,
21 },
22
23 #[serde(rename = "agent.started")]
24 AgentStarted {
25 #[serde(skip_serializing_if = "Option::is_none")]
26 parent_agent_instance_id: Option<String>,
27 },
28
29 #[serde(rename = "agent.spawned")]
30 AgentSpawned {
31 spawned_by_agent_instance_id: String,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 reason: Option<String>,
34 },
35
36 #[serde(rename = "agent.handoff")]
37 AgentHandoff {
38 from_agent_instance_id: String,
39 to_agent_instance_id: String,
40 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 artifacts: Vec<String>,
42 },
43
44 #[serde(rename = "agent.collaborated")]
45 AgentCollaborated {
46 #[serde(default, skip_serializing_if = "Vec::is_empty")]
47 collaborator_agent_instance_ids: Vec<String>,
48 },
49
50 #[serde(rename = "agent.returned")]
51 AgentReturned {
52 returned_to_agent_instance_id: String,
53 },
54
55 #[serde(rename = "agent.completed")]
56 AgentCompleted {
57 #[serde(skip_serializing_if = "Option::is_none")]
58 termination_reason: Option<String>,
59 },
60
61 #[serde(rename = "agent.failed")]
62 AgentFailed {
63 #[serde(skip_serializing_if = "Option::is_none")]
64 reason: Option<String>,
65 },
66
67 #[serde(rename = "agent.called_tool")]
68 AgentCalledTool {
69 tool_name: String,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 tool_input_digest: Option<String>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 tool_output_digest: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 duration_ms: Option<u64>,
76 },
77
78 #[serde(rename = "agent.read_file")]
79 AgentReadFile {
80 file_path: String,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 digest: Option<String>,
83 },
84
85 #[serde(rename = "agent.wrote_file")]
86 AgentWroteFile {
87 file_path: String,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 digest: Option<String>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 operation: Option<String>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 additions: Option<u32>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 deletions: Option<u32>,
97 },
98
99 #[serde(rename = "agent.opened_port")]
100 AgentOpenedPort {
101 port: u16,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 protocol: Option<String>,
104 },
105
106 #[serde(rename = "agent.connected_network")]
107 AgentConnectedNetwork {
108 destination: String,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 port: Option<u16>,
111 },
112
113 #[serde(rename = "agent.started_process")]
114 AgentStartedProcess {
115 process_name: String,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pid: Option<u32>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 command: Option<String>,
121 },
122
123 #[serde(rename = "agent.completed_process")]
124 AgentCompletedProcess {
125 process_name: String,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 exit_code: Option<i32>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 duration_ms: Option<u64>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 command: Option<String>,
132 },
133
134 #[serde(rename = "agent.decision")]
136 AgentDecision {
137 #[serde(skip_serializing_if = "Option::is_none")]
138 model: Option<String>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 tokens_in: Option<u64>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 tokens_out: Option<u64>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 cost_usd: Option<f64>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 summary: Option<String>,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 confidence: Option<f64>,
149 },
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct SessionEvent {
155 pub session_id: String,
156 pub event_id: String,
157 pub timestamp: String,
158 pub sequence_no: u64,
159 pub trace_id: String,
160 pub span_id: String,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub parent_span_id: Option<String>,
163 pub agent_id: String,
164 pub agent_instance_id: String,
165 pub agent_name: String,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub agent_role: Option<String>,
168 pub host_id: String,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub tool_runtime_id: Option<String>,
171 #[serde(flatten)]
172 pub event_type: EventType,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub artifact_ref: Option<String>,
175 #[serde(skip_serializing_if = "Option::is_none")]
176 pub meta: Option<serde_json::Value>,
177}
178
179pub fn generate_event_id() -> String {
181 let mut buf = [0u8; 8];
182 use rand::RngCore;
183 rand::thread_rng().fill_bytes(&mut buf);
184 format!("evt_{}", hex::encode(buf))
185}
186
187pub fn generate_span_id() -> String {
189 let mut buf = [0u8; 8];
190 use rand::RngCore;
191 rand::thread_rng().fill_bytes(&mut buf);
192 hex::encode(buf)
193}
194
195pub fn generate_trace_id() -> String {
197 let mut buf = [0u8; 16];
198 use rand::RngCore;
199 rand::thread_rng().fill_bytes(&mut buf);
200 hex::encode(buf)
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn event_type_serialization() {
209 let evt = EventType::AgentCalledTool {
210 tool_name: "read_file".into(),
211 tool_input_digest: None,
212 tool_output_digest: None,
213 duration_ms: Some(42),
214 };
215 let json = serde_json::to_string(&evt).unwrap();
216 assert!(json.contains("agent.called_tool"));
217 assert!(json.contains("read_file"));
218
219 let back: EventType = serde_json::from_str(&json).unwrap();
220 assert_eq!(back, evt);
221 }
222
223 #[test]
224 fn full_event_roundtrip() {
225 let event = SessionEvent {
226 session_id: "ssn_001".into(),
227 event_id: generate_event_id(),
228 timestamp: "2026-04-05T08:00:00Z".into(),
229 sequence_no: 1,
230 trace_id: generate_trace_id(),
231 span_id: generate_span_id(),
232 parent_span_id: None,
233 agent_id: "agent://claude-code".into(),
234 agent_instance_id: "ai_cc_1".into(),
235 agent_name: "claude-code".into(),
236 agent_role: Some("planner".into()),
237 host_id: "host_macbook".into(),
238 tool_runtime_id: Some("rt_cc_1".into()),
239 event_type: EventType::AgentStarted {
240 parent_agent_instance_id: None,
241 },
242 artifact_ref: None,
243 meta: None,
244 };
245
246 let json = serde_json::to_string_pretty(&event).unwrap();
247 let back: SessionEvent = serde_json::from_str(&json).unwrap();
248 assert_eq!(back.session_id, "ssn_001");
249 assert_eq!(back.agent_name, "claude-code");
250 }
251
252 #[test]
253 fn id_generation() {
254 let eid = generate_event_id();
255 assert!(eid.starts_with("evt_"));
256 assert_eq!(eid.len(), 4 + 16); let sid = generate_span_id();
259 assert_eq!(sid.len(), 16);
260
261 let tid = generate_trace_id();
262 assert_eq!(tid.len(), 32);
263 }
264}