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:
-
tinychannelscrate: Provides the foundationalChanneltrait, 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 thechannelsfeature 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 theChanneltrait 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 thechannelsfeature 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-exportstinychannels::{Channel, SendMessage, ChannelSendExt}, allowing the interactive harness to reference channel types regardless of whether provider code is included. -
src/openhuman/channels/cli.rs: ImplementsCliChannel, 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 |
channels |
WhatsAppChannel |
|
| WhatsApp-Web | channels + whatsapp-web |
WhatsAppWebChannel |
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:
- Provider Instantiation: Creates enabled provider instances via their respective
new()constructors. - RPC Registration: Registers each provider's controller with the core's dispatcher under the
openhuman.channels_*namespace. - Event Bus Subscription: Subscribes to
DomainEvent::ChannelMessageevents viasrc/openhuman/channels/bus.rs, routing external messages (e.g., Telegram bot callbacks) into the core event system. - 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 definitionssend_message: Dispatches messages to specific channelslist_messages: Retrieves message historyconnect/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
tinychannelsfor provider-agnostic traits while thesrc/openhuman/channels/domain handles runtime orchestration. - Compile-Time Gating: The
channelsfeature flag controls inclusion of provider code, withtraits.rsandcli.rsas zero-dependency carve-outs. - Provider Isolation: Each provider (Telegram, Discord, WhatsApp) implements the
Channeltrait independently, with optional sub-features likewhatsapp-web. - Runtime Coordination: The
Hoststruct insrc/openhuman/channels/host/mod.rsmanages provider lifecycle, RPC registration, and event bus integration viaDomainEvent::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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →