molo_agent/message_channel/
cli.rs1use super::{ChannelError, MessageChannel};
4use std::fmt;
5use tokio::io::{
6 AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader,
7};
8use tokio::sync::Mutex;
9
10const MAX_LINE: usize = 1 << 20;
13
14pub struct CliMessageChannel {
56 io: Mutex<CliIo>,
57}
58
59impl fmt::Debug for CliMessageChannel {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.debug_struct("CliMessageChannel")
64 .field("io", &"Box<dyn AsyncBufRead> + Box<dyn AsyncWrite>")
65 .finish()
66 }
67}
68
69struct CliIo {
70 reader: Box<dyn AsyncBufRead + Unpin + Send>,
71 writer: Box<dyn AsyncWrite + Unpin + Send>,
72}
73
74impl CliMessageChannel {
75 pub fn new() -> Self {
77 Self::with_io(
78 Box::new(BufReader::new(tokio::io::stdin())),
79 Box::new(tokio::io::stdout()),
80 )
81 }
82
83 pub fn with_io(
89 reader: Box<dyn AsyncBufRead + Unpin + Send>,
90 writer: Box<dyn AsyncWrite + Unpin + Send>,
91 ) -> Self {
92 Self {
93 io: Mutex::new(CliIo { reader, writer }),
94 }
95 }
96}
97
98impl Default for CliMessageChannel {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104async fn write_line(
106 writer: &mut (dyn AsyncWrite + Unpin + Send),
107 message: &str,
108) -> Result<(), ChannelError> {
109 writer
110 .write_all(message.as_bytes())
111 .await
112 .map_err(ChannelError::from)?;
113 writer.write_all(b"\n").await.map_err(ChannelError::from)?;
114 writer.flush().await.map_err(ChannelError::from)
115}
116
117#[async_trait::async_trait]
118impl MessageChannel for CliMessageChannel {
119 async fn ask(&self, message: &str) -> Result<String, ChannelError> {
120 let mut io = self.io.lock().await;
121 write_line(io.writer.as_mut(), message).await?;
122 let mut line = String::new();
125 let read = io
126 .reader
127 .as_mut()
128 .take((MAX_LINE + 1) as u64)
129 .read_line(&mut line)
130 .await
131 .map_err(ChannelError::from)?;
132 if line.len() > MAX_LINE {
133 return Err(ChannelError::Io(format!(
134 "input line exceeds {MAX_LINE} bytes"
135 )));
136 }
137 if read == 0 {
138 return Err(ChannelError::Closed);
139 }
140 Ok(line.trim().to_string())
141 }
142
143 async fn notify(&self, message: &str) -> Result<(), ChannelError> {
144 let mut io = self.io.lock().await;
145 write_line(io.writer.as_mut(), message).await
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use std::pin::Pin;
153 use std::sync::{Arc, Mutex as StdMutex};
154 use std::task::{Context, Poll};
155
156 #[derive(Clone, Default)]
158 struct SharedBuf(Arc<StdMutex<Vec<u8>>>);
159
160 impl AsyncWrite for SharedBuf {
161 fn poll_write(
162 self: Pin<&mut Self>,
163 _cx: &mut Context<'_>,
164 buf: &[u8],
165 ) -> Poll<std::io::Result<usize>> {
166 self.0
167 .lock()
168 .expect("internal lock poisoned")
169 .extend_from_slice(buf);
170 Poll::Ready(Ok(buf.len()))
171 }
172
173 fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
174 Poll::Ready(Ok(()))
175 }
176
177 fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
178 Poll::Ready(Ok(()))
179 }
180 }
181
182 async fn channel(seed: &str) -> (CliMessageChannel, SharedBuf) {
184 let buf = SharedBuf(Arc::new(StdMutex::new(Vec::new())));
185 let (mut seed_tx, rx) = tokio::io::duplex(64);
186 seed_tx.write_all(seed.as_bytes()).await.unwrap();
187 let channel =
188 CliMessageChannel::with_io(Box::new(BufReader::new(rx)), Box::new(buf.clone()));
189 (channel, buf)
190 }
191
192 #[tokio::test]
193 async fn ask_reads_line_and_trims() {
194 let (channel, buf) = channel(" reply text \n").await;
195 assert_eq!(channel.ask("question").await.unwrap(), "reply text");
196 let out = String::from_utf8(buf.0.lock().unwrap().clone()).unwrap();
197 assert_eq!(out, "question\n");
198 }
199
200 #[tokio::test]
201 async fn ask_closed_input_returns_closed() {
202 let channel = CliMessageChannel::with_io(
203 Box::new(BufReader::new(tokio::io::empty())),
204 Box::new(tokio::io::sink()),
205 );
206 assert!(matches!(
207 channel.ask("question").await,
208 Err(ChannelError::Closed)
209 ));
210 }
211
212 #[tokio::test]
213 async fn notify_writes_message() {
214 let (channel, buf) = channel("").await;
215 channel.notify("notice").await.unwrap();
216 let out = String::from_utf8(buf.0.lock().unwrap().clone()).unwrap();
217 assert_eq!(out, "notice\n");
218 }
219
220 #[tokio::test]
221 async fn concurrent_asks_serialized() {
222 let (channel, buf) = channel("r1\nr2\n").await;
223 let (a, b) = tokio::join!(channel.ask("m1"), channel.ask("m2"));
226 let mut replies = vec![a.unwrap(), b.unwrap()];
227 replies.sort();
228 assert_eq!(replies, vec!["r1", "r2"]);
229 let out = String::from_utf8(buf.0.lock().unwrap().clone()).unwrap();
231 let lines: Vec<&str> = out.lines().collect();
232 assert_eq!(lines.len(), 2);
233 assert!(lines.contains(&"m1") && lines.contains(&"m2"));
234 }
235}