use serde::Serialize;
use serde_json::Value;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::sync::mpsc;
use super::error::AcpPeerError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentRequest {
pub id: Value,
pub method: String,
pub params: Value,
}
pub struct AcpAgentPeer {
request_rx: mpsc::Receiver<AgentRequest>,
write_tx: mpsc::Sender<Vec<u8>>,
}
impl std::fmt::Debug for AcpAgentPeer {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("AcpAgentPeer")
.finish_non_exhaustive()
}
}
impl AcpAgentPeer {
pub fn from_stdio<R, W>(stdin: R, stdout: W) -> Self
where
R: tokio::io::AsyncRead + Unpin + Send + 'static,
W: tokio::io::AsyncWrite + Unpin + Send + 'static,
{
let (request_tx, request_rx) = mpsc::channel::<AgentRequest>(32);
let (write_tx, write_rx) = mpsc::channel::<Vec<u8>>(32);
tokio::spawn(spawn_reader(BufReader::new(stdin), request_tx));
tokio::spawn(spawn_writer(stdout, write_rx));
Self {
request_rx,
write_tx,
}
}
pub async fn recv_request(&mut self) -> Option<AgentRequest> {
self.request_rx.recv().await
}
pub async fn send_reply(&self, id: Value, result: Value) -> Result<(), AcpPeerError> {
let envelope = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"result": result,
});
let bytes = serde_json::to_vec(&envelope).map_err(AcpPeerError::Decode)?;
let mut framed = bytes;
framed.push(b'\n');
self.write_tx
.send(framed)
.await
.map_err(|_| AcpPeerError::Write(io_other("the writer task is gone")))?;
Ok(())
}
pub async fn send_error(
&self,
id: Value,
code: i64,
message: impl Into<String>,
) -> Result<(), AcpPeerError> {
let envelope = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"error": { "code": code, "message": message.into() },
});
let bytes = serde_json::to_vec(&envelope).map_err(AcpPeerError::Decode)?;
let mut framed = bytes;
framed.push(b'\n');
self.write_tx
.send(framed)
.await
.map_err(|_| AcpPeerError::Write(io_other("the writer task is gone")))?;
Ok(())
}
pub async fn send_notification<Params>(
&self,
method: &str,
params: Params,
) -> Result<(), AcpPeerError>
where
Params: Serialize,
{
let params_value: Value = serde_json::to_value(¶ms).map_err(AcpPeerError::Decode)?;
let envelope = serde_json::json!({
"jsonrpc": "2.0",
"method": method,
"params": params_value,
});
let bytes = serde_json::to_vec(&envelope).map_err(AcpPeerError::Decode)?;
let mut framed = bytes;
framed.push(b'\n');
self.write_tx
.send(framed)
.await
.map_err(|_| AcpPeerError::Write(io_other("the writer task is gone")))?;
Ok(())
}
}
async fn spawn_reader<R>(mut reader: BufReader<R>, request_tx: mpsc::Sender<AgentRequest>)
where
R: tokio::io::AsyncRead + Unpin + Send + 'static,
{
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) | Err(_) => return,
Ok(_) => {}
}
let trimmed = line.trim_end_matches('\n');
if trimmed.is_empty() {
continue;
}
let value: Value = match serde_json::from_str(trimmed) {
Ok(value) => value,
Err(_) => continue,
};
if value.get("id").is_none() {
continue;
}
let id = value.get("id").cloned().unwrap_or(Value::Null);
let method = value
.get("method")
.and_then(Value::as_str)
.unwrap_or("")
.to_owned();
let params = value.get("params").cloned().unwrap_or(Value::Null);
let request = AgentRequest { id, method, params };
if request_tx.send(request).await.is_err() {
return;
}
}
}
async fn spawn_writer<W>(mut stdout: W, mut write_rx: mpsc::Receiver<Vec<u8>>)
where
W: tokio::io::AsyncWrite + Unpin + Send + 'static,
{
while let Some(bytes) = write_rx.recv().await {
if stdout.write_all(&bytes).await.is_err() {
return;
}
if stdout.flush().await.is_err() {
return;
}
}
}
fn io_other(message: &str) -> std::io::Error {
std::io::Error::other(message)
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::AsyncBufReadExt;
use tokio::io::BufReader;
use tokio::io::duplex;
#[tokio::test]
async fn a_request_arrives_at_the_agent_peer() -> Result<(), AcpPeerError> {
let (mut client_end, agent_end) = duplex(4096);
let (agent_read, agent_write) = tokio::io::split(agent_end);
let mut peer = AcpAgentPeer::from_stdio(agent_read, agent_write);
let request_bytes = b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"foo\":\"bar\"}}\n";
client_end
.write_all(request_bytes)
.await
.map_err(AcpPeerError::Write)?;
let request = peer
.recv_request()
.await
.ok_or_else(|| AcpPeerError::Write(io_other("no request arrived")))?;
assert_eq!(request.id, serde_json::json!(1));
assert_eq!(request.method, "initialize");
assert_eq!(request.params, serde_json::json!({"foo": "bar"}));
drop(peer);
drop(client_end);
Ok(())
}
#[tokio::test]
async fn a_reply_written_by_the_agent_peer_round_trips() -> Result<(), AcpPeerError> {
let (client_end, agent_end) = duplex(4096);
let (agent_read, agent_write) = tokio::io::split(agent_end);
let peer = AcpAgentPeer::from_stdio(agent_read, agent_write);
peer.send_reply(serde_json::json!(7), serde_json::json!({"answer": 42}))
.await?;
let line = read_line_from(BufReader::new(client_end)).await?;
let value: Value = serde_json::from_str(&line).map_err(AcpPeerError::Decode)?;
assert_eq!(value.get("jsonrpc"), Some(&Value::String("2.0".to_owned())));
assert_eq!(value.get("id"), Some(&Value::from(7u64)));
assert_eq!(
value.get("result"),
Some(&Value::Object(
std::iter::once(("answer".to_owned(), Value::from(42u64))).collect()
))
);
drop(peer);
Ok(())
}
#[tokio::test]
async fn a_notification_written_by_the_agent_peer_omits_the_id() -> Result<(), AcpPeerError> {
let (client_end, agent_end) = duplex(4096);
let (agent_read, agent_write) = tokio::io::split(agent_end);
let peer = AcpAgentPeer::from_stdio(agent_read, agent_write);
peer.send_notification("session/update", serde_json::json!({"foo": "bar"}))
.await?;
let line = read_line_from(BufReader::new(client_end)).await?;
let value: Value = serde_json::from_str(&line).map_err(AcpPeerError::Decode)?;
assert!(
value.get("id").is_none(),
"the notification has no `id` field"
);
assert_eq!(
value.get("method"),
Some(&Value::String("session/update".to_owned()))
);
drop(peer);
Ok(())
}
#[tokio::test]
async fn an_error_reply_written_by_the_agent_peer_round_trips() -> Result<(), AcpPeerError> {
let (client_end, agent_end) = duplex(4096);
let (agent_read, agent_write) = tokio::io::split(agent_end);
let peer = AcpAgentPeer::from_stdio(agent_read, agent_write);
peer.send_error(serde_json::json!(3), -32601, "Method not found")
.await?;
let line = read_line_from(BufReader::new(client_end)).await?;
let value: Value = serde_json::from_str(&line).map_err(AcpPeerError::Decode)?;
assert_eq!(value.get("id"), Some(&Value::from(3u64)));
let error = value
.get("error")
.ok_or_else(|| AcpPeerError::Write(io_other("missing `error` field")))?;
assert_eq!(error.get("code"), Some(&Value::from(-32601)));
assert_eq!(
error.get("message"),
Some(&Value::String("Method not found".to_owned()))
);
drop(peer);
Ok(())
}
async fn read_line_from<R>(mut reader: BufReader<R>) -> Result<String, AcpPeerError>
where
R: tokio::io::AsyncRead + Unpin + Send,
{
let mut line = String::new();
reader
.read_line(&mut line)
.await
.map_err(AcpPeerError::Write)?;
Ok(line.trim_end_matches('\n').to_owned())
}
}