1#[derive(Debug, Clone)]
6pub struct ElicitationField {
7 pub name: String,
8 pub description: Option<String>,
9 pub field_type: ElicitationFieldType,
10 pub required: bool,
11}
12
13#[derive(Debug, Clone)]
15pub enum ElicitationFieldType {
16 String,
17 Integer,
18 Number,
19 Boolean,
20 Enum(Vec<String>),
22}
23
24#[derive(Debug, Clone)]
26pub struct ElicitationRequest {
27 pub server_name: String,
29 pub message: String,
31 pub fields: Vec<ElicitationField>,
33}
34
35#[derive(Debug, Clone)]
37pub enum ElicitationResponse {
38 Accepted(serde_json::Value),
40 Declined,
42 Cancelled,
44}
45
46#[derive(Debug, thiserror::Error)]
48pub enum ChannelError {
49 #[error("I/O error: {0}")]
51 Io(#[from] std::io::Error),
52
53 #[error("channel closed")]
55 ChannelClosed,
56
57 #[error("confirmation cancelled")]
59 ConfirmCancelled,
60
61 #[error("{0}")]
63 Other(String),
64}
65
66impl ChannelError {
67 pub fn other(e: impl std::fmt::Display) -> Self {
72 Self::Other(e.to_string())
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum AttachmentKind {
79 Audio,
80 Image,
81 Video,
82 File,
83}
84
85#[derive(Debug, Clone)]
87pub struct Attachment {
88 pub kind: AttachmentKind,
89 pub data: Vec<u8>,
90 pub filename: Option<String>,
91}
92
93#[derive(Debug, Clone)]
95pub struct ChannelMessage {
96 pub text: String,
97 pub attachments: Vec<Attachment>,
98}
99
100pub trait Channel: Send {
102 fn recv(&mut self)
108 -> impl Future<Output = Result<Option<ChannelMessage>, ChannelError>> + Send;
109
110 fn try_recv(&mut self) -> Option<ChannelMessage> {
112 None
113 }
114
115 fn supports_exit(&self) -> bool {
120 true
121 }
122
123 fn send(&mut self, text: &str) -> impl Future<Output = Result<(), ChannelError>> + Send;
129
130 fn send_chunk(&mut self, chunk: &str) -> impl Future<Output = Result<(), ChannelError>> + Send;
136
137 fn flush_chunks(&mut self) -> impl Future<Output = Result<(), ChannelError>> + Send;
143
144 fn send_typing(&mut self) -> impl Future<Output = Result<(), ChannelError>> + Send {
150 async { Ok(()) }
151 }
152
153 fn send_status(
159 &mut self,
160 _text: &str,
161 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
162 async { Ok(()) }
163 }
164
165 fn send_thinking_chunk(
171 &mut self,
172 _chunk: &str,
173 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
174 async { Ok(()) }
175 }
176
177 fn send_queue_count(
183 &mut self,
184 _count: usize,
185 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
186 async { Ok(()) }
187 }
188
189 fn send_usage(
195 &mut self,
196 _input_tokens: u64,
197 _output_tokens: u64,
198 _context_window: u64,
199 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
200 async { Ok(()) }
201 }
202
203 fn send_diff(
209 &mut self,
210 _diff: crate::DiffData,
211 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
212 async { Ok(()) }
213 }
214
215 fn send_tool_start(
225 &mut self,
226 _event: ToolStartEvent,
227 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
228 async { Ok(()) }
229 }
230
231 fn send_tool_output(
241 &mut self,
242 event: ToolOutputEvent,
243 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
244 let formatted = crate::agent::format_tool_output(event.tool_name.as_str(), &event.display);
245 async move { self.send(&formatted).await }
246 }
247
248 fn confirm(
255 &mut self,
256 _prompt: &str,
257 ) -> impl Future<Output = Result<bool, ChannelError>> + Send {
258 async { Ok(true) }
259 }
260
261 fn elicit(
270 &mut self,
271 _request: ElicitationRequest,
272 ) -> impl Future<Output = Result<ElicitationResponse, ChannelError>> + Send {
273 async { Ok(ElicitationResponse::Declined) }
274 }
275
276 fn send_stop_hint(
285 &mut self,
286 _hint: StopHint,
287 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
288 async { Ok(()) }
289 }
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub enum StopHint {
299 MaxTokens,
301 MaxTurnRequests,
303}
304
305#[derive(Debug, Clone)]
310pub struct ToolStartEvent {
311 pub tool_name: zeph_common::ToolName,
313 pub tool_call_id: String,
315 pub params: Option<serde_json::Value>,
317 pub parent_tool_use_id: Option<String>,
319 pub started_at: std::time::Instant,
321 pub speculative: bool,
325 pub sandbox_profile: Option<zeph_tools::SandboxProfile>,
329}
330
331#[derive(Debug, Clone)]
336pub struct ToolOutputEvent {
337 pub tool_name: zeph_common::ToolName,
339 pub display: String,
341 pub diff: Option<crate::DiffData>,
343 pub filter_stats: Option<String>,
345 pub kept_lines: Option<Vec<usize>>,
347 pub locations: Option<Vec<String>>,
349 pub tool_call_id: String,
351 pub is_error: bool,
353 pub terminal_id: Option<String>,
355 pub parent_tool_use_id: Option<String>,
357 pub raw_response: Option<serde_json::Value>,
359 pub started_at: Option<std::time::Instant>,
361}
362
363pub type ToolStartData = ToolStartEvent;
367
368pub type ToolOutputData = ToolOutputEvent;
372
373#[derive(Debug, Clone)]
375pub enum LoopbackEvent {
376 Chunk(String),
377 Flush,
378 FullMessage(String),
379 Status(String),
380 ToolStart(Box<ToolStartEvent>),
382 ToolOutput(Box<ToolOutputEvent>),
383 Usage {
385 input_tokens: u64,
386 output_tokens: u64,
387 context_window: u64,
388 },
389 SessionTitle(String),
391 Plan(Vec<(String, PlanItemStatus)>),
393 ThinkingChunk(String),
395 Stop(StopHint),
399}
400
401#[derive(Debug, Clone)]
403pub enum PlanItemStatus {
404 Pending,
405 InProgress,
406 Completed,
407}
408
409pub struct LoopbackHandle {
411 pub input_tx: tokio::sync::mpsc::Sender<ChannelMessage>,
412 pub output_rx: tokio::sync::mpsc::Receiver<LoopbackEvent>,
413 pub cancel_signal: std::sync::Arc<tokio::sync::Notify>,
415}
416
417pub struct LoopbackChannel {
419 input_rx: tokio::sync::mpsc::Receiver<ChannelMessage>,
420 output_tx: tokio::sync::mpsc::Sender<LoopbackEvent>,
421}
422
423impl LoopbackChannel {
424 #[must_use]
426 pub fn pair(buffer: usize) -> (Self, LoopbackHandle) {
427 let (input_tx, input_rx) = tokio::sync::mpsc::channel(buffer);
428 let (output_tx, output_rx) = tokio::sync::mpsc::channel(buffer);
429 let cancel_signal = std::sync::Arc::new(tokio::sync::Notify::new());
430 (
431 Self {
432 input_rx,
433 output_tx,
434 },
435 LoopbackHandle {
436 input_tx,
437 output_rx,
438 cancel_signal,
439 },
440 )
441 }
442}
443
444impl Channel for LoopbackChannel {
445 fn supports_exit(&self) -> bool {
446 false
447 }
448
449 async fn recv(&mut self) -> Result<Option<ChannelMessage>, ChannelError> {
450 Ok(self.input_rx.recv().await)
451 }
452
453 async fn send(&mut self, text: &str) -> Result<(), ChannelError> {
454 self.output_tx
455 .send(LoopbackEvent::FullMessage(text.to_owned()))
456 .await
457 .map_err(|_| ChannelError::ChannelClosed)
458 }
459
460 async fn send_chunk(&mut self, chunk: &str) -> Result<(), ChannelError> {
461 self.output_tx
462 .send(LoopbackEvent::Chunk(chunk.to_owned()))
463 .await
464 .map_err(|_| ChannelError::ChannelClosed)
465 }
466
467 async fn flush_chunks(&mut self) -> Result<(), ChannelError> {
468 self.output_tx
469 .send(LoopbackEvent::Flush)
470 .await
471 .map_err(|_| ChannelError::ChannelClosed)
472 }
473
474 async fn send_status(&mut self, text: &str) -> Result<(), ChannelError> {
475 self.output_tx
476 .send(LoopbackEvent::Status(text.to_owned()))
477 .await
478 .map_err(|_| ChannelError::ChannelClosed)
479 }
480
481 async fn send_thinking_chunk(&mut self, chunk: &str) -> Result<(), ChannelError> {
482 self.output_tx
483 .send(LoopbackEvent::ThinkingChunk(chunk.to_owned()))
484 .await
485 .map_err(|_| ChannelError::ChannelClosed)
486 }
487
488 async fn send_tool_start(&mut self, event: ToolStartEvent) -> Result<(), ChannelError> {
489 self.output_tx
490 .send(LoopbackEvent::ToolStart(Box::new(event)))
491 .await
492 .map_err(|_| ChannelError::ChannelClosed)
493 }
494
495 async fn send_tool_output(&mut self, event: ToolOutputEvent) -> Result<(), ChannelError> {
496 self.output_tx
497 .send(LoopbackEvent::ToolOutput(Box::new(event)))
498 .await
499 .map_err(|_| ChannelError::ChannelClosed)
500 }
501
502 async fn confirm(&mut self, _prompt: &str) -> Result<bool, ChannelError> {
503 Ok(true)
504 }
505
506 async fn send_stop_hint(&mut self, hint: StopHint) -> Result<(), ChannelError> {
507 self.output_tx
508 .send(LoopbackEvent::Stop(hint))
509 .await
510 .map_err(|_| ChannelError::ChannelClosed)
511 }
512
513 async fn send_usage(
514 &mut self,
515 input_tokens: u64,
516 output_tokens: u64,
517 context_window: u64,
518 ) -> Result<(), ChannelError> {
519 self.output_tx
520 .send(LoopbackEvent::Usage {
521 input_tokens,
522 output_tokens,
523 context_window,
524 })
525 .await
526 .map_err(|_| ChannelError::ChannelClosed)
527 }
528}
529
530pub(crate) struct ChannelSinkAdapter<'a, C: Channel>(pub &'a mut C);
535
536impl<C: Channel> zeph_commands::ChannelSink for ChannelSinkAdapter<'_, C> {
537 fn send<'a>(
538 &'a mut self,
539 msg: &'a str,
540 ) -> std::pin::Pin<
541 Box<dyn std::future::Future<Output = Result<(), zeph_commands::CommandError>> + Send + 'a>,
542 > {
543 Box::pin(async move {
544 self.0
545 .send(msg)
546 .await
547 .map_err(zeph_commands::CommandError::new)
548 })
549 }
550
551 fn flush_chunks<'a>(
552 &'a mut self,
553 ) -> std::pin::Pin<
554 Box<dyn std::future::Future<Output = Result<(), zeph_commands::CommandError>> + Send + 'a>,
555 > {
556 Box::pin(async move {
557 self.0
558 .flush_chunks()
559 .await
560 .map_err(zeph_commands::CommandError::new)
561 })
562 }
563
564 fn send_queue_count<'a>(
565 &'a mut self,
566 count: usize,
567 ) -> std::pin::Pin<
568 Box<dyn std::future::Future<Output = Result<(), zeph_commands::CommandError>> + Send + 'a>,
569 > {
570 Box::pin(async move {
571 self.0
572 .send_queue_count(count)
573 .await
574 .map_err(zeph_commands::CommandError::new)
575 })
576 }
577
578 fn supports_exit(&self) -> bool {
579 self.0.supports_exit()
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586
587 #[test]
588 fn channel_message_creation() {
589 let msg = ChannelMessage {
590 text: "hello".to_string(),
591 attachments: vec![],
592 };
593 assert_eq!(msg.text, "hello");
594 assert!(msg.attachments.is_empty());
595 }
596
597 struct StubChannel;
598
599 impl Channel for StubChannel {
600 async fn recv(&mut self) -> Result<Option<ChannelMessage>, ChannelError> {
601 Ok(None)
602 }
603
604 async fn send(&mut self, _text: &str) -> Result<(), ChannelError> {
605 Ok(())
606 }
607
608 async fn send_chunk(&mut self, _chunk: &str) -> Result<(), ChannelError> {
609 Ok(())
610 }
611
612 async fn flush_chunks(&mut self) -> Result<(), ChannelError> {
613 Ok(())
614 }
615 }
616
617 #[tokio::test]
618 async fn send_chunk_default_is_noop() {
619 let mut ch = StubChannel;
620 ch.send_chunk("partial").await.unwrap();
621 }
622
623 #[tokio::test]
624 async fn flush_chunks_default_is_noop() {
625 let mut ch = StubChannel;
626 ch.flush_chunks().await.unwrap();
627 }
628
629 #[tokio::test]
630 async fn stub_channel_confirm_auto_approves() {
631 let mut ch = StubChannel;
632 let result = ch.confirm("Delete everything?").await.unwrap();
633 assert!(result);
634 }
635
636 #[tokio::test]
637 async fn stub_channel_send_typing_default() {
638 let mut ch = StubChannel;
639 ch.send_typing().await.unwrap();
640 }
641
642 #[tokio::test]
643 async fn stub_channel_recv_returns_none() {
644 let mut ch = StubChannel;
645 let msg = ch.recv().await.unwrap();
646 assert!(msg.is_none());
647 }
648
649 #[tokio::test]
650 async fn stub_channel_send_ok() {
651 let mut ch = StubChannel;
652 ch.send("hello").await.unwrap();
653 }
654
655 #[test]
656 fn channel_message_clone() {
657 let msg = ChannelMessage {
658 text: "test".to_string(),
659 attachments: vec![],
660 };
661 let cloned = msg.clone();
662 assert_eq!(cloned.text, "test");
663 }
664
665 #[test]
666 fn channel_message_debug() {
667 let msg = ChannelMessage {
668 text: "debug".to_string(),
669 attachments: vec![],
670 };
671 let debug = format!("{msg:?}");
672 assert!(debug.contains("debug"));
673 }
674
675 #[test]
676 fn attachment_kind_equality() {
677 assert_eq!(AttachmentKind::Audio, AttachmentKind::Audio);
678 assert_ne!(AttachmentKind::Audio, AttachmentKind::Image);
679 }
680
681 #[test]
682 fn attachment_construction() {
683 let a = Attachment {
684 kind: AttachmentKind::Audio,
685 data: vec![0, 1, 2],
686 filename: Some("test.wav".into()),
687 };
688 assert_eq!(a.kind, AttachmentKind::Audio);
689 assert_eq!(a.data.len(), 3);
690 assert_eq!(a.filename.as_deref(), Some("test.wav"));
691 }
692
693 #[test]
694 fn channel_message_with_attachments() {
695 let msg = ChannelMessage {
696 text: String::new(),
697 attachments: vec![Attachment {
698 kind: AttachmentKind::Audio,
699 data: vec![42],
700 filename: None,
701 }],
702 };
703 assert_eq!(msg.attachments.len(), 1);
704 assert_eq!(msg.attachments[0].kind, AttachmentKind::Audio);
705 }
706
707 #[test]
708 fn stub_channel_try_recv_returns_none() {
709 let mut ch = StubChannel;
710 assert!(ch.try_recv().is_none());
711 }
712
713 #[tokio::test]
714 async fn stub_channel_send_queue_count_noop() {
715 let mut ch = StubChannel;
716 ch.send_queue_count(5).await.unwrap();
717 }
718
719 #[test]
722 fn loopback_pair_returns_linked_handles() {
723 let (channel, handle) = LoopbackChannel::pair(8);
724 drop(channel);
726 drop(handle);
727 }
728
729 #[tokio::test]
730 async fn loopback_cancel_signal_can_be_notified_and_awaited() {
731 let (_channel, handle) = LoopbackChannel::pair(8);
732 let signal = std::sync::Arc::clone(&handle.cancel_signal);
733 let notified = signal.notified();
735 handle.cancel_signal.notify_one();
736 notified.await; }
738
739 #[tokio::test]
740 async fn loopback_cancel_signal_shared_across_clones() {
741 let (_channel, handle) = LoopbackChannel::pair(8);
742 let signal_a = std::sync::Arc::clone(&handle.cancel_signal);
743 let signal_b = std::sync::Arc::clone(&handle.cancel_signal);
744 let notified = signal_b.notified();
745 signal_a.notify_one();
746 notified.await;
747 }
748
749 #[tokio::test]
750 async fn loopback_send_recv_round_trip() {
751 let (mut channel, handle) = LoopbackChannel::pair(8);
752 handle
753 .input_tx
754 .send(ChannelMessage {
755 text: "hello".to_owned(),
756 attachments: vec![],
757 })
758 .await
759 .unwrap();
760 let msg = channel.recv().await.unwrap().unwrap();
761 assert_eq!(msg.text, "hello");
762 }
763
764 #[tokio::test]
765 async fn loopback_recv_returns_none_when_handle_dropped() {
766 let (mut channel, handle) = LoopbackChannel::pair(8);
767 drop(handle);
768 let result = channel.recv().await.unwrap();
769 assert!(result.is_none());
770 }
771
772 #[tokio::test]
773 async fn loopback_send_produces_full_message_event() {
774 let (mut channel, mut handle) = LoopbackChannel::pair(8);
775 channel.send("world").await.unwrap();
776 let event = handle.output_rx.recv().await.unwrap();
777 assert!(matches!(event, LoopbackEvent::FullMessage(t) if t == "world"));
778 }
779
780 #[tokio::test]
781 async fn loopback_send_chunk_then_flush() {
782 let (mut channel, mut handle) = LoopbackChannel::pair(8);
783 channel.send_chunk("part1").await.unwrap();
784 channel.flush_chunks().await.unwrap();
785 let ev1 = handle.output_rx.recv().await.unwrap();
786 let ev2 = handle.output_rx.recv().await.unwrap();
787 assert!(matches!(ev1, LoopbackEvent::Chunk(t) if t == "part1"));
788 assert!(matches!(ev2, LoopbackEvent::Flush));
789 }
790
791 #[tokio::test]
792 async fn loopback_send_tool_output() {
793 let (mut channel, mut handle) = LoopbackChannel::pair(8);
794 channel
795 .send_tool_output(ToolOutputEvent {
796 tool_name: "bash".into(),
797 display: "exit 0".into(),
798 diff: None,
799 filter_stats: None,
800 kept_lines: None,
801 locations: None,
802 tool_call_id: String::new(),
803 terminal_id: None,
804 is_error: false,
805 parent_tool_use_id: None,
806 raw_response: None,
807 started_at: None,
808 })
809 .await
810 .unwrap();
811 let event = handle.output_rx.recv().await.unwrap();
812 match event {
813 LoopbackEvent::ToolOutput(data) => {
814 assert_eq!(data.tool_name, "bash");
815 assert_eq!(data.display, "exit 0");
816 assert!(data.diff.is_none());
817 assert!(data.filter_stats.is_none());
818 assert!(data.kept_lines.is_none());
819 assert!(data.locations.is_none());
820 assert_eq!(data.tool_call_id, "");
821 assert!(!data.is_error);
822 assert!(data.terminal_id.is_none());
823 assert!(data.parent_tool_use_id.is_none());
824 assert!(data.raw_response.is_none());
825 }
826 _ => panic!("expected ToolOutput event"),
827 }
828 }
829
830 #[tokio::test]
831 async fn loopback_confirm_auto_approves() {
832 let (mut channel, _handle) = LoopbackChannel::pair(8);
833 let result = channel.confirm("are you sure?").await.unwrap();
834 assert!(result);
835 }
836
837 #[tokio::test]
838 async fn loopback_send_error_when_output_closed() {
839 let (mut channel, handle) = LoopbackChannel::pair(8);
840 drop(handle);
842 let result = channel.send("too late").await;
843 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
844 }
845
846 #[tokio::test]
847 async fn loopback_send_chunk_error_when_output_closed() {
848 let (mut channel, handle) = LoopbackChannel::pair(8);
849 drop(handle);
850 let result = channel.send_chunk("chunk").await;
851 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
852 }
853
854 #[tokio::test]
855 async fn loopback_flush_error_when_output_closed() {
856 let (mut channel, handle) = LoopbackChannel::pair(8);
857 drop(handle);
858 let result = channel.flush_chunks().await;
859 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
860 }
861
862 #[tokio::test]
863 async fn loopback_send_status_event() {
864 let (mut channel, mut handle) = LoopbackChannel::pair(8);
865 channel.send_status("working...").await.unwrap();
866 let event = handle.output_rx.recv().await.unwrap();
867 assert!(matches!(event, LoopbackEvent::Status(s) if s == "working..."));
868 }
869
870 #[tokio::test]
871 async fn loopback_send_usage_produces_usage_event() {
872 let (mut channel, mut handle) = LoopbackChannel::pair(8);
873 channel.send_usage(100, 50, 200_000).await.unwrap();
874 let event = handle.output_rx.recv().await.unwrap();
875 match event {
876 LoopbackEvent::Usage {
877 input_tokens,
878 output_tokens,
879 context_window,
880 } => {
881 assert_eq!(input_tokens, 100);
882 assert_eq!(output_tokens, 50);
883 assert_eq!(context_window, 200_000);
884 }
885 _ => panic!("expected Usage event"),
886 }
887 }
888
889 #[tokio::test]
890 async fn loopback_send_usage_error_when_closed() {
891 let (mut channel, handle) = LoopbackChannel::pair(8);
892 drop(handle);
893 let result = channel.send_usage(1, 2, 3).await;
894 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
895 }
896
897 #[test]
898 fn plan_item_status_variants_are_distinct() {
899 assert!(!matches!(
900 PlanItemStatus::Pending,
901 PlanItemStatus::InProgress
902 ));
903 assert!(!matches!(
904 PlanItemStatus::InProgress,
905 PlanItemStatus::Completed
906 ));
907 assert!(!matches!(
908 PlanItemStatus::Completed,
909 PlanItemStatus::Pending
910 ));
911 }
912
913 #[test]
914 fn loopback_event_session_title_carries_string() {
915 let event = LoopbackEvent::SessionTitle("hello".to_owned());
916 assert!(matches!(event, LoopbackEvent::SessionTitle(s) if s == "hello"));
917 }
918
919 #[test]
920 fn loopback_event_plan_carries_entries() {
921 let entries = vec![
922 ("step 1".to_owned(), PlanItemStatus::Pending),
923 ("step 2".to_owned(), PlanItemStatus::InProgress),
924 ];
925 let event = LoopbackEvent::Plan(entries);
926 match event {
927 LoopbackEvent::Plan(e) => {
928 assert_eq!(e.len(), 2);
929 assert!(matches!(e[0].1, PlanItemStatus::Pending));
930 assert!(matches!(e[1].1, PlanItemStatus::InProgress));
931 }
932 _ => panic!("expected Plan event"),
933 }
934 }
935
936 #[tokio::test]
937 async fn loopback_send_tool_start_produces_tool_start_event() {
938 let (mut channel, mut handle) = LoopbackChannel::pair(8);
939 channel
940 .send_tool_start(ToolStartEvent {
941 tool_name: "shell".into(),
942 tool_call_id: "tc-001".into(),
943 params: Some(serde_json::json!({"command": "ls"})),
944 parent_tool_use_id: None,
945 started_at: std::time::Instant::now(),
946 speculative: false,
947 sandbox_profile: None,
948 })
949 .await
950 .unwrap();
951 let event = handle.output_rx.recv().await.unwrap();
952 match event {
953 LoopbackEvent::ToolStart(data) => {
954 assert_eq!(data.tool_name.as_str(), "shell");
955 assert_eq!(data.tool_call_id.as_str(), "tc-001");
956 assert!(data.params.is_some());
957 assert!(data.parent_tool_use_id.is_none());
958 }
959 _ => panic!("expected ToolStart event"),
960 }
961 }
962
963 #[tokio::test]
964 async fn loopback_send_tool_start_with_parent_id() {
965 let (mut channel, mut handle) = LoopbackChannel::pair(8);
966 channel
967 .send_tool_start(ToolStartEvent {
968 tool_name: "web".into(),
969 tool_call_id: "tc-002".into(),
970 params: None,
971 parent_tool_use_id: Some("parent-123".into()),
972 started_at: std::time::Instant::now(),
973 speculative: false,
974 sandbox_profile: None,
975 })
976 .await
977 .unwrap();
978 let event = handle.output_rx.recv().await.unwrap();
979 assert!(matches!(
980 event,
981 LoopbackEvent::ToolStart(ref data) if data.parent_tool_use_id.as_deref() == Some("parent-123")
982 ));
983 }
984
985 #[tokio::test]
986 async fn loopback_send_tool_start_error_when_output_closed() {
987 let (mut channel, handle) = LoopbackChannel::pair(8);
988 drop(handle);
989 let result = channel
990 .send_tool_start(ToolStartEvent {
991 tool_name: "shell".into(),
992 tool_call_id: "tc-003".into(),
993 params: None,
994 parent_tool_use_id: None,
995 started_at: std::time::Instant::now(),
996 speculative: false,
997 sandbox_profile: None,
998 })
999 .await;
1000 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
1001 }
1002
1003 #[tokio::test]
1004 async fn default_send_tool_output_formats_message() {
1005 let mut ch = StubChannel;
1006 ch.send_tool_output(ToolOutputEvent {
1008 tool_name: "bash".into(),
1009 display: "hello".into(),
1010 diff: None,
1011 filter_stats: None,
1012 kept_lines: None,
1013 locations: None,
1014 tool_call_id: "id".into(),
1015 terminal_id: None,
1016 is_error: false,
1017 parent_tool_use_id: None,
1018 raw_response: None,
1019 started_at: None,
1020 })
1021 .await
1022 .unwrap();
1023 }
1024}