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 = r#"{"type":"turn.completed","usage":{"input_tokens":100,"output_tokens":50}}"#;
381 let event: Event = serde_json::from_str(json).unwrap();
382 match event {
383 Event::TurnCompleted {
384 usage: Some(usage), ..
385 } => {
386 assert_eq!(usage.input_tokens, 100);
387 assert_eq!(usage.output_tokens, 50);
388 }
389 other => panic!("expected TurnCompleted with usage, got {other:?}"),
390 }
391 }
392
393 #[test]
394 fn deserialize_turn_completed_without_usage() {
395 let json = r#"{"type":"turn.completed"}"#;
396 let event: Event = serde_json::from_str(json).unwrap();
397 match event {
398 Event::TurnCompleted { usage: None } => {}
399 other => panic!("expected TurnCompleted without usage, got {other:?}"),
400 }
401 }
402
403 #[test]
404 fn deserialize_turn_failed() {
405 let json =
406 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 = r#"{"type":"item.started","item":{"type":"reasoning","id":"r_1","text":"Let me think..."}}"#;
438 let event: Event = serde_json::from_str(json).unwrap();
439 match event {
440 Event::ItemStarted {
441 item: Item::Reasoning { id, text },
442 } => {
443 assert_eq!(id.as_deref(), Some("r_1"));
444 assert_eq!(text.as_deref(), Some("Let me think..."));
445 }
446 other => panic!("expected ItemStarted with Reasoning, got {other:?}"),
447 }
448 }
449
450 #[test]
451 fn deserialize_item_command_execution() {
452 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"}}"#;
453 let event: Event = serde_json::from_str(json).unwrap();
454 match event {
455 Event::ItemCompleted {
456 item:
457 Item::CommandExecution {
458 id,
459 command,
460 exit_code,
461 status,
462 ..
463 },
464 } => {
465 assert_eq!(id.as_deref(), Some("cmd_1"));
466 assert_eq!(command.as_deref(), Some("ls -la"));
467 assert_eq!(exit_code, Some(0));
468 assert_eq!(status.as_deref(), Some("completed"));
469 }
470 other => panic!("expected CommandExecution, got {other:?}"),
471 }
472 }
473
474 #[test]
475 fn deserialize_item_file_change() {
476 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"}}"#;
477 let event: Event = serde_json::from_str(json).unwrap();
478 match event {
479 Event::ItemCompleted {
480 item:
481 Item::FileChange {
482 id,
483 changes,
484 status,
485 },
486 } => {
487 assert_eq!(id.as_deref(), Some("fc_1"));
488 assert_eq!(changes.len(), 1);
489 assert_eq!(changes[0].file_path.as_deref(), Some("src/main.rs"));
490 assert_eq!(status.as_deref(), Some("completed"));
491 }
492 other => panic!("expected FileChange, got {other:?}"),
493 }
494 }
495
496 #[test]
497 fn deserialize_token_count() {
498 let json = r#"{"type":"token_count","input_tokens":200,"cached_input_tokens":50,"output_tokens":100}"#;
499 let event: Event = serde_json::from_str(json).unwrap();
500 match event {
501 Event::TokenCount {
502 input_tokens,
503 cached_input_tokens,
504 output_tokens,
505 } => {
506 assert_eq!(input_tokens, 200);
507 assert_eq!(cached_input_tokens, 50);
508 assert_eq!(output_tokens, 100);
509 }
510 other => panic!("expected TokenCount, got {other:?}"),
511 }
512 }
513
514 #[test]
515 fn deserialize_error_event() {
516 let json = r#"{"type":"error","message":"something went wrong"}"#;
517 let event: Event = serde_json::from_str(json).unwrap();
518 match event {
519 Event::Error { message } => {
520 assert_eq!(message.as_deref(), Some("something went wrong"))
521 }
522 other => panic!("expected Error, got {other:?}"),
523 }
524 }
525
526 #[test]
527 fn deserialize_unknown_event_type() {
528 let json = r#"{"type":"future.event","some_field":"value"}"#;
529 let event: Event = serde_json::from_str(json).unwrap();
530 assert!(matches!(event, Event::Unknown));
531 }
532
533 #[test]
534 fn deserialize_unknown_item_type() {
535 let json = r#"{"type":"item.completed","item":{"type":"future_item","id":"x"}}"#;
536 let event: Event = serde_json::from_str(json).unwrap();
537 match event {
538 Event::ItemCompleted {
539 item: Item::Unknown,
540 } => {}
541 other => panic!("expected ItemCompleted with Unknown item, got {other:?}"),
542 }
543 }
544
545 #[test]
546 fn item_id_helper() {
547 let item = Item::AgentMessage {
548 id: Some("msg_1".into()),
549 text: Some("hi".into()),
550 };
551 assert_eq!(item.id(), Some("msg_1"));
552 assert_eq!(Item::Unknown.id(), None);
553 }
554
555 #[test]
556 fn item_text_helper() {
557 let item = Item::Reasoning {
558 id: None,
559 text: Some("thinking...".into()),
560 };
561 assert_eq!(item.text(), Some("thinking..."));
562
563 let cmd = Item::CommandExecution {
564 id: None,
565 command: None,
566 aggregated_output: None,
567 exit_code: None,
568 status: None,
569 };
570 assert_eq!(cmd.text(), None);
571 }
572
573 #[test]
574 fn event_item_helper() {
575 let event = Event::ItemCompleted {
576 item: Item::AgentMessage {
577 id: Some("m1".into()),
578 text: Some("hello".into()),
579 },
580 };
581 assert!(event.item().is_some());
582 assert_eq!(event.item().unwrap().id(), Some("m1"));
583
584 assert!(Event::TurnStarted.item().is_none());
585 }
586}