use async_trait::async_trait; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use crate::bus::{BusError, CommittedTurnDelta, InboundMessage, MessageBus, OutboundMessage}; use crate::delivery::PresentationPolicy; use crate::session::TurnSnapshot; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LivePolicy { FinalOnly, Snapshot { min_interval: Duration }, } #[derive(Debug, Clone)] pub struct TurnTarget { pub channel: String, pub chat_id: String, pub session_id: String, pub reply_to: Option, pub metadata: HashMap, } #[async_trait] pub trait TurnSink: Send { async fn update(&mut self, snapshot: &TurnSnapshot) -> Result<(), ChannelError>; async fn finish(&mut self, snapshot: &TurnSnapshot) -> Result<(), ChannelError>; async fn abort(&mut self, snapshot: &TurnSnapshot) -> Result<(), ChannelError>; } #[derive(Debug)] pub enum ChannelError { ConfigError(String), ConnectionError(String), SendError(String), BusError(String), Other(String), } impl std::fmt::Display for ChannelError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ChannelError::ConfigError(s) => write!(f, "Config error: {}", s), ChannelError::ConnectionError(s) => write!(f, "Connection error: {}", s), ChannelError::SendError(s) => write!(f, "Send error: {}", s), ChannelError::BusError(s) => write!(f, "Bus error: {}", s), ChannelError::Other(s) => write!(f, "Error: {}", s), } } } impl std::error::Error for ChannelError {} impl ChannelError { pub fn is_transient(&self) -> bool { matches!(self, Self::ConnectionError(_) | Self::SendError(_)) } } impl From for ChannelError { fn from(e: BusError) -> Self { ChannelError::BusError(e.to_string()) } } #[async_trait] pub trait Channel: Send + Sync + 'static { fn name(&self) -> &str; fn is_running(&self) -> bool; /// Start the channel with a reference to the MessageBus async fn start(&self, bus: Arc) -> Result<(), ChannelError>; /// Stop the channel async fn stop(&self) -> Result<(), ChannelError>; fn live_policy(&self) -> LivePolicy { LivePolicy::FinalOnly } fn presentation_policy(&self) -> PresentationPolicy { PresentationPolicy::external(matches!(self.live_policy(), LivePolicy::Snapshot { .. })) } async fn open_turn(&self, _target: TurnTarget) -> Result, ChannelError> { Err(ChannelError::Other(format!( "channel {} does not support turn delivery", self.name() ))) } /// Deliver a durable history delta after a Turn commit. Channels without /// local history views intentionally ignore this event. async fn commit_turn( &self, _target: &TurnTarget, _delta: CommittedTurnDelta, ) -> Result<(), ChannelError> { Ok(()) } /// Whether `commit_turn` presents media references from the committed /// assistant message to the user. Channels that return false receive a /// separate media-only outbound delivery after the durable commit. fn commit_turn_presents_media(&self) -> bool { false } /// Send a message to the channel (called by OutboundDispatcher) async fn send(&self, msg: OutboundMessage) -> Result<(), ChannelError>; /// Check if a sender is allowed to use this channel fn is_allowed(&self, _sender_id: &str) -> bool { true } /// Handle an inbound message: check permissions and publish to bus async fn handle_and_publish( &self, bus: &Arc, msg: &InboundMessage, ) -> Result<(), ChannelError> { if !self.is_allowed(&msg.sender_id) { tracing::warn!( channel = %self.name(), sender = %msg.sender_id, "Access denied" ); return Ok(()); } bus.publish_inbound(msg.clone()).await?; Ok(()) } }