1#[derive(Debug, Clone)]
25pub struct ElicitationField {
26 pub name: String,
28 pub description: Option<String>,
30 pub field_type: ElicitationFieldType,
32 pub required: bool,
34}
35
36#[derive(Debug, Clone)]
47pub enum ElicitationFieldType {
48 String,
49 Integer,
50 Number,
51 Boolean,
52 Enum(Vec<String>),
54}
55
56#[derive(Debug, Clone)]
81pub struct ElicitationRequest {
82 pub server_name: String,
84 pub message: String,
86 pub fields: Vec<ElicitationField>,
88}
89
90#[derive(Debug, Clone)]
108pub enum ElicitationResponse {
109 Accepted(serde_json::Value),
111 Declined,
113 Cancelled,
115}
116
117#[derive(Debug, thiserror::Error)]
119pub enum ChannelError {
120 #[error("I/O error: {0}")]
122 Io(#[from] std::io::Error),
123
124 #[error("channel closed")]
126 ChannelClosed,
127
128 #[error("confirmation cancelled")]
130 ConfirmCancelled,
131
132 #[error("{0}")]
134 Other(String),
135}
136
137impl ChannelError {
138 pub fn other(e: impl std::fmt::Display) -> Self {
143 Self::Other(e.to_string())
144 }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum AttachmentKind {
150 Audio,
151 Image,
152 Video,
153 File,
154}
155
156#[derive(Debug, Clone)]
158pub struct Attachment {
159 pub kind: AttachmentKind,
160 pub data: Vec<u8>,
161 pub filename: Option<String>,
162}
163
164#[derive(Debug, Clone)]
166pub struct ChannelMessage {
167 pub text: String,
168 pub attachments: Vec<Attachment>,
169}
170
171pub trait Channel: Send {
173 fn recv(&mut self)
179 -> impl Future<Output = Result<Option<ChannelMessage>, ChannelError>> + Send;
180
181 fn try_recv(&mut self) -> Option<ChannelMessage> {
183 None
184 }
185
186 fn supports_exit(&self) -> bool {
191 true
192 }
193
194 fn send(&mut self, text: &str) -> impl Future<Output = Result<(), ChannelError>> + Send;
200
201 fn send_chunk(&mut self, chunk: &str) -> impl Future<Output = Result<(), ChannelError>> + Send;
207
208 fn flush_chunks(&mut self) -> impl Future<Output = Result<(), ChannelError>> + Send;
214
215 fn send_typing(&mut self) -> impl Future<Output = Result<(), ChannelError>> + Send {
221 async { Ok(()) }
222 }
223
224 fn send_status(
230 &mut self,
231 _text: &str,
232 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
233 async { Ok(()) }
234 }
235
236 fn send_thinking_chunk(
242 &mut self,
243 _chunk: &str,
244 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
245 async { Ok(()) }
246 }
247
248 fn send_queue_count(
254 &mut self,
255 _count: usize,
256 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
257 async { Ok(()) }
258 }
259
260 fn send_usage(
266 &mut self,
267 _input_tokens: u64,
268 _output_tokens: u64,
269 _context_window: u64,
270 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
271 async { Ok(()) }
272 }
273
274 fn send_diff(
280 &mut self,
281 _diff: crate::DiffData,
282 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
283 async { Ok(()) }
284 }
285
286 fn send_tool_start(
296 &mut self,
297 _event: ToolStartEvent,
298 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
299 async { Ok(()) }
300 }
301
302 fn send_tool_output(
312 &mut self,
313 event: ToolOutputEvent,
314 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
315 let formatted = crate::agent::format_tool_output(event.tool_name.as_str(), &event.display);
316 async move { self.send(&formatted).await }
317 }
318
319 fn confirm(
326 &mut self,
327 _prompt: &str,
328 ) -> impl Future<Output = Result<bool, ChannelError>> + Send {
329 async { Ok(true) }
330 }
331
332 fn elicit(
341 &mut self,
342 _request: ElicitationRequest,
343 ) -> impl Future<Output = Result<ElicitationResponse, ChannelError>> + Send {
344 async { Ok(ElicitationResponse::Declined) }
345 }
346
347 fn send_stop_hint(
356 &mut self,
357 _hint: StopHint,
358 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
359 async { Ok(()) }
360 }
361}
362
363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub enum StopHint {
370 MaxTokens,
372 MaxTurnRequests,
374}
375
376#[derive(Debug, Clone)]
381pub struct ToolStartEvent {
382 pub tool_name: zeph_common::ToolName,
384 pub tool_call_id: String,
386 pub params: Option<serde_json::Value>,
388 pub parent_tool_use_id: Option<String>,
390 pub started_at: std::time::Instant,
392 pub speculative: bool,
396 pub sandbox_profile: Option<zeph_tools::SandboxProfile>,
400}
401
402#[derive(Debug, Clone)]
407pub struct ToolOutputEvent {
408 pub tool_name: zeph_common::ToolName,
410 pub display: String,
412 pub diff: Option<crate::DiffData>,
414 pub filter_stats: Option<String>,
416 pub kept_lines: Option<Vec<usize>>,
418 pub locations: Option<Vec<String>>,
420 pub tool_call_id: String,
422 pub is_error: bool,
424 pub terminal_id: Option<String>,
426 pub parent_tool_use_id: Option<String>,
428 pub raw_response: Option<serde_json::Value>,
430 pub started_at: Option<std::time::Instant>,
432}
433
434pub type ToolStartData = ToolStartEvent;
438
439pub type ToolOutputData = ToolOutputEvent;
443
444#[derive(Debug, Clone)]
446pub enum LoopbackEvent {
447 Chunk(String),
448 Flush,
449 FullMessage(String),
450 Status(String),
451 ToolStart(Box<ToolStartEvent>),
453 ToolOutput(Box<ToolOutputEvent>),
454 Usage {
456 input_tokens: u64,
457 output_tokens: u64,
458 context_window: u64,
459 },
460 SessionTitle(String),
462 Plan(Vec<(String, PlanItemStatus)>),
464 ThinkingChunk(String),
466 Stop(StopHint),
470}
471
472#[derive(Debug, Clone)]
474pub enum PlanItemStatus {
475 Pending,
476 InProgress,
477 Completed,
478}
479
480pub struct LoopbackHandle {
482 pub input_tx: tokio::sync::mpsc::Sender<ChannelMessage>,
483 pub output_rx: tokio::sync::mpsc::Receiver<LoopbackEvent>,
484 pub cancel_signal: std::sync::Arc<tokio::sync::Notify>,
486}
487
488pub struct LoopbackChannel {
490 input_rx: tokio::sync::mpsc::Receiver<ChannelMessage>,
491 output_tx: tokio::sync::mpsc::Sender<LoopbackEvent>,
492}
493
494impl LoopbackChannel {
495 #[must_use]
497 pub fn pair(buffer: usize) -> (Self, LoopbackHandle) {
498 let (input_tx, input_rx) = tokio::sync::mpsc::channel(buffer);
499 let (output_tx, output_rx) = tokio::sync::mpsc::channel(buffer);
500 let cancel_signal = std::sync::Arc::new(tokio::sync::Notify::new());
501 (
502 Self {
503 input_rx,
504 output_tx,
505 },
506 LoopbackHandle {
507 input_tx,
508 output_rx,
509 cancel_signal,
510 },
511 )
512 }
513}
514
515impl Channel for LoopbackChannel {
516 fn supports_exit(&self) -> bool {
517 false
518 }
519
520 async fn recv(&mut self) -> Result<Option<ChannelMessage>, ChannelError> {
521 Ok(self.input_rx.recv().await)
522 }
523
524 async fn send(&mut self, text: &str) -> Result<(), ChannelError> {
525 self.output_tx
526 .send(LoopbackEvent::FullMessage(text.to_owned()))
527 .await
528 .map_err(|_| ChannelError::ChannelClosed)
529 }
530
531 async fn send_chunk(&mut self, chunk: &str) -> Result<(), ChannelError> {
532 self.output_tx
533 .send(LoopbackEvent::Chunk(chunk.to_owned()))
534 .await
535 .map_err(|_| ChannelError::ChannelClosed)
536 }
537
538 async fn flush_chunks(&mut self) -> Result<(), ChannelError> {
539 self.output_tx
540 .send(LoopbackEvent::Flush)
541 .await
542 .map_err(|_| ChannelError::ChannelClosed)
543 }
544
545 async fn send_status(&mut self, text: &str) -> Result<(), ChannelError> {
546 self.output_tx
547 .send(LoopbackEvent::Status(text.to_owned()))
548 .await
549 .map_err(|_| ChannelError::ChannelClosed)
550 }
551
552 async fn send_thinking_chunk(&mut self, chunk: &str) -> Result<(), ChannelError> {
553 self.output_tx
554 .send(LoopbackEvent::ThinkingChunk(chunk.to_owned()))
555 .await
556 .map_err(|_| ChannelError::ChannelClosed)
557 }
558
559 async fn send_tool_start(&mut self, event: ToolStartEvent) -> Result<(), ChannelError> {
560 self.output_tx
561 .send(LoopbackEvent::ToolStart(Box::new(event)))
562 .await
563 .map_err(|_| ChannelError::ChannelClosed)
564 }
565
566 async fn send_tool_output(&mut self, event: ToolOutputEvent) -> Result<(), ChannelError> {
567 self.output_tx
568 .send(LoopbackEvent::ToolOutput(Box::new(event)))
569 .await
570 .map_err(|_| ChannelError::ChannelClosed)
571 }
572
573 async fn confirm(&mut self, _prompt: &str) -> Result<bool, ChannelError> {
574 Ok(true)
575 }
576
577 async fn send_stop_hint(&mut self, hint: StopHint) -> Result<(), ChannelError> {
578 self.output_tx
579 .send(LoopbackEvent::Stop(hint))
580 .await
581 .map_err(|_| ChannelError::ChannelClosed)
582 }
583
584 async fn send_usage(
585 &mut self,
586 input_tokens: u64,
587 output_tokens: u64,
588 context_window: u64,
589 ) -> Result<(), ChannelError> {
590 self.output_tx
591 .send(LoopbackEvent::Usage {
592 input_tokens,
593 output_tokens,
594 context_window,
595 })
596 .await
597 .map_err(|_| ChannelError::ChannelClosed)
598 }
599}
600
601pub(crate) struct ChannelSinkAdapter<'a, C: Channel>(pub &'a mut C);
606
607impl<C: Channel> zeph_commands::ChannelSink for ChannelSinkAdapter<'_, C> {
608 fn send<'a>(
609 &'a mut self,
610 msg: &'a str,
611 ) -> std::pin::Pin<
612 Box<dyn std::future::Future<Output = Result<(), zeph_commands::CommandError>> + Send + 'a>,
613 > {
614 Box::pin(async move {
615 self.0
616 .send(msg)
617 .await
618 .map_err(zeph_commands::CommandError::new)
619 })
620 }
621
622 fn flush_chunks<'a>(
623 &'a mut self,
624 ) -> std::pin::Pin<
625 Box<dyn std::future::Future<Output = Result<(), zeph_commands::CommandError>> + Send + 'a>,
626 > {
627 Box::pin(async move {
628 self.0
629 .flush_chunks()
630 .await
631 .map_err(zeph_commands::CommandError::new)
632 })
633 }
634
635 fn send_queue_count<'a>(
636 &'a mut self,
637 count: usize,
638 ) -> std::pin::Pin<
639 Box<dyn std::future::Future<Output = Result<(), zeph_commands::CommandError>> + Send + 'a>,
640 > {
641 Box::pin(async move {
642 self.0
643 .send_queue_count(count)
644 .await
645 .map_err(zeph_commands::CommandError::new)
646 })
647 }
648
649 fn supports_exit(&self) -> bool {
650 self.0.supports_exit()
651 }
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657
658 #[test]
659 fn channel_message_creation() {
660 let msg = ChannelMessage {
661 text: "hello".to_string(),
662 attachments: vec![],
663 };
664 assert_eq!(msg.text, "hello");
665 assert!(msg.attachments.is_empty());
666 }
667
668 struct StubChannel;
669
670 impl Channel for StubChannel {
671 async fn recv(&mut self) -> Result<Option<ChannelMessage>, ChannelError> {
672 Ok(None)
673 }
674
675 async fn send(&mut self, _text: &str) -> Result<(), ChannelError> {
676 Ok(())
677 }
678
679 async fn send_chunk(&mut self, _chunk: &str) -> Result<(), ChannelError> {
680 Ok(())
681 }
682
683 async fn flush_chunks(&mut self) -> Result<(), ChannelError> {
684 Ok(())
685 }
686 }
687
688 #[tokio::test]
689 async fn send_chunk_default_is_noop() {
690 let mut ch = StubChannel;
691 ch.send_chunk("partial").await.unwrap();
692 }
693
694 #[tokio::test]
695 async fn flush_chunks_default_is_noop() {
696 let mut ch = StubChannel;
697 ch.flush_chunks().await.unwrap();
698 }
699
700 #[tokio::test]
701 async fn stub_channel_confirm_auto_approves() {
702 let mut ch = StubChannel;
703 let result = ch.confirm("Delete everything?").await.unwrap();
704 assert!(result);
705 }
706
707 #[tokio::test]
708 async fn stub_channel_send_typing_default() {
709 let mut ch = StubChannel;
710 ch.send_typing().await.unwrap();
711 }
712
713 #[tokio::test]
714 async fn stub_channel_recv_returns_none() {
715 let mut ch = StubChannel;
716 let msg = ch.recv().await.unwrap();
717 assert!(msg.is_none());
718 }
719
720 #[tokio::test]
721 async fn stub_channel_send_ok() {
722 let mut ch = StubChannel;
723 ch.send("hello").await.unwrap();
724 }
725
726 #[test]
727 fn channel_message_clone() {
728 let msg = ChannelMessage {
729 text: "test".to_string(),
730 attachments: vec![],
731 };
732 let cloned = msg.clone();
733 assert_eq!(cloned.text, "test");
734 }
735
736 #[test]
737 fn channel_message_debug() {
738 let msg = ChannelMessage {
739 text: "debug".to_string(),
740 attachments: vec![],
741 };
742 let debug = format!("{msg:?}");
743 assert!(debug.contains("debug"));
744 }
745
746 #[test]
747 fn attachment_kind_equality() {
748 assert_eq!(AttachmentKind::Audio, AttachmentKind::Audio);
749 assert_ne!(AttachmentKind::Audio, AttachmentKind::Image);
750 }
751
752 #[test]
753 fn attachment_construction() {
754 let a = Attachment {
755 kind: AttachmentKind::Audio,
756 data: vec![0, 1, 2],
757 filename: Some("test.wav".into()),
758 };
759 assert_eq!(a.kind, AttachmentKind::Audio);
760 assert_eq!(a.data.len(), 3);
761 assert_eq!(a.filename.as_deref(), Some("test.wav"));
762 }
763
764 #[test]
765 fn channel_message_with_attachments() {
766 let msg = ChannelMessage {
767 text: String::new(),
768 attachments: vec![Attachment {
769 kind: AttachmentKind::Audio,
770 data: vec![42],
771 filename: None,
772 }],
773 };
774 assert_eq!(msg.attachments.len(), 1);
775 assert_eq!(msg.attachments[0].kind, AttachmentKind::Audio);
776 }
777
778 #[test]
779 fn stub_channel_try_recv_returns_none() {
780 let mut ch = StubChannel;
781 assert!(ch.try_recv().is_none());
782 }
783
784 #[tokio::test]
785 async fn stub_channel_send_queue_count_noop() {
786 let mut ch = StubChannel;
787 ch.send_queue_count(5).await.unwrap();
788 }
789
790 #[test]
793 fn loopback_pair_returns_linked_handles() {
794 let (channel, handle) = LoopbackChannel::pair(8);
795 drop(channel);
797 drop(handle);
798 }
799
800 #[tokio::test]
801 async fn loopback_cancel_signal_can_be_notified_and_awaited() {
802 let (_channel, handle) = LoopbackChannel::pair(8);
803 let signal = std::sync::Arc::clone(&handle.cancel_signal);
804 let notified = signal.notified();
806 handle.cancel_signal.notify_one();
807 notified.await; }
809
810 #[tokio::test]
811 async fn loopback_cancel_signal_shared_across_clones() {
812 let (_channel, handle) = LoopbackChannel::pair(8);
813 let signal_a = std::sync::Arc::clone(&handle.cancel_signal);
814 let signal_b = std::sync::Arc::clone(&handle.cancel_signal);
815 let notified = signal_b.notified();
816 signal_a.notify_one();
817 notified.await;
818 }
819
820 #[tokio::test]
821 async fn loopback_send_recv_round_trip() {
822 let (mut channel, handle) = LoopbackChannel::pair(8);
823 handle
824 .input_tx
825 .send(ChannelMessage {
826 text: "hello".to_owned(),
827 attachments: vec![],
828 })
829 .await
830 .unwrap();
831 let msg = channel.recv().await.unwrap().unwrap();
832 assert_eq!(msg.text, "hello");
833 }
834
835 #[tokio::test]
836 async fn loopback_recv_returns_none_when_handle_dropped() {
837 let (mut channel, handle) = LoopbackChannel::pair(8);
838 drop(handle);
839 let result = channel.recv().await.unwrap();
840 assert!(result.is_none());
841 }
842
843 #[tokio::test]
844 async fn loopback_send_produces_full_message_event() {
845 let (mut channel, mut handle) = LoopbackChannel::pair(8);
846 channel.send("world").await.unwrap();
847 let event = handle.output_rx.recv().await.unwrap();
848 assert!(matches!(event, LoopbackEvent::FullMessage(t) if t == "world"));
849 }
850
851 #[tokio::test]
852 async fn loopback_send_chunk_then_flush() {
853 let (mut channel, mut handle) = LoopbackChannel::pair(8);
854 channel.send_chunk("part1").await.unwrap();
855 channel.flush_chunks().await.unwrap();
856 let ev1 = handle.output_rx.recv().await.unwrap();
857 let ev2 = handle.output_rx.recv().await.unwrap();
858 assert!(matches!(ev1, LoopbackEvent::Chunk(t) if t == "part1"));
859 assert!(matches!(ev2, LoopbackEvent::Flush));
860 }
861
862 #[tokio::test]
863 async fn loopback_send_tool_output() {
864 let (mut channel, mut handle) = LoopbackChannel::pair(8);
865 channel
866 .send_tool_output(ToolOutputEvent {
867 tool_name: "bash".into(),
868 display: "exit 0".into(),
869 diff: None,
870 filter_stats: None,
871 kept_lines: None,
872 locations: None,
873 tool_call_id: String::new(),
874 terminal_id: None,
875 is_error: false,
876 parent_tool_use_id: None,
877 raw_response: None,
878 started_at: None,
879 })
880 .await
881 .unwrap();
882 let event = handle.output_rx.recv().await.unwrap();
883 match event {
884 LoopbackEvent::ToolOutput(data) => {
885 assert_eq!(data.tool_name, "bash");
886 assert_eq!(data.display, "exit 0");
887 assert!(data.diff.is_none());
888 assert!(data.filter_stats.is_none());
889 assert!(data.kept_lines.is_none());
890 assert!(data.locations.is_none());
891 assert_eq!(data.tool_call_id, "");
892 assert!(!data.is_error);
893 assert!(data.terminal_id.is_none());
894 assert!(data.parent_tool_use_id.is_none());
895 assert!(data.raw_response.is_none());
896 }
897 _ => panic!("expected ToolOutput event"),
898 }
899 }
900
901 #[tokio::test]
902 async fn loopback_confirm_auto_approves() {
903 let (mut channel, _handle) = LoopbackChannel::pair(8);
904 let result = channel.confirm("are you sure?").await.unwrap();
905 assert!(result);
906 }
907
908 #[tokio::test]
909 async fn loopback_send_error_when_output_closed() {
910 let (mut channel, handle) = LoopbackChannel::pair(8);
911 drop(handle);
913 let result = channel.send("too late").await;
914 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
915 }
916
917 #[tokio::test]
918 async fn loopback_send_chunk_error_when_output_closed() {
919 let (mut channel, handle) = LoopbackChannel::pair(8);
920 drop(handle);
921 let result = channel.send_chunk("chunk").await;
922 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
923 }
924
925 #[tokio::test]
926 async fn loopback_flush_error_when_output_closed() {
927 let (mut channel, handle) = LoopbackChannel::pair(8);
928 drop(handle);
929 let result = channel.flush_chunks().await;
930 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
931 }
932
933 #[tokio::test]
934 async fn loopback_send_status_event() {
935 let (mut channel, mut handle) = LoopbackChannel::pair(8);
936 channel.send_status("working...").await.unwrap();
937 let event = handle.output_rx.recv().await.unwrap();
938 assert!(matches!(event, LoopbackEvent::Status(s) if s == "working..."));
939 }
940
941 #[tokio::test]
942 async fn loopback_send_usage_produces_usage_event() {
943 let (mut channel, mut handle) = LoopbackChannel::pair(8);
944 channel.send_usage(100, 50, 200_000).await.unwrap();
945 let event = handle.output_rx.recv().await.unwrap();
946 match event {
947 LoopbackEvent::Usage {
948 input_tokens,
949 output_tokens,
950 context_window,
951 } => {
952 assert_eq!(input_tokens, 100);
953 assert_eq!(output_tokens, 50);
954 assert_eq!(context_window, 200_000);
955 }
956 _ => panic!("expected Usage event"),
957 }
958 }
959
960 #[tokio::test]
961 async fn loopback_send_usage_error_when_closed() {
962 let (mut channel, handle) = LoopbackChannel::pair(8);
963 drop(handle);
964 let result = channel.send_usage(1, 2, 3).await;
965 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
966 }
967
968 #[test]
969 fn plan_item_status_variants_are_distinct() {
970 assert!(!matches!(
971 PlanItemStatus::Pending,
972 PlanItemStatus::InProgress
973 ));
974 assert!(!matches!(
975 PlanItemStatus::InProgress,
976 PlanItemStatus::Completed
977 ));
978 assert!(!matches!(
979 PlanItemStatus::Completed,
980 PlanItemStatus::Pending
981 ));
982 }
983
984 #[test]
985 fn loopback_event_session_title_carries_string() {
986 let event = LoopbackEvent::SessionTitle("hello".to_owned());
987 assert!(matches!(event, LoopbackEvent::SessionTitle(s) if s == "hello"));
988 }
989
990 #[test]
991 fn loopback_event_plan_carries_entries() {
992 let entries = vec![
993 ("step 1".to_owned(), PlanItemStatus::Pending),
994 ("step 2".to_owned(), PlanItemStatus::InProgress),
995 ];
996 let event = LoopbackEvent::Plan(entries);
997 match event {
998 LoopbackEvent::Plan(e) => {
999 assert_eq!(e.len(), 2);
1000 assert!(matches!(e[0].1, PlanItemStatus::Pending));
1001 assert!(matches!(e[1].1, PlanItemStatus::InProgress));
1002 }
1003 _ => panic!("expected Plan event"),
1004 }
1005 }
1006
1007 #[tokio::test]
1008 async fn loopback_send_tool_start_produces_tool_start_event() {
1009 let (mut channel, mut handle) = LoopbackChannel::pair(8);
1010 channel
1011 .send_tool_start(ToolStartEvent {
1012 tool_name: "shell".into(),
1013 tool_call_id: "tc-001".into(),
1014 params: Some(serde_json::json!({"command": "ls"})),
1015 parent_tool_use_id: None,
1016 started_at: std::time::Instant::now(),
1017 speculative: false,
1018 sandbox_profile: None,
1019 })
1020 .await
1021 .unwrap();
1022 let event = handle.output_rx.recv().await.unwrap();
1023 match event {
1024 LoopbackEvent::ToolStart(data) => {
1025 assert_eq!(data.tool_name.as_str(), "shell");
1026 assert_eq!(data.tool_call_id.as_str(), "tc-001");
1027 assert!(data.params.is_some());
1028 assert!(data.parent_tool_use_id.is_none());
1029 }
1030 _ => panic!("expected ToolStart event"),
1031 }
1032 }
1033
1034 #[tokio::test]
1035 async fn loopback_send_tool_start_with_parent_id() {
1036 let (mut channel, mut handle) = LoopbackChannel::pair(8);
1037 channel
1038 .send_tool_start(ToolStartEvent {
1039 tool_name: "web".into(),
1040 tool_call_id: "tc-002".into(),
1041 params: None,
1042 parent_tool_use_id: Some("parent-123".into()),
1043 started_at: std::time::Instant::now(),
1044 speculative: false,
1045 sandbox_profile: None,
1046 })
1047 .await
1048 .unwrap();
1049 let event = handle.output_rx.recv().await.unwrap();
1050 assert!(matches!(
1051 event,
1052 LoopbackEvent::ToolStart(ref data) if data.parent_tool_use_id.as_deref() == Some("parent-123")
1053 ));
1054 }
1055
1056 #[tokio::test]
1057 async fn loopback_send_tool_start_error_when_output_closed() {
1058 let (mut channel, handle) = LoopbackChannel::pair(8);
1059 drop(handle);
1060 let result = channel
1061 .send_tool_start(ToolStartEvent {
1062 tool_name: "shell".into(),
1063 tool_call_id: "tc-003".into(),
1064 params: None,
1065 parent_tool_use_id: None,
1066 started_at: std::time::Instant::now(),
1067 speculative: false,
1068 sandbox_profile: None,
1069 })
1070 .await;
1071 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
1072 }
1073
1074 #[tokio::test]
1075 async fn default_send_tool_output_formats_message() {
1076 let mut ch = StubChannel;
1077 ch.send_tool_output(ToolOutputEvent {
1079 tool_name: "bash".into(),
1080 display: "hello".into(),
1081 diff: None,
1082 filter_stats: None,
1083 kept_lines: None,
1084 locations: None,
1085 tool_call_id: "id".into(),
1086 terminal_id: None,
1087 is_error: false,
1088 parent_tool_use_id: None,
1089 raw_response: None,
1090 started_at: None,
1091 })
1092 .await
1093 .unwrap();
1094 }
1095}