PicoBot/src/channels/base.rs

137 lines
4.0 KiB
Rust

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<String>,
pub metadata: HashMap<String, String>,
}
#[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<BusError> 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<MessageBus>) -> 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<Box<dyn TurnSink>, 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<MessageBus>,
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(())
}
}