use core::future::Future;
use core::marker::PhantomData;
use core::pin::Pin;
use core::task::{Context, Poll};
use std::sync::Arc;
use futures_core::Stream;
use tokio::sync::{Mutex, OwnedMutexGuard, mpsc};
use crate::domain::acp::notification::SessionNotification;
use super::agent::AgentRequest;
pub struct NotificationStream<'a> {
rx: Arc<Mutex<mpsc::Receiver<SessionNotification>>>,
state: NotificationState,
_marker: PhantomData<&'a ()>,
}
enum NotificationState {
Idle,
Locking(
Pin<Box<dyn Future<Output = OwnedMutexGuard<mpsc::Receiver<SessionNotification>>> + Send>>,
),
Ready(OwnedMutexGuard<mpsc::Receiver<SessionNotification>>),
}
impl NotificationStream<'_> {
pub(crate) const fn new(rx: Arc<Mutex<mpsc::Receiver<SessionNotification>>>) -> Self {
Self {
rx,
state: NotificationState::Idle,
_marker: PhantomData,
}
}
}
impl core::fmt::Debug for NotificationStream<'_> {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("NotificationStream")
.finish_non_exhaustive()
}
}
impl core::fmt::Debug for AgentRequestStream<'_> {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("AgentRequestStream")
.finish_non_exhaustive()
}
}
impl Stream for NotificationStream<'_> {
type Item = SessionNotification;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
loop {
match self.as_mut().get_mut().step(cx) {
PollStep::Progress => {}
PollStep::Pending => return Poll::Pending,
PollStep::Ready(item) => return Poll::Ready(item),
}
}
}
}
enum PollStep<T> {
Progress,
Pending,
Ready(Option<T>),
}
impl NotificationStream<'_> {
fn step(&mut self, cx: &mut Context<'_>) -> PollStep<SessionNotification> {
match &mut self.state {
NotificationState::Idle => {
let fut = self.rx.clone().lock_owned();
self.state = NotificationState::Locking(Box::pin(fut));
PollStep::Progress
}
NotificationState::Locking(fut) => match fut.as_mut().poll(cx) {
Poll::Ready(guard) => {
self.state = NotificationState::Ready(guard);
PollStep::Progress
}
Poll::Pending => PollStep::Pending,
},
NotificationState::Ready(guard) => match guard.poll_recv(cx) {
Poll::Ready(item) => {
self.state = NotificationState::Idle;
PollStep::Ready(item)
}
Poll::Pending => PollStep::Pending,
},
}
}
}
pub struct AgentRequestStream<'a> {
rx: Arc<Mutex<mpsc::Receiver<AgentRequest>>>,
state: AgentRequestState,
_marker: PhantomData<&'a ()>,
}
enum AgentRequestState {
Idle,
Locking(Pin<Box<dyn Future<Output = OwnedMutexGuard<mpsc::Receiver<AgentRequest>>> + Send>>),
Ready(OwnedMutexGuard<mpsc::Receiver<AgentRequest>>),
}
impl AgentRequestStream<'_> {
pub(crate) const fn new(rx: Arc<Mutex<mpsc::Receiver<AgentRequest>>>) -> Self {
Self {
rx,
state: AgentRequestState::Idle,
_marker: PhantomData,
}
}
}
impl Stream for AgentRequestStream<'_> {
type Item = AgentRequest;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
loop {
match self.as_mut().get_mut().step(cx) {
PollStep::Progress => {}
PollStep::Pending => return Poll::Pending,
PollStep::Ready(item) => return Poll::Ready(item),
}
}
}
}
impl AgentRequestStream<'_> {
fn step(&mut self, cx: &mut Context<'_>) -> PollStep<AgentRequest> {
match &mut self.state {
AgentRequestState::Idle => {
let fut = self.rx.clone().lock_owned();
self.state = AgentRequestState::Locking(Box::pin(fut));
PollStep::Progress
}
AgentRequestState::Locking(fut) => match fut.as_mut().poll(cx) {
Poll::Ready(guard) => {
self.state = AgentRequestState::Ready(guard);
PollStep::Progress
}
Poll::Pending => PollStep::Pending,
},
AgentRequestState::Ready(guard) => match guard.poll_recv(cx) {
Poll::Ready(item) => {
self.state = AgentRequestState::Idle;
PollStep::Ready(item)
}
Poll::Pending => PollStep::Pending,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::acp::content::{ContentBlock, TextContent};
use crate::domain::acp::notification::SessionUpdate;
use crate::domain::acp::session::SessionId;
use futures_util::StreamExt;
use std::collections::BTreeMap;
fn text_chunk(text: &str) -> SessionUpdate {
SessionUpdate::UserMessageChunk(crate::domain::acp::notification::ContentChunk {
content: ContentBlock::Text(TextContent {
text: text.to_owned(),
annotations: None,
meta: BTreeMap::new(),
}),
message_id: None,
meta: BTreeMap::new(),
})
}
fn notification(text: &str) -> SessionNotification {
SessionNotification {
session_id: SessionId::new("test-session"),
update: text_chunk(text),
}
}
#[tokio::test]
async fn yields_values_already_in_the_channel() {
let (tx, rx) = mpsc::channel::<SessionNotification>(4);
let rx = Arc::new(Mutex::new(rx));
let send_result = tx.send(notification("hello")).await;
assert!(
send_result.is_ok(),
"the channel is open at the time of send"
);
drop(tx);
let mut stream = NotificationStream::new(rx);
let first = stream.next().await;
assert!(first.is_some(), "the queued value must surface");
let second = stream.next().await;
assert!(
second.is_none(),
"the channel is closed after the sender drops"
);
}
#[tokio::test]
async fn returns_none_when_sender_already_dropped() {
let (tx, rx) = mpsc::channel::<SessionNotification>(4);
drop(tx);
let rx = Arc::new(Mutex::new(rx));
let mut stream = NotificationStream::new(rx);
assert!(stream.next().await.is_none());
}
#[tokio::test]
async fn a_second_stream_does_not_deadlock_against_itself() {
let (tx, rx) = mpsc::channel::<SessionNotification>(4);
let rx = Arc::new(Mutex::new(rx));
let send_result = tx.send(notification("hello")).await;
assert!(
send_result.is_ok(),
"the channel is open at the time of send"
);
drop(tx);
let mut first = NotificationStream::new(Arc::clone(&rx));
let mut second = NotificationStream::new(Arc::clone(&rx));
assert!(first.next().await.is_some());
assert!(second.next().await.is_none());
}
#[tokio::test]
async fn agent_request_stream_round_trip() {
let (tx, rx) = mpsc::channel::<AgentRequest>(4);
let rx = Arc::new(Mutex::new(rx));
let send_result = tx
.send(AgentRequest {
id: serde_json::json!(1),
method: "fs/read_text_file".to_owned(),
params: serde_json::json!({}),
})
.await;
assert!(
send_result.is_ok(),
"the channel is open at the time of send"
);
drop(tx);
let mut stream = AgentRequestStream::new(rx);
let first = stream.next().await;
let method = match first {
Some(request) => request.method,
None => String::new(),
};
assert_eq!(method, "fs/read_text_file");
assert!(stream.next().await.is_none());
}
}