How Messaging Channels Handle Inbound and Outbound Communication in OpenHuman

OpenHuman routes every external chat message through a unified channel abstraction: inbound messages are published to a global event bus, processed by an agent runtime, and returned via provider-specific SendMessage trait implementations that preserve thread context and platform metadata.

The tinyhumansai/openhuman repository implements a Rust-based messaging layer that abstracts Telegram, Discord, Slack, and web interfaces into a single pipeline. Understanding how these messaging channels handle both inbound and outbound communication reveals a clean separation between transport concerns and agent logic, orchestrated through the src/openhuman/channels/runtime/dispatch.rs runtime.

The Unified Channel Architecture

Common Traits and Types

All channels implement the Channel trait defined in src/openhuman/channels/mod.rs. This interface standardizes how messages enter and exit the system through the ChannelMessage and SendMessage structures, ensuring that provider-specific details (like Telegram's chat IDs or Discord's thread timestamps) remain isolated from the core agent logic.

The Dispatch Runtime

The src/openhuman/channels/runtime/dispatch.rs file contains the orchestration logic that coordinates the full turn lifecycle. It manages the transition from external provider events to internal AgentTurnRequest objects, handling context assembly and acknowledgment generation through dedicated helper functions.

Inbound Communication Flow

Publishing to the Global Event Bus

When a provider listener receives a native message, it constructs a ChannelMessage and publishes a DomainEvent::ChannelInboundMessage using event_bus::publish_global. This decouples the transport layer from processing logic, allowing the system to scale message ingestion independently from agent computation.

// 1. Provider listener creates a ChannelMessage
let msg = ChannelMessage { channel: "telegram".into(), ... };

// 2. Publish to the event bus
event_bus::publish_global(DomainEvent::ChannelInboundMessage(msg));

// 3. In dispatch.rs the inbound message worker builds context
let context = build_channel_context_block(&msg);
let reaction = select_acknowledgment_reaction(&msg.content);

// 4. The context + reaction are spliced into the prompt sent to the agent

Context Assembly and Acknowledgments

The Channel Inbound Subscriber (registered in src/openhuman/channels/bus.rs) consumes these events and prepares the agent prompt. Two critical functions in src/openhuman/channels/runtime/dispatch.rs handle this preparation.

The build_channel_context_block function injects channel metadata into the agent context, instructing the LLM how to route any scheduled reminders back to the correct platform:

fn build_channel_context_block(msg: &traits::ChannelMessage) -> String {
    if msg.channel.trim().eq_ignore_ascii_case("web") {
        return String::new();               // no context for web/CLI
    }
    let reply_target = msg.reply_target.trim();
    if reply_target.is_empty() {
        return String::new();
    }
    format!(
        "[Channel context]\n\
         You are responding via the \"{channel}\" channel. Reply target: \"{reply_target}\".\n\
         For any cron/scheduled reminder you create with `cron_add`, set `delivery` to \
         {{ \"mode\": \"announce\", \"channel\": \"{channel}\", \"to\": \"{reply_target}\" }} \
         so the reminder is delivered back here instead of the in‑app web stream.\n\n",
        channel = msg.channel,
        reply_target = msg.reply_target,
    )
}

The select_acknowledgment_reaction function analyzes message content to choose an appropriate emoji reaction, providing immediate user feedback while the agent processes the request:

fn select_acknowledgment_reaction(content: &str) -> &'static str {
    let l = content.to_lowercase();
    let v = content.len().wrapping_add(content.chars().next().map_or(0, |c| c as usize)) & 1;
    let opts = if contains_any(&l, &["thank", "thx"]) {
        &["❤️", "🙏"]
    } else if contains_any(&l, &["amazing", "awesome"]) {
        &["🔥", "🎉"]
    } else if contains_any(&l, &["price", "btc"]) {
        &["💯", "⚡"]
    } else if contains_any(&l, &["code", "function"]) {
        &["👨‍💻", "🤓"]
    } else if starts_with_any(&l, &["hi", "hello"]) {
        &["🤗", "😁"]
    } else if l.contains('?') {
        &["🤔", "✍️"]
    } else {
        &["👀", "✍️"]
    };
    opts[v % opts.len()]
}

