//! Channel manager for coordinating multiple input channels. use std::collections::HashMap; use std::sync::Arc; use futures::stream; use tokio::sync::{RwLock, mpsc}; use crate::channels::{Channel, IncomingMessage, MessageStream, OutgoingResponse, StatusUpdate}; use crate::error::ChannelError; /// Manages multiple input channels and merges their message streams. /// /// Includes an injection channel so background tasks (e.g., job monitors) can /// push messages into the agent loop without being a full `Channel` impl. pub struct ChannelManager { channels: Arc>>>, inject_tx: mpsc::Sender, /// Taken once in `start_all()` and merged into the stream. inject_rx: tokio::sync::Mutex>>, } impl ChannelManager { /// Create a new channel manager. pub fn new() -> Self { let (inject_tx, inject_rx) = mpsc::channel(64); Self { channels: Arc::new(RwLock::new(HashMap::new())), inject_tx, inject_rx: tokio::sync::Mutex::new(Some(inject_rx)), } } /// Get a clone of the injection sender. /// /// Background tasks (like job monitors) use this to push messages into the /// agent loop without being a full `Channel` implementation. pub fn inject_sender(&self) -> mpsc::Sender { self.inject_tx.clone() } /// Add a channel to the manager. pub async fn add(&self, channel: Box) { let name = channel.name().to_string(); self.channels.write().await.insert(name.clone(), channel); tracing::debug!("Added channel: {}", name); } /// Hot-add a channel to a running agent. /// /// Starts the channel, registers it in the channels map for `respond()`/`broadcast()`, /// and spawns a task that forwards its stream messages through `inject_tx` into /// the agent loop. pub async fn hot_add(&self, channel: Box) -> Result<(), ChannelError> { let name = channel.name().to_string(); let stream = channel.start().await?; // Register for respond/broadcast/send_status self.channels.write().await.insert(name.clone(), channel); // Forward stream messages through inject_tx let tx = self.inject_tx.clone(); tokio::spawn(async move { use futures::StreamExt; let mut stream = stream; while let Some(msg) = stream.next().await { if tx.send(msg).await.is_err() { tracing::warn!(channel = %name, "Inject channel closed, stopping hot-added channel"); break; } } tracing::info!(channel = %name, "Hot-added channel stream ended"); }); Ok(()) } /// Start all channels and return a merged stream of messages. /// /// Also merges the injection channel so background tasks can push messages /// into the same stream. pub async fn start_all(&self) -> Result { let channels = self.channels.read().await; let mut streams: Vec = Vec::new(); for (name, channel) in channels.iter() { match channel.start().await { Ok(stream) => { tracing::info!("Started channel: {}", name); streams.push(stream); } Err(e) => { tracing::error!("Failed to start channel {}: {}", name, e); // Continue with other channels, don't fail completely } } } if streams.is_empty() { return Err(ChannelError::StartupFailed { name: "all".to_string(), reason: "No channels started successfully".to_string(), }); } // Take the injection receiver (can only be taken once) if let Some(inject_rx) = self.inject_rx.lock().await.take() { let inject_stream = tokio_stream::wrappers::ReceiverStream::new(inject_rx); streams.push(Box::pin(inject_stream)); tracing::debug!("Injection channel merged into message stream"); } // Merge all streams into one let merged = stream::select_all(streams); Ok(Box::pin(merged)) } /// Send a response to a specific channel. pub async fn respond( &self, msg: &IncomingMessage, response: OutgoingResponse, ) -> Result<(), ChannelError> { let channels = self.channels.read().await; if let Some(channel) = channels.get(&msg.channel) { channel.respond(msg, response).await } else { Err(ChannelError::SendFailed { name: msg.channel.clone(), reason: "Channel not found".to_string(), }) } } /// Send a status update to a specific channel. /// /// The metadata contains channel-specific routing info (e.g., Telegram chat_id) /// needed to deliver the status to the correct destination. pub async fn send_status( &self, channel_name: &str, status: StatusUpdate, metadata: &serde_json::Value, ) -> Result<(), ChannelError> { let channels = self.channels.read().await; if let Some(channel) = channels.get(channel_name) { channel.send_status(status, metadata).await } else { // Silently ignore if channel not found (status is best-effort) Ok(()) } } /// Broadcast a message to a specific user on a specific channel. /// /// Used for proactive notifications like heartbeat alerts. pub async fn broadcast( &self, channel_name: &str, user_id: &str, response: OutgoingResponse, ) -> Result<(), ChannelError> { let channels = self.channels.read().await; if let Some(channel) = channels.get(channel_name) { channel.broadcast(user_id, response).await } else { Err(ChannelError::SendFailed { name: channel_name.to_string(), reason: "Channel not found".to_string(), }) } } /// Broadcast a message to all channels. /// /// Sends to the specified user on every registered channel. pub async fn broadcast_all( &self, user_id: &str, response: OutgoingResponse, ) -> Vec<(String, Result<(), ChannelError>)> { let channels = self.channels.read().await; let mut results = Vec::new(); for (name, channel) in channels.iter() { let result = channel.broadcast(user_id, response.clone()).await; results.push((name.clone(), result)); } results } /// Check health of all channels. pub async fn health_check_all(&self) -> HashMap> { let channels = self.channels.read().await; let mut results = HashMap::new(); for (name, channel) in channels.iter() { results.insert(name.clone(), channel.health_check().await); } results } /// Shutdown all channels. pub async fn shutdown_all(&self) -> Result<(), ChannelError> { let channels = self.channels.read().await; for (name, channel) in channels.iter() { if let Err(e) = channel.shutdown().await { tracing::error!("Error shutting down channel {}: {}", name, e); } } Ok(()) } /// Get list of channel names. pub async fn channel_names(&self) -> Vec { self.channels.read().await.keys().cloned().collect() } } impl Default for ChannelManager { fn default() -> Self { Self::new() } }