Files
flynn/docs/plans/2026-02-05-phase3-channel-adapters.md
T
William Valentin aa95f2132c feat: add channel adapter abstraction with Telegram and WebChat adapters
Implement Phase 3 channel adapters that decouple message sources from
the agent via a uniform ChannelAdapter interface and ChannelRegistry.

- Add ChannelAdapter/InboundMessage/OutboundMessage types
- Add ChannelRegistry for adapter lifecycle and message routing
- Add TelegramAdapter (grammy bot, auth middleware, confirmations, chunking)
- Add WebChatAdapter (thin shim over GatewayServer)
- Refactor daemon to use ChannelRegistry with per-channel-per-user agents
- Add config.get/config.patch gateway handlers (Phase 2 loose end)
- Add system.restart gateway handler (Phase 2 loose end)
- Add implementation plans and design docs

Tests: 225 passing (33 new channel adapter + gateway handler tests)
2026-02-05 20:00:36 -08:00

7.3 KiB

Phase 3: Channel Adapters — Implementation Plan

Goal

Introduce a ChannelAdapter abstraction that decouples message sources (Telegram, WebChat, future Discord/WhatsApp/Slack) from the agent. Each adapter handles platform-specific I/O and maps messages to a common interface. A ChannelRegistry manages adapter lifecycle and routes messages to/from the agent.

Scope (This Iteration)

  1. Channel typesChannelAdapter interface, InboundMessage, OutboundMessage, ChannelStatus
  2. Channel registry — Register, start/stop, route messages, adapter lifecycle
  3. Telegram adapter — Refactor existing src/frontends/telegram/ into a ChannelAdapter
  4. WebChat adapter — Wrap the existing gateway WS into a ChannelAdapter
  5. Daemon integration — Replace direct bot/gateway creation with registry-managed adapters

Discord, WhatsApp, and Slack adapters are deferred (require new dependencies + credentials).

Architecture

src/channels/
├── types.ts              # ChannelAdapter interface, message types
├── registry.ts           # ChannelRegistry: lifecycle + message routing
├── registry.test.ts      # Registry tests
├── index.ts              # Barrel exports
├── telegram/
│   ├── adapter.ts        # TelegramAdapter implements ChannelAdapter
│   ├── adapter.test.ts   # Adapter tests
│   └── index.ts          # Barrel
└── webchat/
    ├── adapter.ts        # WebChatAdapter implements ChannelAdapter
    ├── adapter.test.ts   # Adapter tests
    └── index.ts          # Barrel

The existing src/frontends/telegram/ code (bot.ts, handlers.ts, confirmations.ts) stays in place and is wrapped by the adapter. The adapter delegates to the existing bot creation logic. No breaking changes to existing code.

Types Design

InboundMessage

interface InboundMessage {
  id: string;                    // Platform message ID
  channel: string;               // Adapter name: "telegram", "webchat", etc.
  senderId: string;              // Platform user ID
  senderName?: string;           // Display name (optional)
  text: string;                  // Message text
  replyTo?: string;              // ID of message being replied to
  timestamp: number;             // Unix ms
  metadata?: Record<string, unknown>;  // Platform-specific extras
}

OutboundMessage

interface OutboundMessage {
  text: string;                  // Response text (markdown)
  replyTo?: string;              // Original message ID
  metadata?: Record<string, unknown>;  // Platform-specific extras (e.g. parse_mode)
}

ChannelAdapter

interface ChannelAdapter {
  readonly name: string;
  readonly status: ChannelStatus;

  /** Start the adapter (connect to platform, begin listening). */
  connect(): Promise<void>;

  /** Stop the adapter (disconnect, clean up). */
  disconnect(): Promise<void>;

  /** Send a message to a specific peer. */
  send(peerId: string, message: OutboundMessage): Promise<void>;

  /** Register the inbound message handler. Called by registry. */
  onMessage(handler: (msg: InboundMessage) => void): void;

  /** Register a tool event handler for displaying tool execution status. */
  onToolEvent?(handler: (peerId: string, event: ToolStatusEvent) => void): void;
}

type ChannelStatus = 'disconnected' | 'connecting' | 'connected' | 'error';

interface ToolStatusEvent {
  type: 'start' | 'end';
  tool: string;
  args?: unknown;
  result?: { success: boolean; output: string; error?: string };
}

ChannelRegistry

class ChannelRegistry {
  register(adapter: ChannelAdapter): void;
  unregister(name: string): void;
  get(name: string): ChannelAdapter | undefined;
  list(): ChannelAdapter[];

  /** Start all registered adapters. */
  startAll(): Promise<void>;

  /** Stop all registered adapters. */
  stopAll(): Promise<void>;

  /** Set the message handler that all adapters route to. */
  setMessageHandler(handler: (msg: InboundMessage, reply: (msg: OutboundMessage) => Promise<void>) => Promise<void>): void;
}

Telegram Adapter Design

The TelegramAdapter wraps the existing createTelegramBot() logic:

  • connect(): Creates grammy Bot, starts long polling
  • disconnect(): Stops the bot
  • send(): Calls bot.api.sendMessage(peerId, text, { parse_mode: 'Markdown' })
  • onMessage(): Sets up bot.on('message:text', ...) to convert grammy context to InboundMessage
  • Preserves existing confirmations, commands (/start, /reset, /status, /local, /cloud, /model)
  • Preserves chat ID allowlist check as middleware
  • Tool status display: adapter handles the onToolUse events by posting/editing Telegram messages

Constructor takes:

interface TelegramAdapterConfig {
  botToken: string;
  allowedChatIds: number[];
  hookEngine?: HookEngine;
}

The adapter does NOT take an agent directly — the registry routes messages to the agent.

WebChat Adapter Design

The WebChatAdapter is a thin shim since the gateway already handles WS connections.

  • connect(): No-op (gateway server is already running)
  • disconnect(): No-op (gateway lifecycle managed by daemon)
  • send(): Sends via the gateway's WS connection to the peer
  • onMessage(): Hooks into the gateway's agent.send handler to intercept messages

Constructor takes:

interface WebChatAdapterConfig {
  gateway: GatewayServer;
}

This adapter is simpler because the gateway already has its own session bridge and agent management. The adapter primarily exists to:

  1. Report WebChat as a registered channel in the registry
  2. Allow the daemon to manage all channels uniformly
  3. Provide status/metrics via a common interface

Daemon Integration

The daemon currently:

  1. Creates a grammy Bot directly
  2. Creates a GatewayServer directly
  3. Starts both independently

After refactor:

  1. Creates a ChannelRegistry
  2. Creates TelegramAdapter + WebChatAdapter
  3. Registers both with the registry
  4. Registry starts all adapters

The message handler in the registry creates per-channel agents via the session manager, same as the existing session bridge pattern.

Implementation Order

  1. src/channels/types.ts — Pure types (no runtime)
  2. src/channels/registry.ts — Registry class
  3. src/channels/registry.test.ts — Registry unit tests
  4. src/channels/telegram/adapter.ts — Telegram adapter
  5. src/channels/telegram/adapter.test.ts — Telegram adapter tests
  6. src/channels/webchat/adapter.ts — WebChat adapter
  7. src/channels/webchat/adapter.test.ts — WebChat adapter tests
  8. src/channels/index.ts + sub-barrel exports
  9. src/daemon/index.ts — Wire registry
  10. Run full test suite

Existing Code Impact

  • src/frontends/telegram/NOT deleted. The adapter wraps these existing modules.
  • src/gateway/NOT modified. WebChat adapter wraps the existing gateway.
  • src/daemon/index.tsModified to use ChannelRegistry.
  • src/backends/native/agent.tsNOT modified. Agent creation happens in the registry message handler.

Test Strategy

  • Unit tests for ChannelRegistry (mock adapters)
  • Unit tests for TelegramAdapter (mock grammy Bot)
  • Unit tests for WebChatAdapter (mock GatewayServer)
  • Existing tests remain unchanged (frontends/telegram, gateway)

Plan Version: 1.0 Created: 2026-02-05 Parent: docs/plans/2026-02-05-openclaw-parity-design.md Phase 3