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")]
142 AgentDecision {
143 #[serde(skip_serializing_if = "Option::is_none")]
144 model: Option<String>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 tokens_in: Option<u64>,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 tokens_out: Option<u64>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
151 provider: Option<String>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 summary: Option<String>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 confidence: Option<f64>,
156 },
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct SessionEvent {
162 pub session_id: String,
163 pub event_id: String,
164 pub timestamp: String,
165 pub sequence_no: u64,
166 pub trace_id: String,
167 pub span_id: String,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub parent_span_id: Option<String>,
170 pub agent_id: String,
171 pub agent_instance_id: String,
172 pub agent_name: String,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub agent_role: Option<String>,
175 pub host_id: String,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub tool_runtime_id: Option<String>,
178 #[serde(flatten)]
179 pub event_type: EventType,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub artifact_ref: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub meta: Option<serde_json::Value>,
184}
185
186pub fn generate_event_id() -> String {
188 let mut buf = [0u8; 8];
189 use rand::RngCore;
190 rand::thread_rng().fill_bytes(&mut buf);
191 format!("evt_{}", hex::encode(buf))
192}
193
194pub fn generate_span_id() -> String {
196 let mut buf = [0u8; 8];
197 use rand::RngCore;
198 rand::thread_rng().fill_bytes(&mut buf);
199 hex::encode(buf)
200}
201
202pub fn generate_trace_id() -> String {
204 let mut buf = [0u8; 16];
205 use rand::RngCore;
206 rand::thread_rng().fill_bytes(&mut buf);
207 hex::encode(buf)
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn event_type_serialization() {
216 let evt = EventType::AgentCalledTool {
217 tool_name: "read_file".into(),
218 tool_input_digest: None,
219 tool_output_digest: None,
220 duration_ms: Some(42),
221 };
222 let json = serde_json::to_string(&evt).unwrap();
223 assert!(json.contains("agent.called_tool"));
224 assert!(json.contains("read_file"));
225
226 let back: EventType = serde_json::from_str(&json).unwrap();
227 assert_eq!(back, evt);
228 }
229
230 #[test]
231 fn full_event_roundtrip() {
232 let event = SessionEvent {
233 session_id: "ssn_001".into(),
234 event_id: generate_event_id(),
235 timestamp: "2026-04-05T08:00:00Z".into(),
236 sequence_no: 1,
237 trace_id: generate_trace_id(),
238 span_id: generate_span_id(),
239 parent_span_id: None,
240 agent_id: "agent://claude-code".into(),
241 agent_instance_id: "ai_cc_1".into(),
242 agent_name: "claude-code".into(),
243 agent_role: Some("planner".into()),
244 host_id: "host_macbook".into(),
245 tool_runtime_id: Some("rt_cc_1".into()),
246 event_type: EventType::AgentStarted {
247 parent_agent_instance_id: None,
248 },
249 artifact_ref: None,
250 meta: None,
251 };
252
253 let json = serde_json::to_string_pretty(&event).unwrap();
254 let back: SessionEvent = serde_json::from_str(&json).unwrap();
255 assert_eq!(back.session_id, "ssn_001");
256 assert_eq!(back.agent_name, "claude-code");
257 }
258
259 #[test]
260 fn id_generation() {
261 let eid = generate_event_id();
262 assert!(eid.starts_with("evt_"));
263 assert_eq!(eid.len(), 4 + 16); let sid = generate_span_id();
266 assert_eq!(sid.len(), 16);
267
268 let tid = generate_trace_id();
269 assert_eq!(tid.len(), 32);
270 }
271}