Outbound Communication Flow

The SendMessage Trait

After the agent generates a response, the runtime calls the send_message method on the concrete channel implementation. This method receives a SendMessage struct containing the response text, target recipient, and preserved thread identifiers from the original inbound message:

// After the agent turn finishes, the runtime calls:
channel.send_message(SendMessage {
    text: response_text,
    reply_target: msg.reply_target.clone(),
    thread_ts: msg.thread_ts,          // preserved for threaded platforms
});

Platform-Specific Delivery

Each provider implements Channel::send_message to handle API authentication, message formatting, and thread preservation. The implementations in src/openhuman/channels/providers/telegram/channel_send.rs and src/openhuman/channels/providers/discord/channel_send.rs demonstrate how the same trait accommodates different platform constraints.

Telegram automatically handles thread timestamps via set_thread_ts:

impl Channel for TelegramChannel {
    async fn send_message(&self, msg: SendMessage) -> Result<()> {
        // Preserve thread_ts if the inbound message was part of a thread
        let mut payload = self.api.send_text(&msg.text, &msg.recipient).await?;
        if let Some(thread) = msg.thread_ts {
            payload.set_thread_ts(thread);
        }
        self.api.execute(payload).await?;
        Ok(())
    }
}

Discord requires explicit message referencing through reference_message:

impl Channel for DiscordChannel {
    async fn send_message(&self, msg: SendMessage) -> Result<()> {
        let mut builder = self.api.create_message(&msg.recipient);
        builder.content(&msg.text);
        // Discord does **not** fabricate a thread_ts when none is supplied
        if let Some(ts) = msg.thread_ts {
            builder.reference_message(ts);
        }
        builder.await?;
        Ok(())
    }
}

Both implementations preserve the thread_ts from the inbound ChannelMessage to ensure replies appear in the correct conversation thread.

Summary

  • OpenHuman abstracts Telegram, Discord, and other platforms through a unified Channel trait defined in src/openhuman/channels/mod.rs.
  • Inbound messages flow through event_bus::publish_global and are processed by src/openhuman/channels/runtime/dispatch.rs to build context blocks and select acknowledgment reactions.
  • The build_channel_context_block function ensures agents know how to route scheduled reminders back to the correct channel and thread.
  • Outbound replies use the SendMessage trait, with concrete implementations in provider-specific channel_send.rs files preserving thread_ts to maintain conversation continuity.
  • Provider-specific logic handles platform API nuances while keeping the core agent runtime platform-agnostic.

Frequently Asked Questions

How does OpenHuman preserve conversation threads across different messaging platforms?

The system extracts thread identifiers from inbound messages and stores them in the ChannelMessage struct's thread_ts field. When the agent runtime calls send_message, it passes this identifier back to the provider implementation. Telegram uses set_thread_ts while Discord uses reference_message, ensuring replies appear in the same thread regardless of platform-specific API differences.

Can new messaging channels be added without modifying the core agent logic?

Yes. New platforms only need to implement the Channel trait from src/openhuman/channels/mod.rs, specifically the send_message method, and register an inbound listener that publishes DomainEvent::ChannelInboundMessage events. The dispatch runtime and agent processing logic remain unchanged because they operate on the abstract ChannelMessage type rather than platform-specific implementations.

What determines which acknowledgment emoji gets sent?

The select_acknowledgment_reaction function in src/openhuman/channels/runtime/dispatch.rs analyzes message content using keyword matching. Questions trigger 🤔 or ✍️, gratitude triggers ❤️ or 🙏, and code-related messages trigger 👨‍💻 or 🤓. This provides immediate visual feedback while the LLM agent generates its formal response.

Where does the channel context block appear in the agent prompt?

The build_channel_context_block function in src/openhuman/channels/runtime/dispatch.rs injects metadata at the beginning of the prompt. This block informs the agent which channel originated the message and specifies the delivery parameters for any cron reminders, ensuring scheduled messages return to the correct external platform rather than the default web stream.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →