ChannelPort interface from @comis/core, which defines messaging operations (send, receive, edit, delete, react). This guide walks you through building a complete adapter, using the EchoChannelAdapter as a reference implementation.
ChannelPort Interface
TheChannelPort interface defines the contract that every channel adapter must implement. It covers the full messaging lifecycle: connecting, receiving messages, sending responses, and disconnecting.
You do not need to implement every method fully. Methods like
editMessage, reactToMessage, fetchMessages, and platformAction can return err(new Error("Not supported")) if your platform does not support them. The system uses ChannelCapability metadata to know which features are available.Capturing inbound reactions (optional)
onReaction? is an optional capability — like getStatus?, an adapter may omit it entirely and the system treats it as an honest no-op (the orchestrator calls it as adapter.onReaction?.(handler), which does nothing when absent). Implement it only if your platform delivers an inbound reaction-add event that carries the reactor’s identity. The reference adapters do: Discord (MessageReactionAdd), Slack (reaction_added), and Telegram (message_reaction) all implement it; iMessage, LINE, IRC, Email, and Echo omit it because their inbound APIs expose no reactor id.
When you implement it, convert each inbound reaction-add into a NormalizedReaction and pass it to the handler, mirroring how onMessage mints a NormalizedMessage:
NormalizedReaction is deliberately trust-free — it carries no authority field. It is untrusted inbound data; the daemon resolves the reactor’s trust downstream (from config) and a reaction can only weight a learning outcome, never assert one. Filter out the bot’s own reactions before dispatching (so the agent does not learn from itself), and keep the handler dispatch non-fatal — a throwing handler must not crash your platform’s event loop.
File Structure
The canonical file structure for a channel adapter follows this layout:ChannelPort) and the plugin wrapper (implements ChannelPluginPort).
Tutorial: Building a Minimal Adapter
1
Implement ChannelPort
Create the adapter class that implements the
ChannelPort interface. The EchoChannelAdapter at packages/channels/src/echo/echo-adapter.ts (251 lines) is the simplest real example to reference.2
Normalize incoming messages
When your platform receives a message, convert it to a
NormalizedMessage before passing it to the handler. This is the standard message format that all of Comis understands.Key fields of NormalizedMessage:Create a
message-mapper.ts file to keep the normalization logic separate from the adapter:3
Define ChannelCapability metadata
The Set each feature flag honestly. The system trusts your declared capabilities to decide how to deliver messages. For example, if
ChannelCapability schema describes what your adapter supports. The system uses this metadata for feature negotiation — for example, deciding whether to attempt streaming or threading on your channel.streaming.supported is false, Comis will send complete messages instead of streaming edits.4
Create the ChannelPluginPort wrapper
Wrap your adapter in a
ChannelPluginPort for registration with the plugin system. The plugin provides metadata (ID, name, capabilities) and lifecycle hooks (activate, deactivate).Reference packages/channels/src/echo/echo-plugin.ts for the simplest real example:5
Register and test
The plugin is registered through the
PluginRegistry during bootstrap. The channel registry validates your declared capabilities against the ChannelCapabilitySchema at registration time.For testing, use the co-located test pattern. Create myplatform-adapter.test.ts alongside the adapter and test:- Lifecycle:
start()returnsok,stop()returnsok - Message sending:
sendMessage()returns a message ID - Message receiving: Call
onMessage()handler with a normalized message and verify it processes correctly - Unsupported operations: Methods that return
err()do so with descriptive messages
Reference: EchoChannelAdapter
TheEchoChannelAdapter is the canonical reference implementation. It is an in-memory adapter with no external dependencies, used extensively in integration tests. Located at packages/channels/src/echo/echo-adapter.ts (251 lines).
The Echo adapter implements every ChannelPort method by storing data in memory maps. It also provides test helper methods like injectMessage() (simulates incoming messages) and getSentMessages() (retrieves sent messages for assertions).
Study this adapter to see every ChannelPort method implemented correctly with the Result pattern.
Reference: Real Adapters
For more complex adapter implementations, reference these production adapters:- Telegram — The most complete adapter with 8 files. Webhook and polling modes, rich reactions, voice messages, platform actions (pin, kick, poll, ban). Located at
packages/channels/src/telegram/. - Discord — Rich feature support including buttons, embeds, reactions, threads, and guild management. Located at
packages/channels/src/discord/. - Slack — Block Kit integration with both Socket Mode and HTTP mode. Located at
packages/channels/src/slack/.
Related
Architecture
Port interfaces and hexagonal pattern
Plugins
General plugin system (hooks, tools, routes)
Event Bus
Channel events your adapter should emit
Channels Overview
User-facing channel documentation
