1#[derive(Debug, thiserror::Error)]
6pub enum ChannelError {
7 #[error("I/O error: {0}")]
9 Io(#[from] std::io::Error),
10
11 #[error("channel closed")]
13 ChannelClosed,
14
15 #[error("confirmation cancelled")]
17 ConfirmCancelled,
18
19 #[error("{0}")]
21 Other(String),
22}
23
24impl ChannelError {
25 pub fn other(e: impl std::fmt::Display) -> Self {
26 Self::Other(e.to_string())
27 }
28}
29
30#[derive(Debug)]
32pub struct ToolStartEvent<'a> {
33 pub tool_name: &'a str,
34 pub tool_call_id: &'a str,
35 pub params: Option<serde_json::Value>,
36 pub parent_tool_use_id: Option<String>,
37}
38
39#[derive(Debug)]
41pub struct ToolOutputEvent<'a> {
42 pub tool_name: &'a str,
43 pub body: &'a str,
44 pub diff: Option<crate::DiffData>,
45 pub filter_stats: Option<String>,
46 pub kept_lines: Option<Vec<usize>>,
47 pub locations: Option<Vec<String>>,
48 pub tool_call_id: &'a str,
49 pub is_error: bool,
50 pub parent_tool_use_id: Option<String>,
51 pub raw_response: Option<serde_json::Value>,
52 pub started_at: Option<std::time::Instant>,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum AttachmentKind {
58 Audio,
59 Image,
60 Video,
61 File,
62}
63
64#[derive(Debug, Clone)]
66pub struct Attachment {
67 pub kind: AttachmentKind,
68 pub data: Vec<u8>,
69 pub filename: Option<String>,
70}
71
72#[derive(Debug, Clone)]
74pub struct ChannelMessage {
75 pub text: String,
76 pub attachments: Vec<Attachment>,
77}
78
79pub trait Channel: Send {
81 fn recv(&mut self)
87 -> impl Future<Output = Result<Option<ChannelMessage>, ChannelError>> + Send;
88
89 fn try_recv(&mut self) -> Option<ChannelMessage> {
91 None
92 }
93
94 fn supports_exit(&self) -> bool {
99 true
100 }
101
102 fn send(&mut self, text: &str) -> impl Future<Output = Result<(), ChannelError>> + Send;
108
109 fn send_chunk(&mut self, chunk: &str) -> impl Future<Output = Result<(), ChannelError>> + Send;
115
116 fn flush_chunks(&mut self) -> impl Future<Output = Result<(), ChannelError>> + Send;
122
123 fn send_typing(&mut self) -> impl Future<Output = Result<(), ChannelError>> + Send {
129 async { Ok(()) }
130 }
131
132 fn send_status(
138 &mut self,
139 _text: &str,
140 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
141 async { Ok(()) }
142 }
143
144 fn send_thinking_chunk(
150 &mut self,
151 _chunk: &str,
152 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
153 async { Ok(()) }
154 }
155
156 fn send_queue_count(
162 &mut self,
163 _count: usize,
164 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
165 async { Ok(()) }
166 }
167
168 fn send_usage(
174 &mut self,
175 _input_tokens: u64,
176 _output_tokens: u64,
177 _context_window: u64,
178 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
179 async { Ok(()) }
180 }
181
182 fn send_diff(
188 &mut self,
189 _diff: crate::DiffData,
190 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
191 async { Ok(()) }
192 }
193
194 fn send_tool_start(
204 &mut self,
205 _event: ToolStartEvent<'_>,
206 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
207 async { Ok(()) }
208 }
209
210 fn send_tool_output(
221 &mut self,
222 event: ToolOutputEvent<'_>,
223 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
224 let formatted = crate::agent::format_tool_output(event.tool_name, event.body);
225 async move { self.send(&formatted).await }
226 }
227
228 fn confirm(
235 &mut self,
236 _prompt: &str,
237 ) -> impl Future<Output = Result<bool, ChannelError>> + Send {
238 async { Ok(true) }
239 }
240
241 fn send_stop_hint(
250 &mut self,
251 _hint: StopHint,
252 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
253 async { Ok(()) }
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub enum StopHint {
264 MaxTokens,
266 MaxTurnRequests,
268}
269
270#[derive(Debug, Clone)]
272pub struct ToolStartData {
273 pub tool_name: String,
274 pub tool_call_id: String,
275 pub params: Option<serde_json::Value>,
277 pub parent_tool_use_id: Option<String>,
279 pub started_at: std::time::Instant,
281}
282
283#[derive(Debug, Clone)]
285pub struct ToolOutputData {
286 pub tool_name: String,
287 pub display: String,
288 pub diff: Option<crate::DiffData>,
289 pub filter_stats: Option<String>,
290 pub kept_lines: Option<Vec<usize>>,
291 pub locations: Option<Vec<String>>,
292 pub tool_call_id: String,
293 pub is_error: bool,
294 pub terminal_id: Option<String>,
296 pub parent_tool_use_id: Option<String>,
298 pub raw_response: Option<serde_json::Value>,
300 pub started_at: Option<std::time::Instant>,
302}
303
304#[derive(Debug, Clone)]
306pub enum LoopbackEvent {
307 Chunk(String),
308 Flush,
309 FullMessage(String),
310 Status(String),
311 ToolStart(Box<ToolStartData>),
313 ToolOutput(Box<ToolOutputData>),
314 Usage {
316 input_tokens: u64,
317 output_tokens: u64,
318 context_window: u64,
319 },
320 SessionTitle(String),
322 Plan(Vec<(String, PlanItemStatus)>),
324 ThinkingChunk(String),
326 Stop(StopHint),
330}
331
332#[derive(Debug, Clone)]
334pub enum PlanItemStatus {
335 Pending,
336 InProgress,
337 Completed,
338}
339
340pub struct LoopbackHandle {
342 pub input_tx: tokio::sync::mpsc::Sender<ChannelMessage>,
343 pub output_rx: tokio::sync::mpsc::Receiver<LoopbackEvent>,
344 pub cancel_signal: std::sync::Arc<tokio::sync::Notify>,
346}
347
348pub struct LoopbackChannel {
350 input_rx: tokio::sync::mpsc::Receiver<ChannelMessage>,
351 output_tx: tokio::sync::mpsc::Sender<LoopbackEvent>,
352}
353
354impl LoopbackChannel {
355 #[must_use]
357 pub fn pair(buffer: usize) -> (Self, LoopbackHandle) {
358 let (input_tx, input_rx) = tokio::sync::mpsc::channel(buffer);
359 let (output_tx, output_rx) = tokio::sync::mpsc::channel(buffer);
360 let cancel_signal = std::sync::Arc::new(tokio::sync::Notify::new());
361 (
362 Self {
363 input_rx,
364 output_tx,
365 },
366 LoopbackHandle {
367 input_tx,
368 output_rx,
369 cancel_signal,
370 },
371 )
372 }
373}
374
375impl Channel for LoopbackChannel {
376 fn supports_exit(&self) -> bool {
377 false
378 }
379
380 async fn recv(&mut self) -> Result<Option<ChannelMessage>, ChannelError> {
381 Ok(self.input_rx.recv().await)
382 }
383
384 async fn send(&mut self, text: &str) -> Result<(), ChannelError> {
385 self.output_tx
386 .send(LoopbackEvent::FullMessage(text.to_owned()))
387 .await
388 .map_err(|_| ChannelError::ChannelClosed)
389 }
390
391 async fn send_chunk(&mut self, chunk: &str) -> Result<(), ChannelError> {
392 self.output_tx
393 .send(LoopbackEvent::Chunk(chunk.to_owned()))
394 .await
395 .map_err(|_| ChannelError::ChannelClosed)
396 }
397
398 async fn flush_chunks(&mut self) -> Result<(), ChannelError> {
399 self.output_tx
400 .send(LoopbackEvent::Flush)
401 .await
402 .map_err(|_| ChannelError::ChannelClosed)
403 }
404
405 async fn send_status(&mut self, text: &str) -> Result<(), ChannelError> {
406 self.output_tx
407 .send(LoopbackEvent::Status(text.to_owned()))
408 .await
409 .map_err(|_| ChannelError::ChannelClosed)
410 }
411
412 async fn send_thinking_chunk(&mut self, chunk: &str) -> Result<(), ChannelError> {
413 self.output_tx
414 .send(LoopbackEvent::ThinkingChunk(chunk.to_owned()))
415 .await
416 .map_err(|_| ChannelError::ChannelClosed)
417 }
418
419 async fn send_tool_start(&mut self, event: ToolStartEvent<'_>) -> Result<(), ChannelError> {
420 self.output_tx
421 .send(LoopbackEvent::ToolStart(Box::new(ToolStartData {
422 tool_name: event.tool_name.to_owned(),
423 tool_call_id: event.tool_call_id.to_owned(),
424 params: event.params,
425 parent_tool_use_id: event.parent_tool_use_id,
426 started_at: std::time::Instant::now(),
427 })))
428 .await
429 .map_err(|_| ChannelError::ChannelClosed)
430 }
431
432 async fn send_tool_output(&mut self, event: ToolOutputEvent<'_>) -> Result<(), ChannelError> {
433 self.output_tx
434 .send(LoopbackEvent::ToolOutput(Box::new(ToolOutputData {
435 tool_name: event.tool_name.to_owned(),
436 display: event.body.to_owned(),
437 diff: event.diff,
438 filter_stats: event.filter_stats,
439 kept_lines: event.kept_lines,
440 locations: event.locations,
441 tool_call_id: event.tool_call_id.to_owned(),
442 is_error: event.is_error,
443 terminal_id: None,
444 parent_tool_use_id: event.parent_tool_use_id,
445 raw_response: event.raw_response,
446 started_at: event.started_at,
447 })))
448 .await
449 .map_err(|_| ChannelError::ChannelClosed)
450 }
451
452 async fn confirm(&mut self, _prompt: &str) -> Result<bool, ChannelError> {
453 Ok(true)
454 }
455
456 async fn send_stop_hint(&mut self, hint: StopHint) -> Result<(), ChannelError> {
457 self.output_tx
458 .send(LoopbackEvent::Stop(hint))
459 .await
460 .map_err(|_| ChannelError::ChannelClosed)
461 }
462
463 async fn send_usage(
464 &mut self,
465 input_tokens: u64,
466 output_tokens: u64,
467 context_window: u64,
468 ) -> Result<(), ChannelError> {
469 self.output_tx
470 .send(LoopbackEvent::Usage {
471 input_tokens,
472 output_tokens,
473 context_window,
474 })
475 .await
476 .map_err(|_| ChannelError::ChannelClosed)
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn channel_message_creation() {
486 let msg = ChannelMessage {
487 text: "hello".to_string(),
488 attachments: vec![],
489 };
490 assert_eq!(msg.text, "hello");
491 assert!(msg.attachments.is_empty());
492 }
493
494 struct StubChannel;
495
496 impl Channel for StubChannel {
497 async fn recv(&mut self) -> Result<Option<ChannelMessage>, ChannelError> {
498 Ok(None)
499 }
500
501 async fn send(&mut self, _text: &str) -> Result<(), ChannelError> {
502 Ok(())
503 }
504
505 async fn send_chunk(&mut self, _chunk: &str) -> Result<(), ChannelError> {
506 Ok(())
507 }
508
509 async fn flush_chunks(&mut self) -> Result<(), ChannelError> {
510 Ok(())
511 }
512 }
513
514 #[tokio::test]
515 async fn send_chunk_default_is_noop() {
516 let mut ch = StubChannel;
517 ch.send_chunk("partial").await.unwrap();
518 }
519
520 #[tokio::test]
521 async fn flush_chunks_default_is_noop() {
522 let mut ch = StubChannel;
523 ch.flush_chunks().await.unwrap();
524 }
525
526 #[tokio::test]
527 async fn stub_channel_confirm_auto_approves() {
528 let mut ch = StubChannel;
529 let result = ch.confirm("Delete everything?").await.unwrap();
530 assert!(result);
531 }
532
533 #[tokio::test]
534 async fn stub_channel_send_typing_default() {
535 let mut ch = StubChannel;
536 ch.send_typing().await.unwrap();
537 }
538
539 #[tokio::test]
540 async fn stub_channel_recv_returns_none() {
541 let mut ch = StubChannel;
542 let msg = ch.recv().await.unwrap();
543 assert!(msg.is_none());
544 }
545
546 #[tokio::test]
547 async fn stub_channel_send_ok() {
548 let mut ch = StubChannel;
549 ch.send("hello").await.unwrap();
550 }
551
552 #[test]
553 fn channel_message_clone() {
554 let msg = ChannelMessage {
555 text: "test".to_string(),
556 attachments: vec![],
557 };
558 let cloned = msg.clone();
559 assert_eq!(cloned.text, "test");
560 }
561
562 #[test]
563 fn channel_message_debug() {
564 let msg = ChannelMessage {
565 text: "debug".to_string(),
566 attachments: vec![],
567 };
568 let debug = format!("{msg:?}");
569 assert!(debug.contains("debug"));
570 }
571
572 #[test]
573 fn attachment_kind_equality() {
574 assert_eq!(AttachmentKind::Audio, AttachmentKind::Audio);
575 assert_ne!(AttachmentKind::Audio, AttachmentKind::Image);
576 }
577
578 #[test]
579 fn attachment_construction() {
580 let a = Attachment {
581 kind: AttachmentKind::Audio,
582 data: vec![0, 1, 2],
583 filename: Some("test.wav".into()),
584 };
585 assert_eq!(a.kind, AttachmentKind::Audio);
586 assert_eq!(a.data.len(), 3);
587 assert_eq!(a.filename.as_deref(), Some("test.wav"));
588 }
589
590 #[test]
591 fn channel_message_with_attachments() {
592 let msg = ChannelMessage {
593 text: String::new(),
594 attachments: vec![Attachment {
595 kind: AttachmentKind::Audio,
596 data: vec![42],
597 filename: None,
598 }],
599 };
600 assert_eq!(msg.attachments.len(), 1);
601 assert_eq!(msg.attachments[0].kind, AttachmentKind::Audio);
602 }
603
604 #[test]
605 fn stub_channel_try_recv_returns_none() {
606 let mut ch = StubChannel;
607 assert!(ch.try_recv().is_none());
608 }
609
610 #[tokio::test]
611 async fn stub_channel_send_queue_count_noop() {
612 let mut ch = StubChannel;
613 ch.send_queue_count(5).await.unwrap();
614 }
615
616 #[test]
619 fn loopback_pair_returns_linked_handles() {
620 let (channel, handle) = LoopbackChannel::pair(8);
621 drop(channel);
623 drop(handle);
624 }
625
626 #[tokio::test]
627 async fn loopback_cancel_signal_can_be_notified_and_awaited() {
628 let (_channel, handle) = LoopbackChannel::pair(8);
629 let signal = std::sync::Arc::clone(&handle.cancel_signal);
630 let notified = signal.notified();
632 handle.cancel_signal.notify_one();
633 notified.await; }
635
636 #[tokio::test]
637 async fn loopback_cancel_signal_shared_across_clones() {
638 let (_channel, handle) = LoopbackChannel::pair(8);
639 let signal_a = std::sync::Arc::clone(&handle.cancel_signal);
640 let signal_b = std::sync::Arc::clone(&handle.cancel_signal);
641 let notified = signal_b.notified();
642 signal_a.notify_one();
643 notified.await;
644 }
645
646 #[tokio::test]
647 async fn loopback_send_recv_round_trip() {
648 let (mut channel, handle) = LoopbackChannel::pair(8);
649 handle
650 .input_tx
651 .send(ChannelMessage {
652 text: "hello".to_owned(),
653 attachments: vec![],
654 })
655 .await
656 .unwrap();
657 let msg = channel.recv().await.unwrap().unwrap();
658 assert_eq!(msg.text, "hello");
659 }
660
661 #[tokio::test]
662 async fn loopback_recv_returns_none_when_handle_dropped() {
663 let (mut channel, handle) = LoopbackChannel::pair(8);
664 drop(handle);
665 let result = channel.recv().await.unwrap();
666 assert!(result.is_none());
667 }
668
669 #[tokio::test]
670 async fn loopback_send_produces_full_message_event() {
671 let (mut channel, mut handle) = LoopbackChannel::pair(8);
672 channel.send("world").await.unwrap();
673 let event = handle.output_rx.recv().await.unwrap();
674 assert!(matches!(event, LoopbackEvent::FullMessage(t) if t == "world"));
675 }
676
677 #[tokio::test]
678 async fn loopback_send_chunk_then_flush() {
679 let (mut channel, mut handle) = LoopbackChannel::pair(8);
680 channel.send_chunk("part1").await.unwrap();
681 channel.flush_chunks().await.unwrap();
682 let ev1 = handle.output_rx.recv().await.unwrap();
683 let ev2 = handle.output_rx.recv().await.unwrap();
684 assert!(matches!(ev1, LoopbackEvent::Chunk(t) if t == "part1"));
685 assert!(matches!(ev2, LoopbackEvent::Flush));
686 }
687
688 #[tokio::test]
689 async fn loopback_send_tool_output() {
690 let (mut channel, mut handle) = LoopbackChannel::pair(8);
691 channel
692 .send_tool_output(ToolOutputEvent {
693 tool_name: "bash",
694 body: "exit 0",
695 diff: None,
696 filter_stats: None,
697 kept_lines: None,
698 locations: None,
699 tool_call_id: "",
700 is_error: false,
701 parent_tool_use_id: None,
702 raw_response: None,
703 started_at: None,
704 })
705 .await
706 .unwrap();
707 let event = handle.output_rx.recv().await.unwrap();
708 match event {
709 LoopbackEvent::ToolOutput(data) => {
710 assert_eq!(data.tool_name, "bash");
711 assert_eq!(data.display, "exit 0");
712 assert!(data.diff.is_none());
713 assert!(data.filter_stats.is_none());
714 assert!(data.kept_lines.is_none());
715 assert!(data.locations.is_none());
716 assert_eq!(data.tool_call_id, "");
717 assert!(!data.is_error);
718 assert!(data.terminal_id.is_none());
719 assert!(data.parent_tool_use_id.is_none());
720 assert!(data.raw_response.is_none());
721 }
722 _ => panic!("expected ToolOutput event"),
723 }
724 }
725
726 #[tokio::test]
727 async fn loopback_confirm_auto_approves() {
728 let (mut channel, _handle) = LoopbackChannel::pair(8);
729 let result = channel.confirm("are you sure?").await.unwrap();
730 assert!(result);
731 }
732
733 #[tokio::test]
734 async fn loopback_send_error_when_output_closed() {
735 let (mut channel, handle) = LoopbackChannel::pair(8);
736 drop(handle);
738 let result = channel.send("too late").await;
739 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
740 }
741
742 #[tokio::test]
743 async fn loopback_send_chunk_error_when_output_closed() {
744 let (mut channel, handle) = LoopbackChannel::pair(8);
745 drop(handle);
746 let result = channel.send_chunk("chunk").await;
747 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
748 }
749
750 #[tokio::test]
751 async fn loopback_flush_error_when_output_closed() {
752 let (mut channel, handle) = LoopbackChannel::pair(8);
753 drop(handle);
754 let result = channel.flush_chunks().await;
755 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
756 }
757
758 #[tokio::test]
759 async fn loopback_send_status_event() {
760 let (mut channel, mut handle) = LoopbackChannel::pair(8);
761 channel.send_status("working...").await.unwrap();
762 let event = handle.output_rx.recv().await.unwrap();
763 assert!(matches!(event, LoopbackEvent::Status(s) if s == "working..."));
764 }
765
766 #[tokio::test]
767 async fn loopback_send_usage_produces_usage_event() {
768 let (mut channel, mut handle) = LoopbackChannel::pair(8);
769 channel.send_usage(100, 50, 200_000).await.unwrap();
770 let event = handle.output_rx.recv().await.unwrap();
771 match event {
772 LoopbackEvent::Usage {
773 input_tokens,
774 output_tokens,
775 context_window,
776 } => {
777 assert_eq!(input_tokens, 100);
778 assert_eq!(output_tokens, 50);
779 assert_eq!(context_window, 200_000);
780 }
781 _ => panic!("expected Usage event"),
782 }
783 }
784
785 #[tokio::test]
786 async fn loopback_send_usage_error_when_closed() {
787 let (mut channel, handle) = LoopbackChannel::pair(8);
788 drop(handle);
789 let result = channel.send_usage(1, 2, 3).await;
790 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
791 }
792
793 #[test]
794 fn plan_item_status_variants_are_distinct() {
795 assert!(!matches!(
796 PlanItemStatus::Pending,
797 PlanItemStatus::InProgress
798 ));
799 assert!(!matches!(
800 PlanItemStatus::InProgress,
801 PlanItemStatus::Completed
802 ));
803 assert!(!matches!(
804 PlanItemStatus::Completed,
805 PlanItemStatus::Pending
806 ));
807 }
808
809 #[test]
810 fn loopback_event_session_title_carries_string() {
811 let event = LoopbackEvent::SessionTitle("hello".to_owned());
812 assert!(matches!(event, LoopbackEvent::SessionTitle(s) if s == "hello"));
813 }
814
815 #[test]
816 fn loopback_event_plan_carries_entries() {
817 let entries = vec![
818 ("step 1".to_owned(), PlanItemStatus::Pending),
819 ("step 2".to_owned(), PlanItemStatus::InProgress),
820 ];
821 let event = LoopbackEvent::Plan(entries);
822 match event {
823 LoopbackEvent::Plan(e) => {
824 assert_eq!(e.len(), 2);
825 assert!(matches!(e[0].1, PlanItemStatus::Pending));
826 assert!(matches!(e[1].1, PlanItemStatus::InProgress));
827 }
828 _ => panic!("expected Plan event"),
829 }
830 }
831
832 #[tokio::test]
833 async fn loopback_send_tool_start_produces_tool_start_event() {
834 let (mut channel, mut handle) = LoopbackChannel::pair(8);
835 channel
836 .send_tool_start(ToolStartEvent {
837 tool_name: "shell",
838 tool_call_id: "tc-001",
839 params: Some(serde_json::json!({"command": "ls"})),
840 parent_tool_use_id: None,
841 })
842 .await
843 .unwrap();
844 let event = handle.output_rx.recv().await.unwrap();
845 match event {
846 LoopbackEvent::ToolStart(data) => {
847 assert_eq!(data.tool_name, "shell");
848 assert_eq!(data.tool_call_id, "tc-001");
849 assert!(data.params.is_some());
850 assert!(data.parent_tool_use_id.is_none());
851 }
852 _ => panic!("expected ToolStart event"),
853 }
854 }
855
856 #[tokio::test]
857 async fn loopback_send_tool_start_with_parent_id() {
858 let (mut channel, mut handle) = LoopbackChannel::pair(8);
859 channel
860 .send_tool_start(ToolStartEvent {
861 tool_name: "web",
862 tool_call_id: "tc-002",
863 params: None,
864 parent_tool_use_id: Some("parent-123".into()),
865 })
866 .await
867 .unwrap();
868 let event = handle.output_rx.recv().await.unwrap();
869 assert!(matches!(
870 event,
871 LoopbackEvent::ToolStart(ref data) if data.parent_tool_use_id.as_deref() == Some("parent-123")
872 ));
873 }
874
875 #[tokio::test]
876 async fn loopback_send_tool_start_error_when_output_closed() {
877 let (mut channel, handle) = LoopbackChannel::pair(8);
878 drop(handle);
879 let result = channel
880 .send_tool_start(ToolStartEvent {
881 tool_name: "shell",
882 tool_call_id: "tc-003",
883 params: None,
884 parent_tool_use_id: None,
885 })
886 .await;
887 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
888 }
889
890 #[tokio::test]
891 async fn default_send_tool_output_formats_message() {
892 let mut ch = StubChannel;
893 ch.send_tool_output(ToolOutputEvent {
895 tool_name: "bash",
896 body: "hello",
897 diff: None,
898 filter_stats: None,
899 kept_lines: None,
900 locations: None,
901 tool_call_id: "id",
902 is_error: false,
903 parent_tool_use_id: None,
904 raw_response: None,
905 started_at: None,
906 })
907 .await
908 .unwrap();
909 }
910}