1use serde::{Deserialize, Serialize};
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(tag = "type")]
30pub enum Event {
31 #[serde(rename = "thread.started")]
33 ThreadStarted {
34 thread_id: String,
36 },
37
38 #[serde(rename = "turn.started")]
40 TurnStarted,
41
42 #[serde(rename = "turn.completed")]
44 TurnCompleted {
45 #[serde(default)]
47 usage: Option<Usage>,
48 },
49
50 #[serde(rename = "turn.failed")]
52 TurnFailed {
53 #[serde(default)]
55 usage: Option<Usage>,
56 #[serde(default)]
58 error: Option<ThreadError>,
59 },
60
61 #[serde(rename = "item.started")]
63 ItemStarted {
64 item: Item,
66 },
67
68 #[serde(rename = "item.updated")]
70 ItemUpdated {
71 item: Item,
73 },
74
75 #[serde(rename = "item.completed")]
77 ItemCompleted {
78 item: Item,
80 },
81
82 #[serde(rename = "token_count")]
84 TokenCount {
85 #[serde(default)]
87 input_tokens: u64,
88 #[serde(default)]
90 cached_input_tokens: u64,
91 #[serde(default)]
93 output_tokens: u64,
94 },
95
96 #[serde(rename = "error")]
98 Error {
99 #[serde(default)]
101 message: Option<String>,
102 },
103
104 #[serde(other)]
106 Unknown,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
118#[serde(tag = "type", rename_all = "snake_case")]
119pub enum Item {
120 AgentMessage {
122 #[serde(default)]
124 id: Option<String>,
125 #[serde(default)]
127 text: Option<String>,
128 },
129
130 Reasoning {
132 #[serde(default)]
134 id: Option<String>,
135 #[serde(default)]
137 text: Option<String>,
138 },
139
140 CommandExecution {
142 #[serde(default)]
144 id: Option<String>,
145 #[serde(default)]
147 command: Option<String>,
148 #[serde(default)]
150 aggregated_output: Option<String>,
151 #[serde(default)]
153 exit_code: Option<i32>,
154 #[serde(default)]
156 status: Option<String>,
157 },
158
159 FileChange {
161 #[serde(default)]
163 id: Option<String>,
164 #[serde(default)]
166 changes: Vec<FileUpdateChange>,
167 #[serde(default)]
169 status: Option<String>,
170 },
171
172 McpToolCall {
174 #[serde(default)]
176 id: Option<String>,
177 #[serde(default)]
179 server: Option<String>,
180 #[serde(default)]
182 tool: Option<String>,
183 #[serde(default)]
185 status: Option<String>,
186 },
187
188 WebSearch {
190 #[serde(default)]
192 id: Option<String>,
193 #[serde(default)]
195 query: Option<String>,
196 },
197
198 TodoList {
200 #[serde(default)]
202 id: Option<String>,
203 #[serde(default)]
205 items: Vec<TodoItem>,
206 },
207
208 Error {
210 #[serde(default)]
212 id: Option<String>,
213 #[serde(default)]
215 message: Option<String>,
216 },
217
218 #[serde(other)]
220 Unknown,
221}
222
223#[derive(Debug, Clone, Default, Serialize, Deserialize)]
229pub struct Usage {
230 #[serde(default)]
232 pub input_tokens: u64,
233 #[serde(default)]
235 pub output_tokens: u64,
236 #[serde(default)]
238 pub cached_input_tokens: u64,
239 #[serde(default)]
241 pub total_tokens: u64,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct ThreadError {
247 #[serde(default)]
249 pub message: Option<String>,
250 #[serde(default)]
252 pub code: Option<String>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct FileUpdateChange {
258 #[serde(default)]
260 pub file_path: Option<String>,
261 #[serde(default)]
263 pub old_content: Option<String>,
264 #[serde(default)]
266 pub new_content: Option<String>,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct TodoItem {
272 #[serde(default)]
274 pub text: Option<String>,
275 #[serde(default)]
277 pub completed: bool,
278}
279
280impl Event {
285 pub fn is_thread_started(&self) -> bool {
287 matches!(self, Event::ThreadStarted { .. })
288 }
289
290 pub fn is_turn_completed(&self) -> bool {
292 matches!(self, Event::TurnCompleted { .. })
293 }
294
295 pub fn is_turn_failed(&self) -> bool {
297 matches!(self, Event::TurnFailed { .. })
298 }
299
300 pub fn is_error(&self) -> bool {
302 matches!(self, Event::Error { .. })
303 }
304
305 pub fn is_item_completed(&self) -> bool {
307 matches!(self, Event::ItemCompleted { .. })
308 }
309
310 pub fn item(&self) -> Option<&Item> {
312 match self {
313 Event::ItemStarted { item }
314 | Event::ItemUpdated { item }
315 | Event::ItemCompleted { item } => Some(item),
316 _ => None,
317 }
318 }
319}
320
321impl Item {
322 pub fn id(&self) -> Option<&str> {
324 match self {
325 Item::AgentMessage { id, .. }
326 | Item::Reasoning { id, .. }
327 | Item::CommandExecution { id, .. }
328 | Item::FileChange { id, .. }
329 | Item::McpToolCall { id, .. }
330 | Item::WebSearch { id, .. }
331 | Item::TodoList { id, .. }
332 | Item::Error { id, .. } => id.as_deref(),
333 Item::Unknown => None,
334 }
335 }
336
337 pub fn text(&self) -> Option<&str> {
339 match self {
340 Item::AgentMessage { text, .. } | Item::Reasoning { text, .. } => text.as_deref(),
341 _ => None,
342 }
343 }
344}
345
346#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn deserialize_thread_started() {
356 let json = r#"{"type":"thread.started","thread_id":"thread_abc123"}"#;
357 let event: Event = serde_json::from_str(json).unwrap();
358 match event {
359 Event::ThreadStarted { thread_id } => assert_eq!(thread_id, "thread_abc123"),
360 other => panic!("expected ThreadStarted, got {other:?}"),
361 }
362 }
363
364 #[test]
365 fn deserialize_turn_started() {
366 let json = r#"{"type":"turn.started"}"#;
367 let event: Event = serde_json::from_str(json).unwrap();
368 assert!(matches!(event, Event::TurnStarted));
369 }
370
371 #[test]
372 fn deserialize_turn_started_with_extra_fields() {
373 let json = r#"{"type":"turn.started","future_field":"hello"}"#;
374 let event: Event = serde_json::from_str(json).unwrap();
375 assert!(matches!(event, Event::TurnStarted));
376 }
377
378 #[test]
379 fn deserialize_turn_completed_with_usage() {
380 let json =
381 r#"{"type":"turn.completed","usage":{"input_tokens":100,"output_tokens":50}}"#;
382 let event: Event = serde_json::from_str(json).unwrap();
383 match event {
384 Event::TurnCompleted {
385 usage: Some(usage), ..
386 } => {
387 assert_eq!(usage.input_tokens, 100);
388 assert_eq!(usage.output_tokens, 50);
389 }
390 other => panic!("expected TurnCompleted with usage, got {other:?}"),
391 }
392 }
393
394 #[test]
395 fn deserialize_turn_completed_without_usage() {
396 let json = r#"{"type":"turn.completed"}"#;
397 let event: Event = serde_json::from_str(json).unwrap();
398 match event {
399 Event::TurnCompleted { usage: None } => {}
400 other => panic!("expected TurnCompleted without usage, got {other:?}"),
401 }
402 }
403
404 #[test]
405 fn deserialize_turn_failed() {
406 let json = r#"{"type":"turn.failed","error":{"message":"rate limited","code":"rate_limit"}}"#;
407 let event: Event = serde_json::from_str(json).unwrap();
408 match event {
409 Event::TurnFailed {
410 error: Some(ref err),
411 ..
412 } => {
413 assert_eq!(err.message.as_deref(), Some("rate limited"));
414 assert_eq!(err.code.as_deref(), Some("rate_limit"));
415 }
416 other => panic!("expected TurnFailed, got {other:?}"),
417 }
418 }
419
420 #[test]
421 fn deserialize_item_completed_agent_message() {
422 let json = r#"{"type":"item.completed","item":{"type":"agent_message","id":"msg_1","text":"Hello!"}}"#;
423 let event: Event = serde_json::from_str(json).unwrap();
424 match event {
425 Event::ItemCompleted {
426 item: Item::AgentMessage { id, text },
427 } => {
428 assert_eq!(id.as_deref(), Some("msg_1"));
429 assert_eq!(text.as_deref(), Some("Hello!"));
430 }
431 other => panic!("expected ItemCompleted with AgentMessage, got {other:?}"),
432 }
433 }
434
435 #[test]
436 fn deserialize_item_reasoning() {
437 let json =
438 r#"{"type":"item.started","item":{"type":"reasoning","id":"r_1","text":"Let me think..."}}"#;
439 let event: Event = serde_json::from_str(json).unwrap();
440 match event {
441 Event::ItemStarted {
442 item: Item::Reasoning { id, text },
443 } => {
444 assert_eq!(id.as_deref(), Some("r_1"));
445 assert_eq!(text.as_deref(), Some("Let me think..."));
446 }
447 other => panic!("expected ItemStarted with Reasoning, got {other:?}"),
448 }
449 }
450
451 #[test]
452 fn deserialize_item_command_execution() {
453 let json = r#"{"type":"item.completed","item":{"type":"command_execution","id":"cmd_1","command":"ls -la","aggregated_output":"total 42\n","exit_code":0,"status":"completed"}}"#;
454 let event: Event = serde_json::from_str(json).unwrap();
455 match event {
456 Event::ItemCompleted {
457 item:
458 Item::CommandExecution {
459 id,
460 command,
461 exit_code,
462 status,
463 ..
464 },
465 } => {
466 assert_eq!(id.as_deref(), Some("cmd_1"));
467 assert_eq!(command.as_deref(), Some("ls -la"));
468 assert_eq!(exit_code, Some(0));
469 assert_eq!(status.as_deref(), Some("completed"));
470 }
471 other => panic!("expected CommandExecution, got {other:?}"),
472 }
473 }
474
475 #[test]
476 fn deserialize_item_file_change() {
477 let json = r#"{"type":"item.completed","item":{"type":"file_change","id":"fc_1","changes":[{"file_path":"src/main.rs","new_content":"fn main() {}"}],"status":"completed"}}"#;
478 let event: Event = serde_json::from_str(json).unwrap();
479 match event {
480 Event::ItemCompleted {
481 item: Item::FileChange {
482 id, changes, status,
483 },
484 } => {
485 assert_eq!(id.as_deref(), Some("fc_1"));
486 assert_eq!(changes.len(), 1);
487 assert_eq!(changes[0].file_path.as_deref(), Some("src/main.rs"));
488 assert_eq!(status.as_deref(), Some("completed"));
489 }
490 other => panic!("expected FileChange, got {other:?}"),
491 }
492 }
493
494 #[test]
495 fn deserialize_token_count() {
496 let json = r#"{"type":"token_count","input_tokens":200,"cached_input_tokens":50,"output_tokens":100}"#;
497 let event: Event = serde_json::from_str(json).unwrap();
498 match event {
499 Event::TokenCount {
500 input_tokens,
501 cached_input_tokens,
502 output_tokens,
503 } => {
504 assert_eq!(input_tokens, 200);
505 assert_eq!(cached_input_tokens, 50);
506 assert_eq!(output_tokens, 100);
507 }
508 other => panic!("expected TokenCount, got {other:?}"),
509 }
510 }
511
512 #[test]
513 fn deserialize_error_event() {
514 let json = r#"{"type":"error","message":"something went wrong"}"#;
515 let event: Event = serde_json::from_str(json).unwrap();
516 match event {
517 Event::Error { message } => {
518 assert_eq!(message.as_deref(), Some("something went wrong"))
519 }
520 other => panic!("expected Error, got {other:?}"),
521 }
522 }
523
524 #[test]
525 fn deserialize_unknown_event_type() {
526 let json = r#"{"type":"future.event","some_field":"value"}"#;
527 let event: Event = serde_json::from_str(json).unwrap();
528 assert!(matches!(event, Event::Unknown));
529 }
530
531 #[test]
532 fn deserialize_unknown_item_type() {
533 let json = r#"{"type":"item.completed","item":{"type":"future_item","id":"x"}}"#;
534 let event: Event = serde_json::from_str(json).unwrap();
535 match event {
536 Event::ItemCompleted {
537 item: Item::Unknown,
538 } => {}
539 other => panic!("expected ItemCompleted with Unknown item, got {other:?}"),
540 }
541 }
542
543 #[test]
544 fn item_id_helper() {
545 let item = Item::AgentMessage {
546 id: Some("msg_1".into()),
547 text: Some("hi".into()),
548 };
549 assert_eq!(item.id(), Some("msg_1"));
550 assert_eq!(Item::Unknown.id(), None);
551 }
552
553 #[test]
554 fn item_text_helper() {
555 let item = Item::Reasoning {
556 id: None,
557 text: Some("thinking...".into()),
558 };
559 assert_eq!(item.text(), Some("thinking..."));
560
561 let cmd = Item::CommandExecution {
562 id: None,
563 command: None,
564 aggregated_output: None,
565 exit_code: None,
566 status: None,
567 };
568 assert_eq!(cmd.text(), None);
569 }
570
571 #[test]
572 fn event_item_helper() {
573 let event = Event::ItemCompleted {
574 item: Item::AgentMessage {
575 id: Some("m1".into()),
576 text: Some("hello".into()),
577 },
578 };
579 assert!(event.item().is_some());
580 assert_eq!(event.item().unwrap().id(), Some("m1"));
581
582 assert!(Event::TurnStarted.item().is_none());
583 }
584}