1use serde::{Deserialize, Serialize};
2
3pub fn now_ms() -> u64 {
5 std::time::SystemTime::now()
6 .duration_since(std::time::UNIX_EPOCH)
7 .unwrap_or_default()
8 .as_millis() as u64
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
13pub struct UsageData {
14 #[serde(skip_serializing_if = "Option::is_none")]
15 pub input_tokens: Option<u64>,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub output_tokens: Option<u64>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub cache_read_tokens: Option<u64>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub cache_creation_tokens: Option<u64>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub cost_usd: Option<f64>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
32#[serde(tag = "type", rename_all = "snake_case")]
33pub enum Event {
34 SessionStart(SessionStartEvent),
36
37 TextDelta(TextDeltaEvent),
39
40 Message(MessageEvent),
42
43 ToolStart(ToolStartEvent),
45
46 ToolEnd(ToolEndEvent),
48
49 UsageDelta(UsageDeltaEvent),
51
52 Result(ResultEvent),
54
55 Error(ErrorEvent),
57}
58
59impl Event {
60 pub fn stamp(self) -> Self {
62 let ts = now_ms();
63 match self {
64 Event::SessionStart(mut e) => { e.timestamp_ms = ts; Event::SessionStart(e) }
65 Event::TextDelta(mut e) => { e.timestamp_ms = ts; Event::TextDelta(e) }
66 Event::Message(mut e) => { e.timestamp_ms = ts; Event::Message(e) }
67 Event::ToolStart(mut e) => { e.timestamp_ms = ts; Event::ToolStart(e) }
68 Event::ToolEnd(mut e) => { e.timestamp_ms = ts; Event::ToolEnd(e) }
69 Event::UsageDelta(mut e) => { e.timestamp_ms = ts; Event::UsageDelta(e) }
70 Event::Result(mut e) => { e.timestamp_ms = ts; Event::Result(e) }
71 Event::Error(mut e) => { e.timestamp_ms = ts; Event::Error(e) }
72 }
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
77pub struct SessionStartEvent {
78 pub session_id: String,
79 pub agent: String,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub model: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub cwd: Option<String>,
84 #[serde(default)]
85 pub timestamp_ms: u64,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
89pub struct TextDeltaEvent {
90 pub text: String,
91 #[serde(default)]
92 pub timestamp_ms: u64,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
96pub struct MessageEvent {
97 pub role: Role,
98 pub text: String,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub usage: Option<UsageData>,
101 #[serde(default)]
102 pub timestamp_ms: u64,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
106#[serde(rename_all = "snake_case")]
107pub enum Role {
108 Assistant,
109 User,
110 System,
111}
112
113impl std::fmt::Display for Role {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 match self {
116 Role::Assistant => f.write_str("assistant"),
117 Role::User => f.write_str("user"),
118 Role::System => f.write_str("system"),
119 }
120 }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124pub struct ToolStartEvent {
125 pub call_id: String,
126 pub tool_name: String,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub input: Option<serde_json::Value>,
129 #[serde(default)]
130 pub timestamp_ms: u64,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
134pub struct ToolEndEvent {
135 pub call_id: String,
136 pub tool_name: String,
137 pub success: bool,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub output: Option<String>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub usage: Option<UsageData>,
142 #[serde(default)]
143 pub timestamp_ms: u64,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
148pub struct UsageDeltaEvent {
149 pub usage: UsageData,
150 #[serde(default)]
151 pub timestamp_ms: u64,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
155pub struct ResultEvent {
156 pub success: bool,
157 pub text: String,
158 pub session_id: String,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub duration_ms: Option<u64>,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub total_cost_usd: Option<f64>,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub usage: Option<UsageData>,
165 #[serde(default)]
166 pub timestamp_ms: u64,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
170pub struct ErrorEvent {
171 pub message: String,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub code: Option<String>,
174 #[serde(default)]
175 pub timestamp_ms: u64,
176}
177
178pub fn sum_costs(events: &[Event]) -> f64 {
182 let mut total = 0.0;
183 for event in events {
184 match event {
185 Event::UsageDelta(u) => {
186 if let Some(c) = u.usage.cost_usd {
187 total += c;
188 }
189 }
190 Event::Result(r) => {
191 if let Some(c) = r.total_cost_usd {
192 total += c;
193 }
194 }
195 _ => {}
196 }
197 }
198 total
199}
200
201pub fn total_tokens(events: &[Event]) -> (u64, u64) {
203 let mut input = 0u64;
204 let mut output = 0u64;
205 for event in events {
206 if let Event::UsageDelta(u) = event {
207 if let Some(i) = u.usage.input_tokens {
208 input += i;
209 }
210 if let Some(o) = u.usage.output_tokens {
211 output += o;
212 }
213 }
214 }
215 (input, output)
216}
217
218pub fn extract_tool_calls(events: &[Event]) -> Vec<(&ToolStartEvent, Option<&ToolEndEvent>)> {
220 let mut starts: Vec<(&ToolStartEvent, Option<&ToolEndEvent>)> = Vec::new();
221 for event in events {
222 if let Event::ToolStart(ts) = event {
223 starts.push((ts, None));
224 }
225 }
226 for event in events {
227 if let Event::ToolEnd(te) = event {
228 for (ts, end) in &mut starts {
229 if ts.call_id == te.call_id && end.is_none() {
230 *end = Some(te);
231 break;
232 }
233 }
234 }
235 }
236 starts
237}
238
239impl std::fmt::Display for Event {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 match self {
242 Event::SessionStart(e) => write!(f, "[session:{}] agent={}", e.session_id, e.agent),
243 Event::TextDelta(e) => write!(f, "{}", e.text),
244 Event::Message(e) => write!(f, "[{}] {}", e.role, e.text),
245 Event::ToolStart(e) => write!(f, "[tool:start] {}({})", e.tool_name, e.call_id),
246 Event::ToolEnd(e) => {
247 let status = if e.success { "ok" } else { "fail" };
248 write!(f, "[tool:{}] {}({})", status, e.tool_name, e.call_id)
249 }
250 Event::UsageDelta(e) => {
251 let input = e.usage.input_tokens.unwrap_or(0);
252 let output = e.usage.output_tokens.unwrap_or(0);
253 write!(f, "[usage] {input} in / {output} out")
254 }
255 Event::Result(e) => {
256 let status = if e.success { "success" } else { "error" };
257 write!(f, "[result:{}] {}", status, e.text)
258 }
259 Event::Error(e) => write!(f, "[error] {}", e.message),
260 }
261 }
262}