Building a Channel Adapter
Built-in channel adapters bridge external messaging platforms through the Unified Message Interface (UMI). Gateway clients use a separate WebSocket frame schema; adapters are in-process classes.
Architecture
Section titled “Architecture”External Platform (e.g., Slack, Teams, custom platform) │ ▼YourChannel extends BaseChannel (your code) │ ├── emitMessage() ──► UMI ──► Gateway Hub ──► Root agent │ ◄── send() ◄── UMI ◄── Gateway Hub ◄── Root agent response ◄── setReaction() ◄── UMI ◄── Gateway Hub ◄── Feedback emoji ◄── sendTyping() ◄── UMI ◄── Gateway Hub ◄── Typing indicatorCreating an Adapter
Section titled “Creating an Adapter”1. Extend BaseChannel
Section titled “1. Extend BaseChannel”The following is SDK-specific pseudocode, not a drop-in implementation. Define
and type MyPlatformClient, load credentials through the config/vault integration,
and map real message IDs and error handling before using it.
import { BaseChannel } from '../interface';import type { ChannelResponse, ChannelType } from '@/core/types';
export class MyPlatformChannel extends BaseChannel { readonly type: ChannelType = 'my-platform'; readonly name = 'My Platform';
private client: MyPlatformClient | null = null;
async connect(): Promise<void> { // Initialize your platform SDK // Resolve credentials through the vault/config integration used by the // rest of the backend. Do not read channel secrets from process.env at // runtime; .env is only a first-boot seed for configured deployments. this.client = new MyPlatformClient(await loadMyPlatformCredentials());
// Bridge: platform messages → channel this.client.on('message', (msg) => { this.emitMessage( this.createUnifiedMessage(msg.channelId, msg.author.id, msg.content, { userName: msg.author.username, metadata: { messageId: msg.id }, }) ); });
await this.client.connect(); this.setConnected(true); }
async disconnect(): Promise<void> { await this.client?.disconnect(); this.setConnected(false); }
// Channel sends response → deliver to platform async send(channelId: string, response: ChannelResponse): Promise<string> { await this.client.sendMessage(channelId, response.content); return 'message-sent'; }
// Optional: emoji reactions async setReaction(channelId: string, messageId: string, emoji: string): Promise<void> { await this.client.addReaction(messageId, emoji); }
// Optional: typing indicator async sendTyping(channelId: string, active: boolean = true): Promise<void> { if (active) { await this.client.startTyping(channelId); } }}2. Make the channel discoverable
Section titled “2. Make the channel discoverable”Export a BaseChannel instance or a constructible subclass from
src/channels/<name>/index.ts. src/channels/discovery.ts discovers it;
src/channels/index.ts registers discovered channels with UMI. Override
isEnabled(config) to gate startup on configured credentials, following a
sibling adapter. Register any new settings and secret mappings, and verify the
channel is also present in the production bundle.
3. Add ChannelType
Section titled “3. Add ChannelType”Add your channel type to src/core/types.ts:
// Append 'my-platform' to the existing ChannelType union.// Preserve existing API/test channel members.And update the configuration schema in src/config/schema.ts to include settings for your channel if needed.
Channel Interface
Section titled “Channel Interface”Methods to Override
Section titled “Methods to Override”Your channel must implement these abstract methods from BaseChannel:
connect(): Promise<void> — Initialize the channel
async connect(): Promise<void> { // Set up client, register event listeners, connect to platform this.setConnected(true);}disconnect(): Promise<void> — Clean up and disconnect
async disconnect(): Promise<void> { // Close connections, clear state this.setConnected(false);}send(channelId: string, response: ChannelResponse): Promise<string> — Deliver a response to the user
async send(channelId: string, response: ChannelResponse): Promise<string> { // response.content — message text // response.attachments — array of files to send // response.replyTo — optional message ID to reply to // response.threadId — optional thread to post in // Return the platform-specific message ID}Optional Methods
Section titled “Optional Methods”setReaction(channelId: string, messageId: string, emoji: string): Promise<void> — Add emoji reaction
async setReaction(channelId: string, messageId: string, emoji: string): Promise<void> { // Add emoji to a message (skip if platform doesn't support)}sendTyping(channelId: string, active: boolean): Promise<void> — Typing indicator
async sendTyping(channelId: string, active: boolean = true): Promise<void> { // Show/hide typing indicator (skip if platform doesn't support)}Sending Messages from Your Platform
Section titled “Sending Messages from Your Platform”When a user sends a message on your platform, emit it to the UMI:
this.emitMessage( this.createUnifiedMessage( 'channel-123', // Platform-specific chat/channel ID 'user-456', // Platform user ID 'Hello!', // Message text { userName: 'Alice', // Display name (optional) attachments: [], // Optional file attachments threadId: 'thread-1', // Optional thread/reply chain metadata: { messageId: 'msg-789' }, // Platform-specific metadata } ));Connection State Changes
Section titled “Connection State Changes”Update the connection state when the channel connects or disconnects:
this.setConnected(true); // Connectedthis.setConnected(false); // DisconnectedTo emit errors, use:
this.emitError(new Error('Connection failed'));Testing
Section titled “Testing”Test your channel by subscribing to UMI events:
import { MyPlatformChannel } from './my-platform';import { getUMI } from '@/channels';
const channel = new MyPlatformChannel();const umi = getUMI();umi.register(channel);
// Listen for messages from the platformconst messages: any[] = [];umi.on('message', (msg) => { messages.push(msg);});
await channel.connect();
// Simulate platform messagechannel.emitMessage( channel.createUnifiedMessage('ch-1', 'u-1', 'test', { userName: 'Alice', }));
expect(messages).toHaveLength(1);expect(messages[0].content).toBe('test');
await channel.disconnect();