Channel Adapter Pattern
Design pattern for implementing messaging channel integrations in AI agent systems. Each channel implements a typed set of optional interfaces (adapters) that the core system calls at well-defined points, rather than inheriting from a monolithic base class.
Pattern structure
A channel plugin is a plain object that satisfies a typed contract. The contract defines 15-20+ optional adapter slots. Each slot handles one concern: connection lifecycle, message sending, authentication, threading, group semantics, security policy, agent tool injection, etc. A channel implements only the slots it needs.
ChannelPlugin = {
id, meta, capabilities,
gateway?: start/stop account connections
outbound?: send formatted messages
config?: account resolution
pairing?: DM access control
security?: per-channel security policy
groups?: group routing, mention handling
threading?: conversation thread binding
streaming?: real-time response streaming
agentTools?: channel-specific tools for the agent
heartbeat?: liveness monitoring
...
}
Why composition over inheritance
Messaging platforms differ wildly. WhatsApp has QR-code login and end-to-end encryption. Discord has servers, channels, and slash commands. Telegram has inline keyboards and webhook vs polling. iMessage has no API at all (bridged via applescript or BlueBubbles). An inheritance hierarchy either becomes a God class that every channel overrides, or fragments into so many abstract methods that the base is meaningless.
Composition avoids both traps. Each adapter slot is a small, typed interface. Channels compose the set they need. Adding a new concern (say, message reactions) means adding a new optional slot — existing channels don’t break.
Two approaches compared
OpenClaw — TypeScript typed objects with 20+ optional adapter interfaces. Factory functions (createChatChannelPlugin()) provide defaults. Lazy-loading boundaries (*.runtime.ts) keep registration fast. Strict SDK boundary: extensions import openclaw/plugin-sdk/*, never core internals. Registry caches plugins by version with deduplication and deterministic ordering.
Hermes Agent — Python ABCs with PlatformAdapter(ABC). Simpler surface (start, stop, send, receive). Platform-specific logic lives in method overrides rather than separate adapter objects. Faster to implement a basic channel but harder to compose fine-grained behaviors without conditional branching.
Key design decisions
Optional vs required adapters — Only id, meta, and config are required in OpenClaw. Everything else is optional. This lets a minimal channel (e.g., IRC) exist with just connection and send logic, while a full-featured channel (e.g., WhatsApp) implements pairing, groups, QR login, threading, agent tools, and more.
Lazy loading — Heavy channel code (send logic, monitoring, probe diagnostics) loads dynamically on first use. Channel registration contributes only metadata and adapter function references, keeping gateway startup fast.
Registry caching — The channel registry maintains a versioned, deduplicated, sorted cache of loaded plugins. Cache invalidation is keyed on registry version and reference identity, not deep comparison.
Multi-account — Each channel type can have multiple accounts (e.g., two Telegram bots, three Discord servers). Config and sessions are keyed by channel + accountId.
When to use this pattern
The channel adapter pattern fits when:
- The system must integrate with many external platforms that share a broad purpose (messaging) but differ in every detail
- New platforms will be added by third-party plugin authors who shouldn’t need to understand core internals
- Platform capabilities vary widely (some support threading, some don’t; some have groups, some don’t)
- Startup performance matters — lazy loading prevents unused channels from slowing initialization
Related
- Multi-Channel AI Gateway — the broader architecture this pattern enables
- Brain-Hands Decoupling — adapters are the “hands” behind stable interfaces