1use crate::common::truncate_text;
34use crate::config::Verbosity;
35use crate::logger::{Colors, CHECK, CROSS};
36use serde::{Deserialize, Serialize};
37use std::cell::RefCell;
38use std::fmt::Write as _;
39use std::io::{self, BufRead, Write};
40use std::rc::Rc;
41
42use super::delta_display::{DeltaRenderer, TextDeltaRenderer};
43use super::health::HealthMonitor;
44use super::health::StreamingQualityMetrics;
45use super::printer::SharedPrinter;
46use super::streaming_state::StreamingSession;
47use super::terminal::TerminalMode;
48use super::types::{format_tool_input, format_unknown_json_event, ContentType};
49
50#[derive(Debug, Clone, Deserialize, Serialize)]
60pub struct OpenCodeEvent {
61 #[serde(rename = "type")]
62 pub(crate) event_type: String,
63 pub(crate) timestamp: Option<u64>,
64 #[serde(rename = "sessionID")]
65 pub(crate) session_id: Option<String>,
66 pub(crate) part: Option<OpenCodePart>,
67}
68
69#[derive(Debug, Clone, Deserialize, Serialize)]
71pub struct OpenCodePart {
72 pub(crate) id: Option<String>,
73 #[serde(rename = "sessionID")]
74 pub(crate) session_id: Option<String>,
75 #[serde(rename = "messageID")]
76 pub(crate) message_id: Option<String>,
77 #[serde(rename = "type")]
78 pub(crate) part_type: Option<String>,
79 pub(crate) snapshot: Option<String>,
81 pub(crate) reason: Option<String>,
83 pub(crate) cost: Option<f64>,
84 pub(crate) tokens: Option<OpenCodeTokens>,
85 #[serde(rename = "callID")]
87 pub(crate) call_id: Option<String>,
88 pub(crate) tool: Option<String>,
89 pub(crate) state: Option<OpenCodeToolState>,
90 pub(crate) text: Option<String>,
92 pub(crate) time: Option<OpenCodeTime>,
94}
95
96#[derive(Debug, Clone, Deserialize, Serialize)]
98pub struct OpenCodeToolState {
99 pub(crate) status: Option<String>,
100 pub(crate) input: Option<serde_json::Value>,
101 pub(crate) output: Option<serde_json::Value>,
102 pub(crate) title: Option<String>,
103 pub(crate) metadata: Option<serde_json::Value>,
104 pub(crate) time: Option<OpenCodeTime>,
105}
106
107#[derive(Debug, Clone, Deserialize, Serialize)]
109pub struct OpenCodeTokens {
110 pub(crate) input: Option<u64>,
111 pub(crate) output: Option<u64>,
112 pub(crate) reasoning: Option<u64>,
113 pub(crate) cache: Option<OpenCodeCache>,
114}
115
116#[derive(Debug, Clone, Deserialize, Serialize)]
118pub struct OpenCodeCache {
119 pub(crate) read: Option<u64>,
120 pub(crate) write: Option<u64>,
121}
122
123#[derive(Debug, Clone, Deserialize, Serialize)]
125pub struct OpenCodeTime {
126 pub(crate) start: Option<u64>,
127 pub(crate) end: Option<u64>,
128}
129
130pub struct OpenCodeParser {
132 colors: Colors,
133 verbosity: Verbosity,
134 log_file: Option<String>,
135 display_name: String,
136 streaming_session: Rc<RefCell<StreamingSession>>,
138 terminal_mode: RefCell<TerminalMode>,
140 show_streaming_metrics: bool,
142 printer: SharedPrinter,
144}
145
146impl OpenCodeParser {
147 pub(crate) fn new(colors: Colors, verbosity: Verbosity) -> Self {
148 Self::with_printer(colors, verbosity, super::printer::shared_stdout())
149 }
150
151 pub(crate) fn with_printer(
163 colors: Colors,
164 verbosity: Verbosity,
165 printer: SharedPrinter,
166 ) -> Self {
167 let verbose_warnings = matches!(verbosity, Verbosity::Debug);
168 let streaming_session = StreamingSession::new().with_verbose_warnings(verbose_warnings);
169
170 let _printer_is_terminal = printer.borrow().is_terminal();
172
173 Self {
174 colors,
175 verbosity,
176 log_file: None,
177 display_name: "OpenCode".to_string(),
178 streaming_session: Rc::new(RefCell::new(streaming_session)),
179 terminal_mode: RefCell::new(TerminalMode::detect()),
180 show_streaming_metrics: false,
181 printer,
182 }
183 }
184
185 pub(crate) const fn with_show_streaming_metrics(mut self, show: bool) -> Self {
186 self.show_streaming_metrics = show;
187 self
188 }
189
190 pub(crate) fn with_display_name(mut self, display_name: &str) -> Self {
191 self.display_name = display_name.to_string();
192 self
193 }
194
195 pub(crate) fn with_log_file(mut self, path: &str) -> Self {
196 self.log_file = Some(path.to_string());
197 self
198 }
199
200 #[cfg(test)]
201 pub fn with_terminal_mode(self, mode: TerminalMode) -> Self {
202 *self.terminal_mode.borrow_mut() = mode;
203 self
204 }
205
206 #[cfg_attr(any(debug_assertions, test, feature = "monitoring"), allow(dead_code))]
215 pub fn printer(&self) -> SharedPrinter {
216 Rc::clone(&self.printer)
217 }
218
219 #[cfg_attr(any(debug_assertions, test, feature = "monitoring"), allow(dead_code))]
228 pub fn streaming_metrics(&self) -> StreamingQualityMetrics {
229 self.streaming_session
230 .borrow()
231 .get_streaming_quality_metrics()
232 }
233
234 pub(crate) fn parse_event(&self, line: &str) -> Option<String> {
242 let event: OpenCodeEvent = if let Ok(e) = serde_json::from_str(line) {
243 e
244 } else {
245 let trimmed = line.trim();
246 if !trimmed.is_empty() && !trimmed.starts_with('{') {
247 return Some(format!("{trimmed}\n"));
248 }
249 return None;
250 };
251 let c = &self.colors;
252 let prefix = &self.display_name;
253
254 let output = match event.event_type.as_str() {
255 "step_start" => self.format_step_start_event(&event),
256 "step_finish" => self.format_step_finish_event(&event),
257 "tool_use" => self.format_tool_use_event(&event),
258 "text" => self.format_text_event(&event),
259 _ => {
260 format_unknown_json_event(line, prefix, *c, self.verbosity.is_verbose())
262 }
263 };
264
265 if output.is_empty() {
266 None
267 } else {
268 Some(output)
269 }
270 }
271
272 fn format_step_start_event(&self, event: &OpenCodeEvent) -> String {
274 let c = &self.colors;
275 let prefix = &self.display_name;
276
277 self.streaming_session.borrow_mut().on_message_start();
279
280 let step_id = event.part.as_ref().map_or_else(
283 || {
284 event
285 .session_id
286 .clone()
287 .unwrap_or_else(|| "unknown".to_string())
288 },
289 |part| {
290 part.message_id.as_ref().map_or_else(
291 || {
292 let session = event.session_id.as_deref().unwrap_or("unknown");
293 let part_id = part.id.as_deref().unwrap_or("step");
294 format!("{session}:{part_id}")
295 },
296 std::clone::Clone::clone,
297 )
298 },
299 );
300 self.streaming_session
301 .borrow_mut()
302 .set_current_message_id(Some(step_id));
303
304 let snapshot = event
305 .part
306 .as_ref()
307 .and_then(|p| p.snapshot.as_ref())
308 .map(|s| format!("({s:.8}...)"))
309 .unwrap_or_default();
310 format!(
311 "{}[{}]{} {}Step started{} {}{}{}\n",
312 c.dim(),
313 prefix,
314 c.reset(),
315 c.cyan(),
316 c.reset(),
317 c.dim(),
318 snapshot,
319 c.reset()
320 )
321 }
322
323 fn format_step_finish_event(&self, event: &OpenCodeEvent) -> String {
325 let c = &self.colors;
326 let prefix = &self.display_name;
327
328 let session = self.streaming_session.borrow();
330 let is_duplicate = session.get_current_message_id().map_or_else(
331 || session.has_any_streamed_content(),
332 |message_id| session.is_duplicate_final_message(message_id),
333 );
334 let was_streaming = session.has_any_streamed_content();
335 let metrics = session.get_streaming_quality_metrics();
336 drop(session);
337
338 let _was_in_block = self.streaming_session.borrow_mut().on_message_stop();
340
341 event.part.as_ref().map_or_else(String::new, |part| {
342 let reason = part.reason.as_deref().unwrap_or("unknown");
343 let cost = part.cost.unwrap_or(0.0);
344
345 let tokens_str = part.tokens.as_ref().map_or_else(String::new, |tokens| {
346 let input = tokens.input.unwrap_or(0);
347 let output = tokens.output.unwrap_or(0);
348 let reasoning = tokens.reasoning.unwrap_or(0);
349 let cache_read = tokens.cache.as_ref().and_then(|c| c.read).unwrap_or(0);
350 if reasoning > 0 {
351 format!("in:{input} out:{output} reason:{reasoning} cache:{cache_read}")
352 } else if cache_read > 0 {
353 format!("in:{input} out:{output} cache:{cache_read}")
354 } else {
355 format!("in:{input} out:{output}")
356 }
357 });
358
359 let is_success = reason == "tool-calls" || reason == "end_turn";
360 let icon = if is_success { CHECK } else { CROSS };
361 let color = if is_success { c.green() } else { c.yellow() };
362
363 let terminal_mode = *self.terminal_mode.borrow();
365 let newline_prefix = if is_duplicate || was_streaming {
366 let completion = TextDeltaRenderer::render_completion(terminal_mode);
367 let show_metrics = (self.verbosity.is_debug() || self.show_streaming_metrics)
368 && metrics.total_deltas > 0;
369 if show_metrics {
370 format!("{}\n{}", completion, metrics.format(*c))
371 } else {
372 completion
373 }
374 } else {
375 String::new()
376 };
377
378 let mut out = format!(
379 "{}{}[{}]{} {}{} Step finished{} {}({}",
380 newline_prefix,
381 c.dim(),
382 prefix,
383 c.reset(),
384 color,
385 icon,
386 c.reset(),
387 c.dim(),
388 reason
389 );
390 if !tokens_str.is_empty() {
391 let _ = write!(out, ", {tokens_str}");
392 }
393 if cost > 0.0 {
394 let _ = write!(out, ", ${cost:.4}");
395 }
396 let _ = writeln!(out, "){}", c.reset());
397 out
398 })
399 }
400
401 fn format_tool_use_event(&self, event: &OpenCodeEvent) -> String {
403 let c = &self.colors;
404 let prefix = &self.display_name;
405
406 event.part.as_ref().map_or_else(String::new, |part| {
407 let tool_name = part.tool.as_deref().unwrap_or("unknown");
408 let status = part
409 .state
410 .as_ref()
411 .and_then(|s| s.status.as_deref())
412 .unwrap_or("pending");
413 let title = part.state.as_ref().and_then(|s| s.title.as_deref());
414
415 let is_completed = status == "completed";
416 let icon = if is_completed { CHECK } else { '…' };
417 let color = if is_completed { c.green() } else { c.yellow() };
418
419 let mut out = format!(
420 "{}[{}]{} {}Tool{}: {}{}{} {}{}{}\n",
421 c.dim(),
422 prefix,
423 c.reset(),
424 c.magenta(),
425 c.reset(),
426 c.bold(),
427 tool_name,
428 c.reset(),
429 color,
430 icon,
431 c.reset()
432 );
433
434 if let Some(t) = title {
436 let limit = self.verbosity.truncate_limit("text");
437 let preview = truncate_text(t, limit);
438 let _ = writeln!(
439 out,
440 "{}[{}]{} {} └─ {}{}",
441 c.dim(),
442 prefix,
443 c.reset(),
444 c.dim(),
445 preview,
446 c.reset()
447 );
448 }
449
450 if self.verbosity.show_tool_input() {
452 if let Some(ref state) = part.state {
453 if let Some(ref input_val) = state.input {
454 let input_str = format_tool_input(input_val);
455 let limit = self.verbosity.truncate_limit("tool_input");
456 let preview = truncate_text(&input_str, limit);
457 if !preview.is_empty() {
458 let _ = writeln!(
459 out,
460 "{}[{}]{} {} └─ {}{}",
461 c.dim(),
462 prefix,
463 c.reset(),
464 c.dim(),
465 preview,
466 c.reset()
467 );
468 }
469 }
470 }
471 }
472
473 if self.verbosity.is_verbose() && is_completed {
475 if let Some(ref state) = part.state {
476 if let Some(ref output_val) = state.output {
477 let output_str = match output_val {
478 serde_json::Value::String(s) => s.as_str(),
479 _ => "",
480 };
481 let output_str = if output_str.is_empty() {
482 output_val.to_string()
483 } else {
484 output_str.to_string()
485 };
486 let limit = self.verbosity.truncate_limit("tool_result");
487 let preview = truncate_text(&output_str, limit);
488 if !preview.is_empty() {
489 let _ = writeln!(
490 out,
491 "{}[{}]{} {} └─ Output: {}{}",
492 c.dim(),
493 prefix,
494 c.reset(),
495 c.dim(),
496 preview,
497 c.reset()
498 );
499 }
500 }
501 }
502 }
503 out
504 })
505 }
506
507 fn format_text_event(&self, event: &OpenCodeEvent) -> String {
509 let c = &self.colors;
510 let prefix = &self.display_name;
511
512 if let Some(ref part) = event.part {
513 if let Some(ref text) = part.text {
514 let (show_prefix, accumulated_text) = {
516 let mut session = self.streaming_session.borrow_mut();
517 let show_prefix = session.on_text_delta_key("main", text);
518 let accumulated_text = session
520 .get_accumulated(ContentType::Text, "main")
521 .unwrap_or("")
522 .to_string();
523 (show_prefix, accumulated_text)
524 };
525
526 let limit = self.verbosity.truncate_limit("text");
528 let preview = truncate_text(&accumulated_text, limit);
529
530 let terminal_mode = *self.terminal_mode.borrow();
532 if show_prefix {
533 return TextDeltaRenderer::render_first_delta(
535 &preview,
536 prefix,
537 *c,
538 terminal_mode,
539 );
540 }
541 return TextDeltaRenderer::render_subsequent_delta(
543 &preview,
544 prefix,
545 *c,
546 terminal_mode,
547 );
548 }
549 }
550 String::new()
551 }
552
553 fn is_control_event(event: &OpenCodeEvent) -> bool {
559 match event.event_type.as_str() {
560 "step_start" | "step_finish" => true,
562 _ => false,
563 }
564 }
565
566 fn is_partial_event(event: &OpenCodeEvent) -> bool {
571 match event.event_type.as_str() {
572 "text" => true,
574 _ => false,
575 }
576 }
577
578 pub(crate) fn parse_stream<R: BufRead>(&self, mut reader: R) -> io::Result<()> {
580 use super::incremental_parser::IncrementalNdjsonParser;
581
582 let c = &self.colors;
583 let monitor = HealthMonitor::new("OpenCode");
584 let mut log_writer = self.log_file.as_ref().and_then(|log_path| {
585 std::fs::OpenOptions::new()
586 .create(true)
587 .append(true)
588 .open(log_path)
589 .ok()
590 .map(std::io::BufWriter::new)
591 });
592
593 let mut incremental_parser = IncrementalNdjsonParser::new();
596 let mut byte_buffer = Vec::new();
597
598 loop {
599 byte_buffer.clear();
601 let chunk = reader.fill_buf()?;
602 if chunk.is_empty() {
603 break;
604 }
605
606 byte_buffer.extend_from_slice(chunk);
608 let consumed = chunk.len();
609 reader.consume(consumed);
610
611 let json_events = incremental_parser.feed(&byte_buffer);
613
614 for line in json_events {
616 let trimmed = line.trim();
617 if trimmed.is_empty() {
618 continue;
619 }
620
621 if self.verbosity.is_debug() {
622 let mut printer = self.printer.borrow_mut();
623 writeln!(
624 printer,
625 "{}[DEBUG]{} {}{}{}",
626 c.dim(),
627 c.reset(),
628 c.dim(),
629 &line,
630 c.reset()
631 )?;
632 printer.flush()?;
633 }
634
635 match self.parse_event(&line) {
637 Some(output) => {
638 if trimmed.starts_with('{') {
640 if let Ok(event) = serde_json::from_str::<OpenCodeEvent>(&line) {
641 if Self::is_partial_event(&event) {
642 monitor.record_partial_event();
643 } else {
644 monitor.record_parsed();
645 }
646 } else {
647 monitor.record_parsed();
648 }
649 } else {
650 monitor.record_parsed();
651 }
652 let mut printer = self.printer.borrow_mut();
654 write!(printer, "{output}")?;
655 printer.flush()?;
656 }
657 None => {
658 if trimmed.starts_with('{') {
660 if let Ok(event) = serde_json::from_str::<OpenCodeEvent>(&line) {
661 if Self::is_control_event(&event) {
662 monitor.record_control_event();
663 } else {
664 monitor.record_unknown_event();
666 }
667 } else {
668 monitor.record_parse_error();
670 }
671 } else {
672 monitor.record_ignored();
673 }
674 }
675 }
676
677 if let Some(ref mut file) = log_writer {
678 writeln!(file, "{line}")?;
679 }
680 }
681 }
682
683 if let Some(ref mut file) = log_writer {
684 file.flush()?;
685 }
686 if let Some(warning) = monitor.check_and_warn(*c) {
687 let mut printer = self.printer.borrow_mut();
688 writeln!(printer, "{warning}")?;
689 }
690 Ok(())
691 }
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697
698 #[test]
699 fn test_opencode_step_start() {
700 let parser = OpenCodeParser::new(Colors { enabled: false }, Verbosity::Normal);
701 let json = r#"{"type":"step_start","timestamp":1768191337567,"sessionID":"ses_44f9562d4ffe","part":{"id":"prt_bb06aa45c001","sessionID":"ses_44f9562d4ffe","messageID":"msg_bb06a9dc1001","type":"step-start","snapshot":"5d36aa035d4df6edb73a68058733063258114ed5"}}"#;
702 let output = parser.parse_event(json);
703 assert!(output.is_some());
704 let out = output.unwrap();
705 assert!(out.contains("Step started"));
706 assert!(out.contains("5d36aa03"));
707 }
708
709 #[test]
710 fn test_opencode_step_finish() {
711 let parser = OpenCodeParser::new(Colors { enabled: false }, Verbosity::Normal);
712 let json = r#"{"type":"step_finish","timestamp":1768191347296,"sessionID":"ses_44f9562d4ffe","part":{"id":"prt_bb06aca1d001","sessionID":"ses_44f9562d4ffe","messageID":"msg_bb06a9dc1001","type":"step-finish","reason":"tool-calls","snapshot":"5d36aa035d4df6edb73a68058733063258114ed5","cost":0,"tokens":{"input":108,"output":151,"reasoning":0,"cache":{"read":11236,"write":0}}}}"#;
713 let output = parser.parse_event(json);
714 assert!(output.is_some());
715 let out = output.unwrap();
716 assert!(out.contains("Step finished"));
717 assert!(out.contains("tool-calls"));
718 assert!(out.contains("in:108"));
719 assert!(out.contains("out:151"));
720 assert!(out.contains("cache:11236"));
721 }
722
723 #[test]
724 fn test_opencode_tool_use_completed() {
725 let parser = OpenCodeParser::new(Colors { enabled: false }, Verbosity::Normal);
726 let json = r#"{"type":"tool_use","timestamp":1768191346712,"sessionID":"ses_44f9562d4ffe","part":{"id":"prt_bb06ac80c001","sessionID":"ses_44f9562d4ffe","messageID":"msg_bb06a9dc1001","type":"tool","callID":"call_8a2985d92e63","tool":"read","state":{"status":"completed","input":{"filePath":"/test/PLAN.md"},"output":"<file>\n00001| # Implementation Plan\n</file>","title":"PLAN.md"}}}"#;
727 let output = parser.parse_event(json);
728 assert!(output.is_some());
729 let out = output.unwrap();
730 assert!(out.contains("Tool"));
731 assert!(out.contains("read"));
732 assert!(out.contains("✓")); assert!(out.contains("PLAN.md"));
734 }
735
736 #[test]
737 fn test_opencode_tool_use_pending() {
738 let parser = OpenCodeParser::new(Colors { enabled: false }, Verbosity::Normal);
739 let json = r#"{"type":"tool_use","timestamp":1768191346712,"sessionID":"ses_44f9562d4ffe","part":{"id":"prt_bb06ac80c001","sessionID":"ses_44f9562d4ffe","messageID":"msg_bb06a9dc1001","type":"tool","callID":"call_8a2985d92e63","tool":"bash","state":{"status":"pending","input":{"command":"ls -la"}}}}"#;
740 let output = parser.parse_event(json);
741 assert!(output.is_some());
742 let out = output.unwrap();
743 assert!(out.contains("Tool"));
744 assert!(out.contains("bash"));
745 assert!(out.contains("…")); }
747
748 #[test]
749 fn test_opencode_tool_use_shows_input() {
750 let parser = OpenCodeParser::new(Colors { enabled: false }, Verbosity::Normal);
751 let json = r#"{"type":"tool_use","timestamp":1768191346712,"sessionID":"ses_44f9562d4ffe","part":{"id":"prt_bb06ac80c001","sessionID":"ses_44f9562d4ffe","messageID":"msg_bb06a9dc1001","type":"tool","callID":"call_8a2985d92e63","tool":"read","state":{"status":"completed","input":{"filePath":"/Users/test/file.rs"}}}}"#;
752 let output = parser.parse_event(json);
753 assert!(output.is_some());
754 let out = output.unwrap();
755 assert!(out.contains("Tool"));
756 assert!(out.contains("read"));
757 assert!(out.contains("filePath=/Users/test/file.rs"));
758 }
759
760 #[test]
761 fn test_opencode_text_event() {
762 let parser = OpenCodeParser::new(Colors { enabled: false }, Verbosity::Normal);
763 let json = r#"{"type":"text","timestamp":1768191347231,"sessionID":"ses_44f9562d4ffe","part":{"id":"prt_bb06ac63300","sessionID":"ses_44f9562d4ffe","messageID":"msg_bb06a9dc1001","type":"text","text":"I'll start by reading the plan and requirements to understand what needs to be implemented.","time":{"start":1768191347226,"end":1768191347226}}}"#;
764 let output = parser.parse_event(json);
765 assert!(output.is_some());
766 let out = output.unwrap();
767 assert!(out.contains("I'll start by reading the plan"));
768 }
769
770 #[test]
771 fn test_opencode_unknown_event_ignored() {
772 let parser = OpenCodeParser::new(Colors { enabled: false }, Verbosity::Normal);
773 let json = r#"{"type":"unknown_event","timestamp":1768191347231,"sessionID":"ses_44f9562d4ffe","part":{}}"#;
774 let output = parser.parse_event(json);
775 assert!(output.is_none());
777 }
778
779 #[test]
780 fn test_opencode_parser_non_json_passthrough() {
781 let parser = OpenCodeParser::new(Colors { enabled: false }, Verbosity::Normal);
782 let output = parser.parse_event("Error: something went wrong");
783 assert!(output.is_some());
784 assert!(output.unwrap().contains("Error: something went wrong"));
785 }
786
787 #[test]
788 fn test_opencode_parser_malformed_json_ignored() {
789 let parser = OpenCodeParser::new(Colors { enabled: false }, Verbosity::Normal);
790 let output = parser.parse_event("{invalid json here}");
791 assert!(output.is_none());
792 }
793
794 #[test]
795 fn test_opencode_step_finish_with_cost() {
796 let parser = OpenCodeParser::new(Colors { enabled: false }, Verbosity::Normal);
797 let json = r#"{"type":"step_finish","timestamp":1768191347296,"sessionID":"ses_44f9562d4ffe","part":{"type":"step-finish","reason":"end_turn","cost":0.0025,"tokens":{"input":1000,"output":500,"reasoning":0,"cache":{"read":0,"write":0}}}}"#;
798 let output = parser.parse_event(json);
799 assert!(output.is_some());
800 let out = output.unwrap();
801 assert!(out.contains("Step finished"));
802 assert!(out.contains("end_turn"));
803 assert!(out.contains("$0.0025"));
804 }
805
806 #[test]
807 fn test_opencode_tool_verbose_shows_output() {
808 let parser = OpenCodeParser::new(Colors { enabled: false }, Verbosity::Verbose);
809 let json = r#"{"type":"tool_use","timestamp":1768191346712,"sessionID":"ses_44f9562d4ffe","part":{"id":"prt_bb06ac80c001","type":"tool","tool":"read","state":{"status":"completed","input":{"filePath":"/test.rs"},"output":"fn main() { println!(\"Hello\"); }"}}}"#;
810 let output = parser.parse_event(json);
811 assert!(output.is_some());
812 let out = output.unwrap();
813 assert!(out.contains("Tool"));
814 assert!(out.contains("read"));
815 assert!(out.contains("Output"));
816 assert!(out.contains("fn main"));
817 }
818}