1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
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 },
91
92 #[serde(rename = "agent.opened_port")]
93 AgentOpenedPort {
94 port: u16,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 protocol: Option<String>,
97 },
98
99 #[serde(rename = "agent.connected_network")]
100 AgentConnectedNetwork {
101 destination: String,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 port: Option<u16>,
104 },
105
106 #[serde(rename = "agent.started_process")]
107 AgentStartedProcess {
108 process_name: String,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pid: Option<u32>,
111 },
112
113 #[serde(rename = "agent.completed_process")]
114 AgentCompletedProcess {
115 process_name: String,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 exit_code: Option<i32>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 duration_ms: Option<u64>,
120 },
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct SessionEvent {
126 pub session_id: String,
127 pub event_id: String,
128 pub timestamp: String,
129 pub sequence_no: u64,
130 pub trace_id: String,
131 pub span_id: String,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub parent_span_id: Option<String>,
134 pub agent_id: String,
135 pub agent_instance_id: String,
136 pub agent_name: String,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub agent_role: Option<String>,
139 pub host_id: String,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub tool_runtime_id: Option<String>,
142 #[serde(flatten)]
143 pub event_type: EventType,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub artifact_ref: Option<String>,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub meta: Option<serde_json::Value>,
148}
149
150pub fn generate_event_id() -> String {
152 let mut buf = [0u8; 8];
153 use rand::RngCore;
154 rand::thread_rng().fill_bytes(&mut buf);
155 format!("evt_{}", hex::encode(buf))
156}
157
158pub fn generate_span_id() -> String {
160 let mut buf = [0u8; 8];
161 use rand::RngCore;
162 rand::thread_rng().fill_bytes(&mut buf);
163 hex::encode(buf)
164}
165
166pub fn generate_trace_id() -> String {
168 let mut buf = [0u8; 16];
169 use rand::RngCore;
170 rand::thread_rng().fill_bytes(&mut buf);
171 hex::encode(buf)
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn event_type_serialization() {
180 let evt = EventType::AgentCalledTool {
181 tool_name: "read_file".into(),
182 tool_input_digest: None,
183 tool_output_digest: None,
184 duration_ms: Some(42),
185 };
186 let json = serde_json::to_string(&evt).unwrap();
187 assert!(json.contains("agent.called_tool"));
188 assert!(json.contains("read_file"));
189
190 let back: EventType = serde_json::from_str(&json).unwrap();
191 assert_eq!(back, evt);
192 }
193
194 #[test]
195 fn full_event_roundtrip() {
196 let event = SessionEvent {
197 session_id: "ssn_001".into(),
198 event_id: generate_event_id(),
199 timestamp: "2026-04-05T08:00:00Z".into(),
200 sequence_no: 1,
201 trace_id: generate_trace_id(),
202 span_id: generate_span_id(),
203 parent_span_id: None,
204 agent_id: "agent://claude-code".into(),
205 agent_instance_id: "ai_cc_1".into(),
206 agent_name: "claude-code".into(),
207 agent_role: Some("planner".into()),
208 host_id: "host_macbook".into(),
209 tool_runtime_id: Some("rt_cc_1".into()),
210 event_type: EventType::AgentStarted {
211 parent_agent_instance_id: None,
212 },
213 artifact_ref: None,
214 meta: None,
215 };
216
217 let json = serde_json::to_string_pretty(&event).unwrap();
218 let back: SessionEvent = serde_json::from_str(&json).unwrap();
219 assert_eq!(back.session_id, "ssn_001");
220 assert_eq!(back.agent_name, "claude-code");
221 }
222
223 #[test]
224 fn id_generation() {
225 let eid = generate_event_id();
226 assert!(eid.starts_with("evt_"));
227 assert_eq!(eid.len(), 4 + 16); let sid = generate_span_id();
230 assert_eq!(sid.len(), 16);
231
232 let tid = generate_trace_id();
233 assert_eq!(tid.len(), 32);
234 }
235}