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 {
68 Self::Other(e.to_string())
69 }
70}
71
72#[derive(Debug)]
74pub struct ToolStartEvent<'a> {
75 pub tool_name: &'a str,
76 pub tool_call_id: &'a str,
77 pub params: Option<serde_json::Value>,
78 pub parent_tool_use_id: Option<String>,
79}
80
81#[derive(Debug)]
83pub struct ToolOutputEvent<'a> {
84 pub tool_name: &'a str,
85 pub body: &'a str,
86 pub diff: Option<crate::DiffData>,
87 pub filter_stats: Option<String>,
88 pub kept_lines: Option<Vec<usize>>,
89 pub locations: Option<Vec<String>>,
90 pub tool_call_id: &'a str,
91 pub is_error: bool,
92 pub parent_tool_use_id: Option<String>,
93 pub raw_response: Option<serde_json::Value>,
94 pub started_at: Option<std::time::Instant>,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum AttachmentKind {
100 Audio,
101 Image,
102 Video,
103 File,
104}
105
106#[derive(Debug, Clone)]
108pub struct Attachment {
109 pub kind: AttachmentKind,
110 pub data: Vec<u8>,
111 pub filename: Option<String>,
112}
113
114#[derive(Debug, Clone)]
116pub struct ChannelMessage {
117 pub text: String,
118 pub attachments: Vec<Attachment>,
119}
120
121pub trait Channel: Send {
123 fn recv(&mut self)
129 -> impl Future<Output = Result<Option<ChannelMessage>, ChannelError>> + Send;
130
131 fn try_recv(&mut self) -> Option<ChannelMessage> {
133 None
134 }
135
136 fn supports_exit(&self) -> bool {
141 true
142 }
143
144 fn send(&mut self, text: &str) -> impl Future<Output = Result<(), ChannelError>> + Send;
150
151 fn send_chunk(&mut self, chunk: &str) -> impl Future<Output = Result<(), ChannelError>> + Send;
157
158 fn flush_chunks(&mut self) -> impl Future<Output = Result<(), ChannelError>> + Send;
164
165 fn send_typing(&mut self) -> impl Future<Output = Result<(), ChannelError>> + Send {
171 async { Ok(()) }
172 }
173
174 fn send_status(
180 &mut self,
181 _text: &str,
182 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
183 async { Ok(()) }
184 }
185
186 fn send_thinking_chunk(
192 &mut self,
193 _chunk: &str,
194 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
195 async { Ok(()) }
196 }
197
198 fn send_queue_count(
204 &mut self,
205 _count: usize,
206 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
207 async { Ok(()) }
208 }
209
210 fn send_usage(
216 &mut self,
217 _input_tokens: u64,
218 _output_tokens: u64,
219 _context_window: u64,
220 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
221 async { Ok(()) }
222 }
223
224 fn send_diff(
230 &mut self,
231 _diff: crate::DiffData,
232 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
233 async { Ok(()) }
234 }
235
236 fn send_tool_start(
246 &mut self,
247 _event: ToolStartEvent<'_>,
248 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
249 async { Ok(()) }
250 }
251
252 fn send_tool_output(
263 &mut self,
264 event: ToolOutputEvent<'_>,
265 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
266 let formatted = crate::agent::format_tool_output(event.tool_name, event.body);
267 async move { self.send(&formatted).await }
268 }
269
270 fn confirm(
277 &mut self,
278 _prompt: &str,
279 ) -> impl Future<Output = Result<bool, ChannelError>> + Send {
280 async { Ok(true) }
281 }
282
283 fn elicit(
292 &mut self,
293 _request: ElicitationRequest,
294 ) -> impl Future<Output = Result<ElicitationResponse, ChannelError>> + Send {
295 async { Ok(ElicitationResponse::Declined) }
296 }
297
298 fn send_stop_hint(
307 &mut self,
308 _hint: StopHint,
309 ) -> impl Future<Output = Result<(), ChannelError>> + Send {
310 async { Ok(()) }
311 }
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub enum StopHint {
321 MaxTokens,
323 MaxTurnRequests,
325}
326
327#[derive(Debug, Clone)]
329pub struct ToolStartData {
330 pub tool_name: String,
331 pub tool_call_id: String,
332 pub params: Option<serde_json::Value>,
334 pub parent_tool_use_id: Option<String>,
336 pub started_at: std::time::Instant,
338}
339
340#[derive(Debug, Clone)]
342pub struct ToolOutputData {
343 pub tool_name: String,
344 pub display: String,
345 pub diff: Option<crate::DiffData>,
346 pub filter_stats: Option<String>,
347 pub kept_lines: Option<Vec<usize>>,
348 pub locations: Option<Vec<String>>,
349 pub tool_call_id: String,
350 pub is_error: bool,
351 pub terminal_id: Option<String>,
353 pub parent_tool_use_id: Option<String>,
355 pub raw_response: Option<serde_json::Value>,
357 pub started_at: Option<std::time::Instant>,
359}
360
361impl From<ToolStartEvent<'_>> for ToolStartData {
362 fn from(e: ToolStartEvent<'_>) -> Self {
363 Self {
364 tool_name: e.tool_name.to_owned(),
365 tool_call_id: e.tool_call_id.to_owned(),
366 params: e.params,
367 parent_tool_use_id: e.parent_tool_use_id,
368 started_at: std::time::Instant::now(),
369 }
370 }
371}
372
373impl From<ToolOutputEvent<'_>> for ToolOutputData {
374 fn from(e: ToolOutputEvent<'_>) -> Self {
375 Self {
376 tool_name: e.tool_name.to_owned(),
377 display: e.body.to_owned(),
378 diff: e.diff,
379 filter_stats: e.filter_stats,
380 kept_lines: e.kept_lines,
381 locations: e.locations,
382 tool_call_id: e.tool_call_id.to_owned(),
383 is_error: e.is_error,
384 terminal_id: None,
385 parent_tool_use_id: e.parent_tool_use_id,
386 raw_response: e.raw_response,
387 started_at: e.started_at,
388 }
389 }
390}
391
392#[derive(Debug, Clone)]
394pub enum LoopbackEvent {
395 Chunk(String),
396 Flush,
397 FullMessage(String),
398 Status(String),
399 ToolStart(Box<ToolStartData>),
401 ToolOutput(Box<ToolOutputData>),
402 Usage {
404 input_tokens: u64,
405 output_tokens: u64,
406 context_window: u64,
407 },
408 SessionTitle(String),
410 Plan(Vec<(String, PlanItemStatus)>),
412 ThinkingChunk(String),
414 Stop(StopHint),
418}
419
420#[derive(Debug, Clone)]
422pub enum PlanItemStatus {
423 Pending,
424 InProgress,
425 Completed,
426}
427
428pub struct LoopbackHandle {
430 pub input_tx: tokio::sync::mpsc::Sender<ChannelMessage>,
431 pub output_rx: tokio::sync::mpsc::Receiver<LoopbackEvent>,
432 pub cancel_signal: std::sync::Arc<tokio::sync::Notify>,
434}
435
436pub struct LoopbackChannel {
438 input_rx: tokio::sync::mpsc::Receiver<ChannelMessage>,
439 output_tx: tokio::sync::mpsc::Sender<LoopbackEvent>,
440}
441
442impl LoopbackChannel {
443 #[must_use]
445 pub fn pair(buffer: usize) -> (Self, LoopbackHandle) {
446 let (input_tx, input_rx) = tokio::sync::mpsc::channel(buffer);
447 let (output_tx, output_rx) = tokio::sync::mpsc::channel(buffer);
448 let cancel_signal = std::sync::Arc::new(tokio::sync::Notify::new());
449 (
450 Self {
451 input_rx,
452 output_tx,
453 },
454 LoopbackHandle {
455 input_tx,
456 output_rx,
457 cancel_signal,
458 },
459 )
460 }
461}
462
463impl Channel for LoopbackChannel {
464 fn supports_exit(&self) -> bool {
465 false
466 }
467
468 async fn recv(&mut self) -> Result<Option<ChannelMessage>, ChannelError> {
469 Ok(self.input_rx.recv().await)
470 }
471
472 async fn send(&mut self, text: &str) -> Result<(), ChannelError> {
473 self.output_tx
474 .send(LoopbackEvent::FullMessage(text.to_owned()))
475 .await
476 .map_err(|_| ChannelError::ChannelClosed)
477 }
478
479 async fn send_chunk(&mut self, chunk: &str) -> Result<(), ChannelError> {
480 self.output_tx
481 .send(LoopbackEvent::Chunk(chunk.to_owned()))
482 .await
483 .map_err(|_| ChannelError::ChannelClosed)
484 }
485
486 async fn flush_chunks(&mut self) -> Result<(), ChannelError> {
487 self.output_tx
488 .send(LoopbackEvent::Flush)
489 .await
490 .map_err(|_| ChannelError::ChannelClosed)
491 }
492
493 async fn send_status(&mut self, text: &str) -> Result<(), ChannelError> {
494 self.output_tx
495 .send(LoopbackEvent::Status(text.to_owned()))
496 .await
497 .map_err(|_| ChannelError::ChannelClosed)
498 }
499
500 async fn send_thinking_chunk(&mut self, chunk: &str) -> Result<(), ChannelError> {
501 self.output_tx
502 .send(LoopbackEvent::ThinkingChunk(chunk.to_owned()))
503 .await
504 .map_err(|_| ChannelError::ChannelClosed)
505 }
506
507 async fn send_tool_start(&mut self, event: ToolStartEvent<'_>) -> Result<(), ChannelError> {
508 self.output_tx
509 .send(LoopbackEvent::ToolStart(Box::new(event.into())))
510 .await
511 .map_err(|_| ChannelError::ChannelClosed)
512 }
513
514 async fn send_tool_output(&mut self, event: ToolOutputEvent<'_>) -> Result<(), ChannelError> {
515 self.output_tx
516 .send(LoopbackEvent::ToolOutput(Box::new(event.into())))
517 .await
518 .map_err(|_| ChannelError::ChannelClosed)
519 }
520
521 async fn confirm(&mut self, _prompt: &str) -> Result<bool, ChannelError> {
522 Ok(true)
523 }
524
525 async fn send_stop_hint(&mut self, hint: StopHint) -> Result<(), ChannelError> {
526 self.output_tx
527 .send(LoopbackEvent::Stop(hint))
528 .await
529 .map_err(|_| ChannelError::ChannelClosed)
530 }
531
532 async fn send_usage(
533 &mut self,
534 input_tokens: u64,
535 output_tokens: u64,
536 context_window: u64,
537 ) -> Result<(), ChannelError> {
538 self.output_tx
539 .send(LoopbackEvent::Usage {
540 input_tokens,
541 output_tokens,
542 context_window,
543 })
544 .await
545 .map_err(|_| ChannelError::ChannelClosed)
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552
553 #[test]
554 fn channel_message_creation() {
555 let msg = ChannelMessage {
556 text: "hello".to_string(),
557 attachments: vec![],
558 };
559 assert_eq!(msg.text, "hello");
560 assert!(msg.attachments.is_empty());
561 }
562
563 struct StubChannel;
564
565 impl Channel for StubChannel {
566 async fn recv(&mut self) -> Result<Option<ChannelMessage>, ChannelError> {
567 Ok(None)
568 }
569
570 async fn send(&mut self, _text: &str) -> Result<(), ChannelError> {
571 Ok(())
572 }
573
574 async fn send_chunk(&mut self, _chunk: &str) -> Result<(), ChannelError> {
575 Ok(())
576 }
577
578 async fn flush_chunks(&mut self) -> Result<(), ChannelError> {
579 Ok(())
580 }
581 }
582
583 #[tokio::test]
584 async fn send_chunk_default_is_noop() {
585 let mut ch = StubChannel;
586 ch.send_chunk("partial").await.unwrap();
587 }
588
589 #[tokio::test]
590 async fn flush_chunks_default_is_noop() {
591 let mut ch = StubChannel;
592 ch.flush_chunks().await.unwrap();
593 }
594
595 #[tokio::test]
596 async fn stub_channel_confirm_auto_approves() {
597 let mut ch = StubChannel;
598 let result = ch.confirm("Delete everything?").await.unwrap();
599 assert!(result);
600 }
601
602 #[tokio::test]
603 async fn stub_channel_send_typing_default() {
604 let mut ch = StubChannel;
605 ch.send_typing().await.unwrap();
606 }
607
608 #[tokio::test]
609 async fn stub_channel_recv_returns_none() {
610 let mut ch = StubChannel;
611 let msg = ch.recv().await.unwrap();
612 assert!(msg.is_none());
613 }
614
615 #[tokio::test]
616 async fn stub_channel_send_ok() {
617 let mut ch = StubChannel;
618 ch.send("hello").await.unwrap();
619 }
620
621 #[test]
622 fn channel_message_clone() {
623 let msg = ChannelMessage {
624 text: "test".to_string(),
625 attachments: vec![],
626 };
627 let cloned = msg.clone();
628 assert_eq!(cloned.text, "test");
629 }
630
631 #[test]
632 fn channel_message_debug() {
633 let msg = ChannelMessage {
634 text: "debug".to_string(),
635 attachments: vec![],
636 };
637 let debug = format!("{msg:?}");
638 assert!(debug.contains("debug"));
639 }
640
641 #[test]
642 fn attachment_kind_equality() {
643 assert_eq!(AttachmentKind::Audio, AttachmentKind::Audio);
644 assert_ne!(AttachmentKind::Audio, AttachmentKind::Image);
645 }
646
647 #[test]
648 fn attachment_construction() {
649 let a = Attachment {
650 kind: AttachmentKind::Audio,
651 data: vec![0, 1, 2],
652 filename: Some("test.wav".into()),
653 };
654 assert_eq!(a.kind, AttachmentKind::Audio);
655 assert_eq!(a.data.len(), 3);
656 assert_eq!(a.filename.as_deref(), Some("test.wav"));
657 }
658
659 #[test]
660 fn channel_message_with_attachments() {
661 let msg = ChannelMessage {
662 text: String::new(),
663 attachments: vec![Attachment {
664 kind: AttachmentKind::Audio,
665 data: vec![42],
666 filename: None,
667 }],
668 };
669 assert_eq!(msg.attachments.len(), 1);
670 assert_eq!(msg.attachments[0].kind, AttachmentKind::Audio);
671 }
672
673 #[test]
674 fn stub_channel_try_recv_returns_none() {
675 let mut ch = StubChannel;
676 assert!(ch.try_recv().is_none());
677 }
678
679 #[tokio::test]
680 async fn stub_channel_send_queue_count_noop() {
681 let mut ch = StubChannel;
682 ch.send_queue_count(5).await.unwrap();
683 }
684
685 #[test]
688 fn loopback_pair_returns_linked_handles() {
689 let (channel, handle) = LoopbackChannel::pair(8);
690 drop(channel);
692 drop(handle);
693 }
694
695 #[tokio::test]
696 async fn loopback_cancel_signal_can_be_notified_and_awaited() {
697 let (_channel, handle) = LoopbackChannel::pair(8);
698 let signal = std::sync::Arc::clone(&handle.cancel_signal);
699 let notified = signal.notified();
701 handle.cancel_signal.notify_one();
702 notified.await; }
704
705 #[tokio::test]
706 async fn loopback_cancel_signal_shared_across_clones() {
707 let (_channel, handle) = LoopbackChannel::pair(8);
708 let signal_a = std::sync::Arc::clone(&handle.cancel_signal);
709 let signal_b = std::sync::Arc::clone(&handle.cancel_signal);
710 let notified = signal_b.notified();
711 signal_a.notify_one();
712 notified.await;
713 }
714
715 #[tokio::test]
716 async fn loopback_send_recv_round_trip() {
717 let (mut channel, handle) = LoopbackChannel::pair(8);
718 handle
719 .input_tx
720 .send(ChannelMessage {
721 text: "hello".to_owned(),
722 attachments: vec![],
723 })
724 .await
725 .unwrap();
726 let msg = channel.recv().await.unwrap().unwrap();
727 assert_eq!(msg.text, "hello");
728 }
729
730 #[tokio::test]
731 async fn loopback_recv_returns_none_when_handle_dropped() {
732 let (mut channel, handle) = LoopbackChannel::pair(8);
733 drop(handle);
734 let result = channel.recv().await.unwrap();
735 assert!(result.is_none());
736 }
737
738 #[tokio::test]
739 async fn loopback_send_produces_full_message_event() {
740 let (mut channel, mut handle) = LoopbackChannel::pair(8);
741 channel.send("world").await.unwrap();
742 let event = handle.output_rx.recv().await.unwrap();
743 assert!(matches!(event, LoopbackEvent::FullMessage(t) if t == "world"));
744 }
745
746 #[tokio::test]
747 async fn loopback_send_chunk_then_flush() {
748 let (mut channel, mut handle) = LoopbackChannel::pair(8);
749 channel.send_chunk("part1").await.unwrap();
750 channel.flush_chunks().await.unwrap();
751 let ev1 = handle.output_rx.recv().await.unwrap();
752 let ev2 = handle.output_rx.recv().await.unwrap();
753 assert!(matches!(ev1, LoopbackEvent::Chunk(t) if t == "part1"));
754 assert!(matches!(ev2, LoopbackEvent::Flush));
755 }
756
757 #[tokio::test]
758 async fn loopback_send_tool_output() {
759 let (mut channel, mut handle) = LoopbackChannel::pair(8);
760 channel
761 .send_tool_output(ToolOutputEvent {
762 tool_name: "bash",
763 body: "exit 0",
764 diff: None,
765 filter_stats: None,
766 kept_lines: None,
767 locations: None,
768 tool_call_id: "",
769 is_error: false,
770 parent_tool_use_id: None,
771 raw_response: None,
772 started_at: None,
773 })
774 .await
775 .unwrap();
776 let event = handle.output_rx.recv().await.unwrap();
777 match event {
778 LoopbackEvent::ToolOutput(data) => {
779 assert_eq!(data.tool_name, "bash");
780 assert_eq!(data.display, "exit 0");
781 assert!(data.diff.is_none());
782 assert!(data.filter_stats.is_none());
783 assert!(data.kept_lines.is_none());
784 assert!(data.locations.is_none());
785 assert_eq!(data.tool_call_id, "");
786 assert!(!data.is_error);
787 assert!(data.terminal_id.is_none());
788 assert!(data.parent_tool_use_id.is_none());
789 assert!(data.raw_response.is_none());
790 }
791 _ => panic!("expected ToolOutput event"),
792 }
793 }
794
795 #[tokio::test]
796 async fn loopback_confirm_auto_approves() {
797 let (mut channel, _handle) = LoopbackChannel::pair(8);
798 let result = channel.confirm("are you sure?").await.unwrap();
799 assert!(result);
800 }
801
802 #[tokio::test]
803 async fn loopback_send_error_when_output_closed() {
804 let (mut channel, handle) = LoopbackChannel::pair(8);
805 drop(handle);
807 let result = channel.send("too late").await;
808 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
809 }
810
811 #[tokio::test]
812 async fn loopback_send_chunk_error_when_output_closed() {
813 let (mut channel, handle) = LoopbackChannel::pair(8);
814 drop(handle);
815 let result = channel.send_chunk("chunk").await;
816 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
817 }
818
819 #[tokio::test]
820 async fn loopback_flush_error_when_output_closed() {
821 let (mut channel, handle) = LoopbackChannel::pair(8);
822 drop(handle);
823 let result = channel.flush_chunks().await;
824 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
825 }
826
827 #[tokio::test]
828 async fn loopback_send_status_event() {
829 let (mut channel, mut handle) = LoopbackChannel::pair(8);
830 channel.send_status("working...").await.unwrap();
831 let event = handle.output_rx.recv().await.unwrap();
832 assert!(matches!(event, LoopbackEvent::Status(s) if s == "working..."));
833 }
834
835 #[tokio::test]
836 async fn loopback_send_usage_produces_usage_event() {
837 let (mut channel, mut handle) = LoopbackChannel::pair(8);
838 channel.send_usage(100, 50, 200_000).await.unwrap();
839 let event = handle.output_rx.recv().await.unwrap();
840 match event {
841 LoopbackEvent::Usage {
842 input_tokens,
843 output_tokens,
844 context_window,
845 } => {
846 assert_eq!(input_tokens, 100);
847 assert_eq!(output_tokens, 50);
848 assert_eq!(context_window, 200_000);
849 }
850 _ => panic!("expected Usage event"),
851 }
852 }
853
854 #[tokio::test]
855 async fn loopback_send_usage_error_when_closed() {
856 let (mut channel, handle) = LoopbackChannel::pair(8);
857 drop(handle);
858 let result = channel.send_usage(1, 2, 3).await;
859 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
860 }
861
862 #[test]
863 fn plan_item_status_variants_are_distinct() {
864 assert!(!matches!(
865 PlanItemStatus::Pending,
866 PlanItemStatus::InProgress
867 ));
868 assert!(!matches!(
869 PlanItemStatus::InProgress,
870 PlanItemStatus::Completed
871 ));
872 assert!(!matches!(
873 PlanItemStatus::Completed,
874 PlanItemStatus::Pending
875 ));
876 }
877
878 #[test]
879 fn loopback_event_session_title_carries_string() {
880 let event = LoopbackEvent::SessionTitle("hello".to_owned());
881 assert!(matches!(event, LoopbackEvent::SessionTitle(s) if s == "hello"));
882 }
883
884 #[test]
885 fn loopback_event_plan_carries_entries() {
886 let entries = vec![
887 ("step 1".to_owned(), PlanItemStatus::Pending),
888 ("step 2".to_owned(), PlanItemStatus::InProgress),
889 ];
890 let event = LoopbackEvent::Plan(entries);
891 match event {
892 LoopbackEvent::Plan(e) => {
893 assert_eq!(e.len(), 2);
894 assert!(matches!(e[0].1, PlanItemStatus::Pending));
895 assert!(matches!(e[1].1, PlanItemStatus::InProgress));
896 }
897 _ => panic!("expected Plan event"),
898 }
899 }
900
901 #[tokio::test]
902 async fn loopback_send_tool_start_produces_tool_start_event() {
903 let (mut channel, mut handle) = LoopbackChannel::pair(8);
904 channel
905 .send_tool_start(ToolStartEvent {
906 tool_name: "shell",
907 tool_call_id: "tc-001",
908 params: Some(serde_json::json!({"command": "ls"})),
909 parent_tool_use_id: None,
910 })
911 .await
912 .unwrap();
913 let event = handle.output_rx.recv().await.unwrap();
914 match event {
915 LoopbackEvent::ToolStart(data) => {
916 assert_eq!(data.tool_name, "shell");
917 assert_eq!(data.tool_call_id, "tc-001");
918 assert!(data.params.is_some());
919 assert!(data.parent_tool_use_id.is_none());
920 }
921 _ => panic!("expected ToolStart event"),
922 }
923 }
924
925 #[tokio::test]
926 async fn loopback_send_tool_start_with_parent_id() {
927 let (mut channel, mut handle) = LoopbackChannel::pair(8);
928 channel
929 .send_tool_start(ToolStartEvent {
930 tool_name: "web",
931 tool_call_id: "tc-002",
932 params: None,
933 parent_tool_use_id: Some("parent-123".into()),
934 })
935 .await
936 .unwrap();
937 let event = handle.output_rx.recv().await.unwrap();
938 assert!(matches!(
939 event,
940 LoopbackEvent::ToolStart(ref data) if data.parent_tool_use_id.as_deref() == Some("parent-123")
941 ));
942 }
943
944 #[tokio::test]
945 async fn loopback_send_tool_start_error_when_output_closed() {
946 let (mut channel, handle) = LoopbackChannel::pair(8);
947 drop(handle);
948 let result = channel
949 .send_tool_start(ToolStartEvent {
950 tool_name: "shell",
951 tool_call_id: "tc-003",
952 params: None,
953 parent_tool_use_id: None,
954 })
955 .await;
956 assert!(matches!(result, Err(ChannelError::ChannelClosed)));
957 }
958
959 #[tokio::test]
960 async fn default_send_tool_output_formats_message() {
961 let mut ch = StubChannel;
962 ch.send_tool_output(ToolOutputEvent {
964 tool_name: "bash",
965 body: "hello",
966 diff: None,
967 filter_stats: None,
968 kept_lines: None,
969 locations: None,
970 tool_call_id: "id",
971 is_error: false,
972 parent_tool_use_id: None,
973 raw_response: None,
974 started_at: None,
975 })
976 .await
977 .unwrap();
978 }
979}