use std::collections::VecDeque;
use std::fmt;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use futures::Stream;
use tokio::sync::mpsc;
use turnframe_core::replay::TurnPhase;
use turnframe_core::response::{AssistantTurn, ResponseBlock};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum TurnEvent {
Phase(TurnPhase),
Step(Box<turnframe_understand::Step>),
StepSaid {
step: Box<turnframe_understand::Step>,
text: String,
},
Block(Box<ResponseBlock>),
Completed(Box<AssistantTurn>),
Failed {
code: String,
},
}
impl TurnEvent {
#[must_use]
pub fn block(&self) -> Option<&ResponseBlock> {
match self {
Self::Block(block) => Some(block),
_ => None,
}
}
#[must_use]
pub const fn is_terminal(&self) -> bool {
matches!(self, Self::Completed(_) | Self::Failed { .. })
}
}
#[must_use]
pub const fn publishes_outcomes(phase: TurnPhase) -> bool {
matches!(
phase,
TurnPhase::Committed | TurnPhase::Composed | TurnPhase::Delivered
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PublicationGate {
phase: TurnPhase,
}
impl PublicationGate {
#[must_use]
pub const fn new() -> Self {
Self {
phase: TurnPhase::Received,
}
}
#[must_use]
pub const fn at(phase: TurnPhase) -> Self {
Self { phase }
}
#[must_use]
pub const fn phase(&self) -> TurnPhase {
self.phase
}
pub const fn advance(&mut self, phase: TurnPhase) {
self.phase = phase;
}
#[must_use]
pub fn admits(&self, block: &ResponseBlock) -> bool {
match block {
ResponseBlock::Notice(_) | ResponseBlock::Interaction(_) => true,
ResponseBlock::Receipt(_)
| ResponseBlock::Answer(_)
| ResponseBlock::Transition(_)
| ResponseBlock::Artifact(_) => publishes_outcomes(self.phase),
_ => publishes_outcomes(self.phase),
}
}
}
impl Default for PublicationGate {
fn default() -> Self {
Self::new()
}
}
pub trait TurnSink: Send + Sync {
fn emit(&self, event: TurnEvent);
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NullSink;
impl TurnSink for NullSink {
fn emit(&self, _event: TurnEvent) {}
}
#[derive(Debug, Default)]
pub struct RecordingSink {
events: Mutex<Vec<TurnEvent>>,
}
impl RecordingSink {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn events(&self) -> Vec<TurnEvent> {
self.events
.lock()
.map(|events| events.clone())
.unwrap_or_default()
}
#[must_use]
pub fn blocks(&self) -> Vec<ResponseBlock> {
self.events()
.into_iter()
.filter_map(|event| match event {
TurnEvent::Block(block) => Some(*block),
_ => None,
})
.collect()
}
}
impl TurnSink for RecordingSink {
fn emit(&self, event: TurnEvent) {
if let Ok(mut events) = self.events.lock() {
events.push(event);
}
}
}
#[derive(Debug, Clone)]
pub struct ChannelSink {
sender: mpsc::UnboundedSender<TurnEvent>,
}
impl TurnSink for ChannelSink {
fn emit(&self, event: TurnEvent) {
let _ = self.sender.send(event);
}
}
pub struct TurnPublisher {
sink: Arc<dyn TurnSink>,
gate: Mutex<PublicationGate>,
live: bool,
}
impl turnframe_understand::StepSink for TurnPublisher {
fn step(&self, step: turnframe_understand::Step) {
self.publish_step(step);
}
}
impl fmt::Debug for TurnPublisher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TurnPublisher")
.field("phase", &self.phase())
.finish_non_exhaustive()
}
}
impl TurnPublisher {
#[must_use]
pub fn new(sink: Arc<dyn TurnSink>) -> Self {
Self {
sink,
gate: Mutex::new(PublicationGate::new()),
live: true,
}
}
#[must_use]
pub fn null() -> Self {
Self {
sink: Arc::new(NullSink),
gate: Mutex::new(PublicationGate::new()),
live: false,
}
}
#[must_use]
pub const fn is_live(&self) -> bool {
self.live
}
#[must_use]
pub fn phase(&self) -> TurnPhase {
self.gate
.lock()
.map_or(TurnPhase::Received, |gate| gate.phase())
}
pub fn phase_reached(&self, phase: TurnPhase) {
if let Ok(mut gate) = self.gate.lock() {
gate.advance(phase);
}
self.sink.emit(TurnEvent::Phase(phase));
}
pub fn publish_step(&self, step: turnframe_understand::Step) {
self.sink.emit(TurnEvent::Step(Box::new(step)));
}
pub fn step_said(&self, step: turnframe_understand::Step, text: String) {
self.sink.emit(TurnEvent::StepSaid {
step: Box::new(step),
text,
});
}
pub fn block(&self, block: &ResponseBlock) -> bool {
let admitted = self.gate.lock().is_ok_and(|gate| gate.admits(block));
if admitted {
self.sink.emit(TurnEvent::Block(Box::new(block.clone())));
}
admitted
}
pub fn blocks(&self, turn: &AssistantTurn) {
for block in &turn.blocks {
self.block(block);
}
}
pub fn completed(&self, turn: &AssistantTurn) {
self.phase_reached(TurnPhase::Delivered);
self.sink.emit(TurnEvent::Completed(Box::new(turn.clone())));
}
pub fn failed(&self, code: impl Into<String>) {
if let Ok(mut gate) = self.gate.lock() {
gate.advance(TurnPhase::Failed);
}
self.sink.emit(TurnEvent::Failed { code: code.into() });
}
}
pub struct TurnStream {
buffered: VecDeque<TurnEvent>,
receiver: Option<mpsc::UnboundedReceiver<TurnEvent>>,
}
impl fmt::Debug for TurnStream {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TurnStream")
.field("buffered", &self.buffered.len())
.field("live", &self.receiver.is_some())
.finish()
}
}
impl TurnStream {
#[must_use]
pub fn channel() -> (Self, ChannelSink) {
let (sender, receiver) = mpsc::unbounded_channel();
(
Self {
buffered: VecDeque::new(),
receiver: Some(receiver),
},
ChannelSink { sender },
)
}
#[must_use]
pub fn immediate(turn: AssistantTurn) -> Self {
let mut buffered = VecDeque::with_capacity(turn.blocks.len() + 2);
buffered.push_back(TurnEvent::Phase(TurnPhase::Delivered));
for block in &turn.blocks {
buffered.push_back(TurnEvent::Block(Box::new(block.clone())));
}
buffered.push_back(TurnEvent::Completed(Box::new(turn)));
Self {
buffered,
receiver: None,
}
}
#[must_use]
pub fn failed(code: impl Into<String>) -> Self {
let mut buffered = VecDeque::with_capacity(1);
buffered.push_back(TurnEvent::Failed { code: code.into() });
Self {
buffered,
receiver: None,
}
}
pub async fn collect_events(mut self) -> Vec<TurnEvent> {
let mut events: Vec<TurnEvent> = self.buffered.drain(..).collect();
if let Some(receiver) = self.receiver.as_mut() {
while let Some(event) = receiver.recv().await {
events.push(event);
}
}
events
}
}
impl Stream for TurnStream {
type Item = TurnEvent;
fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
if let Some(event) = this.buffered.pop_front() {
return Poll::Ready(Some(event));
}
match this.receiver.as_mut() {
Some(receiver) => receiver.poll_recv(context),
None => Poll::Ready(None),
}
}
}
#[cfg(test)]
mod tests {
use turnframe_core::event::{OperationalReceipt, ReceiptSeverity};
use turnframe_core::ids::{BlockId, ConversationId, EventId, ReceiptId, TurnId};
use turnframe_core::locale::LocalizedText;
use turnframe_core::response::{GeneratedTransition, ReceiptBlock, ServerNotice};
use super::*;
fn receipt_block() -> ResponseBlock {
ResponseBlock::Receipt(ReceiptBlock {
block_id: BlockId::from("receipt:1"),
receipt: OperationalReceipt {
receipt_id: ReceiptId::nil(),
event_ids: vec![EventId::nil()],
severity: ReceiptSeverity::Success,
title: LocalizedText::new("Sent"),
body: LocalizedText::new("It went out."),
status_code: "trip.rebooking_sent".to_owned(),
artifact_refs: Vec::new(),
},
})
}
fn transition_block() -> ResponseBlock {
ResponseBlock::Transition(GeneratedTransition {
block_id: BlockId::from("transition:0"),
text: "Right away.".to_owned(),
facts_used: Vec::new(),
})
}
fn notice_block() -> ResponseBlock {
ResponseBlock::Notice(ServerNotice {
block_id: BlockId::from("notice:x"),
code: "x".to_owned(),
severity: turnframe_core::response::NoticeSeverity::Info,
text: LocalizedText::new("nothing was submitted"),
})
}
#[test]
fn no_outcome_leaves_before_commit() {
for phase in [
TurnPhase::Received,
TurnPhase::Interpreted,
TurnPhase::Reduced,
TurnPhase::Executing,
] {
let gate = PublicationGate::at(phase);
assert!(!gate.admits(&receipt_block()), "{phase:?}");
assert!(!gate.admits(&transition_block()), "{phase:?}");
assert!(gate.admits(¬ice_block()), "{phase:?}");
}
}
#[test]
fn everything_is_publishable_once_the_turn_committed() {
let gate = PublicationGate::at(TurnPhase::Committed);
assert!(gate.admits(&receipt_block()));
assert!(gate.admits(&transition_block()));
assert!(gate.admits(¬ice_block()));
}
#[test]
fn the_publisher_drops_a_block_offered_too_early() {
let sink = Arc::new(RecordingSink::new());
let publisher = TurnPublisher::new(sink.clone());
publisher.phase_reached(TurnPhase::Executing);
assert!(!publisher.block(&receipt_block()));
assert!(sink.blocks().is_empty());
publisher.phase_reached(TurnPhase::Committed);
assert!(publisher.block(&receipt_block()));
assert_eq!(sink.blocks().len(), 1);
}
#[tokio::test]
async fn a_click_only_turn_streams_without_waiting() {
let turn = AssistantTurn {
turn_id: TurnId::nil(),
conversation_id: ConversationId::nil(),
blocks: vec![notice_block()],
subjects: Vec::new(),
expectations: Vec::new(),
replay_token: turnframe_core::response::ReplayToken::from("t"),
done: Vec::new(),
};
let events = TurnStream::immediate(turn).collect_events().await;
assert_eq!(events.len(), 3);
assert!(events.last().is_some_and(TurnEvent::is_terminal));
}
}