How Messaging Channels Work in OpenHuman: tinychannels Stack and Provider Gating Explained

OpenHuman's messaging subsystem leverages the external tinychannels crate for provider-agnostic abstractions, wrapped by a compile-time gated domain layer that enables binary size optimization while maintaining a unified runtime interface for Telegram, Discord, WhatsApp, and other providers.

The OpenHuman repository implements a modular messaging architecture that separates protocol-agnostic interfaces from concrete implementations. By combining the tinychannels stack with aggressive compile-time feature gating, the system achieves zero-cost abstraction for unused messaging backends while preserving consistent RPC interfaces and agent integration.

Core Architecture and Layer Responsibilities

The messaging system follows a strict layered architecture where each component has distinct responsibilities:

  • tinychannels crate: Provides the foundational Channel trait, message envelopes, and pairing logic. This external dependency is always compiled because the core requires these types for configuration, event envelopes, and security pairing.

  • src/openhuman/channels/mod.rs: Serves as the domain entry point, housing the channels feature gate and re-exporting provider types. This module coordinates between the tinychannels abstractions and OpenHuman-specific runtime concerns.

  • Provider implementations (src/openhuman/channels/providers/*): Concrete implementations of the Channel trait for each messaging platform (Telegram, Discord, WhatsApp, Email). Each provider registers its own RPC controller and is conditionally compiled based on feature flags.

  • Runtime and Host (src/openhuman/channels/runtime/, src/openhuman/channels/host/mod.rs): Orchestrates provider lifecycle, manages the event bus, and handles both inbound message delivery and proactive outbound polling.

  • Always-on carve-outs (src/openhuman/channels/traits.rs, src/openhuman/channels/cli.rs): Minimal, dependency-free modules that remain compiled even when the channels feature is disabled, ensuring the interactive harness can reference channel types.

Compile-Time Feature Gating Strategy

The entire messaging domain is controlled by the channels feature flag, which is enabled by default. OpenHuman employs a "leaf-gate" strategy that distinguishes between core types and provider implementations.

Only two sub-modules bypass the feature gate:

  • src/openhuman/channels/traits.rs: Re-exports tinychannels::{Channel, SendMessage, ChannelSendExt}, allowing the interactive harness to reference channel types regardless of whether provider code is included.

  • src/openhuman/channels/cli.rs: Implements CliChannel, a minimal stdin/stdout REPL used for debugging that carries no external dependencies.

All other components—including providers, controllers, runtime, bus, proactive polling, and context management—are wrapped in #[cfg(feature = "channels")]. When disabled, these symbols vanish completely rather than generating stub errors, ensuring that unused providers leave no binary footprint.

Provider Gating Mechanisms

Individual providers reside in src/openhuman/channels/providers/ and follow the same gating logic. While most providers activate with the base channels feature, some require additional flags:

Provider Required Feature Exported Type
Telegram channels TelegramChannel
Discord channels DiscordChannel
WhatsApp channels WhatsAppChannel
WhatsApp-Web channels + whatsapp-web WhatsAppWebChannel
Email channels EmailChannel

The public re-exports in mod.rs apply conditional compilation: #[cfg(feature = "channels")] guards the main provider exports, with additional #[cfg(feature = "whatsapp-web")] gates for the WhatsApp-Web variant. This granularity allows embedded builds to exclude heavy dependencies like lettre (Email) or WhatsApp Web libraries while retaining the core messaging interface.

Runtime Orchestration and Event Flow

When the core initializes, start_channels (exposed from src/openhuman/channels/runtime/start_channels.rs) instantiates a Host (src/openhuman/channels/host/mod.rs) that coordinates four critical functions:

  1. Provider Instantiation: Creates enabled provider instances via their respective new() constructors.
  2. RPC Registration: Registers each provider's controller with the core's dispatcher under the openhuman.channels_* namespace.
  3. Event Bus Subscription: Subscribes to DomainEvent::ChannelMessage events via src/openhuman/channels/bus.rs, routing external messages (e.g., Telegram bot callbacks) into the core event system.
  4. Proactive Polling: Spawns background tasks (src/openhuman/channels/proactive.rs) that poll providers for new messages and push them onto the event bus.

This architecture ensures that all provider-specific logic remains isolated while the interface contract (the Channel trait) stays constant, allowing the core to interact with any provider uniformly.

System Prompt Integration

The agent's system prompt dynamically lists available channels so the LLM can reference them in conversations. The function build_system_prompt in src/openhuman/agent/context/channels_prompt.rs constructs this list by querying enabled providers. The channels module re-exports this function for backward compatibility at src/openhuman/channels/mod.rs.

When a provider is omitted via feature flags, it simply does not appear in the generated prompt, maintaining consistency between the compiled binary's capabilities and the agent's knowledge.

RPC Surface and Operations

The channels domain exposes a comprehensive RPC interface defined in src/openhuman/channels/controllers/ops/. Controllers live in the openhuman.channels_* namespace and include operations such as:

  • list_channels: Returns available channel definitions
  • send_message: Dispatches messages to specific channels
  • list_messages: Retrieves message history
  • connect / disconnect: Manages provider connection states

Schemas are defined in src/openhuman/channels/controllers/schemas.rs and automatically generate OpenAPI documentation.

Practical Implementation Examples

Listing Available Channels via Rust API

To enumerate configured channels programmatically, use the runtime's initialization combined with the RPC interface:

use openhuman::channels::{ChannelDefinition, start_channels};
use openhuman::core::CoreBuilder;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let core = CoreBuilder::new()
        .domains(openhuman::core::DomainSet::full())
        .services(openhuman::core::ServiceSet::none())
        .build()
        .await?;

    start_channels(&core).await?;

    let list = core
        .rpc()
        .call::<Vec<ChannelDefinition>>("openhuman.channels_list", ())
        .await?;

    println!("Enabled channels:");
    for def in list {
        println!("• {} ({})", def.name, def.provider);
    }
    Ok(())
}

The start_channels function in src/openhuman/channels/runtime/start_channels.rs wires providers to the host and event bus.

Sending Messages via RPC Client

Dispatch messages to specific providers using the unified interface:

use openhuman::core::CoreBuilder;
use openhuman::channels::ChannelMessage;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let core = CoreBuilder::new()
        .domains(openhuman::core::DomainSet::full())
        .build()
        .await?;

    let msg = ChannelMessage {
        channel: "my_telegram".into(),
        text: "Hello from OpenHuman!".into(),
        ..Default::default()
    };

    core.rpc()
        .call::<()>("openhuman.channels_send_message", msg)
        .await?;
    Ok(())
}

This operation is handled by the messaging controller in src/openhuman/channels/controllers/ops/messaging.rs.

Using the CLI REPL Interface

For debugging without external dependencies, the always-compiled CLI interface provides interactive access:

$ openhuman channels cli
> list
my_telegram (Telegram)
my_discord (Discord)
> send my_telegram "Test from CLI"
✅ message queued
> quit

The CliChannel implementation in src/openhuman/channels/cli.rs provides this functionality in all builds, regardless of the channels feature state.

Summary

  • Layered Abstraction: OpenHuman uses tinychannels for provider-agnostic traits while the src/openhuman/channels/ domain handles runtime orchestration.
  • Compile-Time Gating: The channels feature flag controls inclusion of provider code, with traits.rs and cli.rs as zero-dependency carve-outs.
  • Provider Isolation: Each provider (Telegram, Discord, WhatsApp) implements the Channel trait independently, with optional sub-features like whatsapp-web.
  • Runtime Coordination: The Host struct in src/openhuman/channels/host/mod.rs manages provider lifecycle, RPC registration, and event bus integration via DomainEvent::ChannelMessage.
  • Agent Integration: Channel availability automatically propagates to the LLM system prompt through src/openhuman/agent/context/channels_prompt.rs.

Frequently Asked Questions

How does OpenHuman handle missing providers when the channels feature is disabled?

When the channels feature is disabled, provider-specific modules vanish at compile time via #[cfg(feature = "channels")]. The core returns an "unknown method" RPC error for channel operations, while the always-compiled traits.rs and cli.rs maintain type compatibility. This leaf-gate strategy ensures zero runtime overhead from unused messaging backends.

What is the difference between the WhatsApp and WhatsApp-Web providers?

The standard WhatsApp provider compiles with the base channels feature and uses WhatsAppChannel, while WhatsApp-Web requires the additional whatsapp-web feature flag and exports WhatsAppWebChannel. The latter typically includes heavier dependencies for web-based WhatsApp integration, allowing embedded builds to exclude it while keeping core WhatsApp support.

How do inbound messages from external services reach the OpenHuman agent?

Inbound messages flow through the provider implementation to the Host, which publishes DomainEvent::ChannelMessage events on the event bus (src/openhuman/channels/bus.rs). The core subscribes to these events and routes them to the agent. Additionally, the proactive module polls providers for messages and pushes them onto the same bus, ensuring both push and pull messaging models work uniformly.

Can I add a custom messaging provider without modifying core files?

Yes. Implement the Channel trait from tinychannels in a new file under src/openhuman/channels/providers/, add it to the module exports in src/openhuman/channels/mod.rs with appropriate #[cfg] gates, and register its RPC controller in the runtime initialization. The uniform trait interface in src/openhuman/channels/traits.rs ensures the core recognizes your provider without changes to orchestration logic.

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 →