Overview
Channels receive messages from external sources (CLI, HTTP, Signal, etc.) and convert them to a unified message format for the agent to process.
Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ ChannelManager │
│ │
│ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ ReplChannel │ │ HttpChannel │ │ WasmChannel │ ... │
│ └──────┬───────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └─────────────────┴─────────────────┘ │
│ │ │
│ select_all (futures) │
│ │ │
│ ▼ │
│ MessageStream │
└─────────────────────────────────────────────────────────────────────┘
Channel Trait
All channels implement the Channel trait.
#[async_trait]
pub trait Channel: Send + Sync {
fn name(&self) -> &str;
async fn start(&self) -> Result<MessageStream, ChannelError>;
async fn respond(&self, msg: &IncomingMessage, response: OutgoingResponse) -> Result<(), ChannelError>;
async fn send_status(&self, status: StatusUpdate, metadata: &serde_json::Value) -> Result<(), ChannelError>;
async fn broadcast(&self, user_id: &str, response: OutgoingResponse) -> Result<(), ChannelError>;
async fn health_check(&self) -> Result<(), ChannelError>;
fn conversation_context(&self, metadata: &serde_json::Value) -> HashMap<String, String>;
async fn shutdown(&self) -> Result<(), ChannelError>;
}
name
Returns the channel identifier (e.g., “cli”, “slack”, “telegram”, “http”).
start
async fn start(&self) -> Result<MessageStream, ChannelError>
Starts listening for messages. Returns a stream of incoming messages. The channel should handle reconnection and error recovery internally.
respond
async fn respond(
&self,
msg: &IncomingMessage,
response: OutgoingResponse,
) -> Result<(), ChannelError>
Sends a response back to the user in the context of the original message (same channel, same thread if applicable).
send_status
async fn send_status(
&self,
status: StatusUpdate,
metadata: &serde_json::Value,
) -> Result<(), ChannelError>
Sends a status update (thinking, tool execution, etc.). The metadata contains channel-specific routing info.
broadcast
async fn broadcast(
&self,
user_id: &str,
response: OutgoingResponse,
) -> Result<(), ChannelError>
Sends a proactive message without a prior incoming message. Used for alerts, heartbeat notifications, and other agent-initiated communication.
health_check
async fn health_check(&self) -> Result<(), ChannelError>
Checks if the channel is healthy and able to send/receive messages.
conversation_context
fn conversation_context(&self, metadata: &serde_json::Value) -> HashMap<String, String>
Extracts conversation context from message metadata for the system prompt. Returns key-value pairs like “sender”, “sender_uuid”, “group” that help the LLM understand who it’s talking to.
shutdown
async fn shutdown(&self) -> Result<(), ChannelError>
Gracefully shuts down the channel.
IncomingMessage
A message received from an external channel.
pub struct IncomingMessage {
pub id: Uuid,
pub channel: String,
pub user_id: String,
pub user_name: Option<String>,
pub content: String,
pub thread_id: Option<String>,
pub received_at: DateTime<Utc>,
pub metadata: serde_json::Value,
}
Channel this message came from
User identifier within the channel
Thread/conversation ID for threaded conversations
When the message was received
metadata
serde_json::Value
required
Channel-specific metadata
OutgoingResponse
Response to send back to a channel.
pub struct OutgoingResponse {
pub content: String,
pub thread_id: Option<String>,
pub attachments: Vec<String>,
pub metadata: serde_json::Value,
}
Optional thread ID to reply in
Optional file paths to attach
Channel-specific metadata for the response
StatusUpdate
Status update types for showing agent activity.
pub enum StatusUpdate {
Thinking(String),
ToolStarted { name: String },
ToolCompleted { name: String, success: bool },
ToolResult { name: String, preview: String },
StreamChunk(String),
Status(String),
JobStarted { job_id: String, title: String, browse_url: String },
ApprovalNeeded { request_id: String, tool_name: String, description: String, parameters: serde_json::Value },
AuthRequired { extension_name: String, instructions: Option<String>, auth_url: Option<String>, setup_url: Option<String> },
AuthCompleted { extension_name: String, success: bool, message: String },
}
ChannelManager
Manages multiple input channels and merges their message streams.
Constructor
Creates a new channel manager.
add
pub async fn add(&self, channel: Box<dyn Channel>)
Adds a channel to the manager.
hot_add
pub async fn hot_add(&self, channel: Box<dyn Channel>) -> Result<(), ChannelError>
Hot-adds a channel to a running agent. Starts the channel, registers it for respond/broadcast, and spawns a task to forward its messages.
start_all
pub async fn start_all(&self) -> Result<MessageStream, ChannelError>
Starts all channels and returns a merged stream of messages.
respond
pub async fn respond(
&self,
msg: &IncomingMessage,
response: OutgoingResponse,
) -> Result<(), ChannelError>
Sends a response to a specific channel.
send_status
pub async fn send_status(
&self,
channel_name: &str,
status: StatusUpdate,
metadata: &serde_json::Value,
) -> Result<(), ChannelError>
Sends a status update to a specific channel.
broadcast
pub async fn broadcast(
&self,
channel_name: &str,
user_id: &str,
response: OutgoingResponse,
) -> Result<(), ChannelError>
Broadcasts a message to a specific user on a specific channel.
broadcast_all
pub async fn broadcast_all(
&self,
user_id: &str,
response: OutgoingResponse,
) -> Vec<(String, Result<(), ChannelError>)>
Broadcasts a message to all channels.
inject_sender
pub fn inject_sender(&self) -> mpsc::Sender<IncomingMessage>
Get a clone of the injection sender. Background tasks use this to push messages into the agent loop.
Example: Custom Channel
use ironclaw::channels::{Channel, IncomingMessage, MessageStream, OutgoingResponse};
use async_trait::async_trait;
#[derive(Debug)]
pub struct MyChannel;
#[async_trait]
impl Channel for MyChannel {
fn name(&self) -> &str {
"mychannel"
}
async fn start(&self) -> Result<MessageStream, ChannelError> {
// Create a stream that listens for messages
// Return a stream of IncomingMessage
unimplemented!()
}
async fn respond(
&self,
msg: &IncomingMessage,
response: OutgoingResponse,
) -> Result<(), ChannelError> {
// Send response back to the user
unimplemented!()
}
async fn health_check(&self) -> Result<(), ChannelError> {
// Check if the channel is healthy
Ok(())
}
}
// Register the channel
let manager = ChannelManager::new();
manager.add(Box::new(MyChannel)).await;