← Home

SmolPaws Discord Entry Point live

How the Discord ingress uses the shared channel adapter to dispatch messages to the agent server.
Source: apps/discord/  ·  Adapter pattern: channelAdapter.ts

Architecture

The Discord ingress is built on the BaseChannelAdapter pattern — a shared base class that handles agent-server dispatch, while the DiscordAdapter subclass implements Discord-specific I/O.

flowchart LR
    A["Discord\nGateway"] -->|WebSocket| B["DiscordAdapter\n(discord.js)"]
    B -->|"dispatch()"| C["BaseChannelAdapter\n(shared)"]
    C -->|HTTP POST| D["agent-server\n127.0.0.1:8788"]
    D -->|runs agent| E["OpenHands\nruntime"]
    E -->|turn result +\noutbound msgs| D
    D -->|poll + claim| C
    C -->|"sendReply()"| B
    B -->|reply| A
    

The Adapter Pattern

Two files, clear separation:

FileLinesResponsibility
index.ts~40Entry point — imports adapter, starts via registry, handles shutdown
adapter.ts~300DiscordAdapter — all Discord-specific logic
channelAdapter.ts (shared)~300BaseChannelAdapter + ChannelRegistry — agent-server dispatch loop

What the base class handles

What the Discord adapter implements

Message Flow

sequenceDiagram
    participant U as Discord User
    participant D as Discord API
    participant DA as DiscordAdapter
    participant BA as BaseChannelAdapter
    participant S as agent-server

    U->>D: message or @mention
    D->>DA: MessageCreate (WebSocket)
    DA->>DA: shouldRespond() + isAllowed()
    DA->>DA: extractPrompt() + buildConversationId()
    DA->>BA: dispatch(msg, replyCtx)
    BA->>BA: sendTyping() + start interval
    BA->>S: submitConversationMessage()
    loop poll until terminal
        BA->>S: getTurnStatus()
        BA->>S: claimOutboundMessages()
        BA->>DA: sendReply() for each outbound
        DA->>D: message.reply()
    end
    BA->>S: getTurnResult()
    BA->>DA: sendReply(finalReply)
    DA->>D: message.reply()
    D->>U: sees reply
    

Entry Decision Logic

shouldRespond()

1. Ignore bot messages
2. Respond if bot is @mentioned
3. Respond if trigger pattern matches (default: @smolpaws)
4. Respond to all DMs
5. Otherwise: ignore

isAllowed()

1. If ALLOWED_USERS set → user must be in the list
2. DMs always pass (after user check)
3. If ALLOWED_GUILDS set → guild must be in the list
4. If ALLOWED_CHANNELS set → channel must be in the list

Conversation ID Generation

ContextFormatExample
DMdiscord-dm-{author_id}discord-dm-456789012
Threaddiscord-thread-{channel_id}discord-thread-987654321
Channeldiscord-channel-{channel_id}discord-channel-111222333

Channel Registry

The adapter self-registers with the ChannelRegistry on import. The entry point is minimal:

import { channelRegistry } from '../../../src/shared/channelAdapter.js';
import './adapter.js'; // triggers registration

await channelRegistry.startAdapter('discord', {
  runnerUrl: RUNNER_URL,
  runnerToken: RUNNER_TOKEN,
  logger,
});

The registry provides:

Shared Infrastructure

All ingress apps share the same adapter base and turn client:

flowchart TB
    subgraph "Ingress Apps"
        D["Discord\nDiscordAdapter"]
        S["Slack\n(next)"]
        G["GitHub\n(next)"]
    end
    subgraph "Shared Layer"
        BA["BaseChannelAdapter\nchannelAdapter.ts"]
        T["turnClient.ts"]
        R["ChannelRegistry"]
    end
    subgraph "Backend"
        A["agent-server"]
    end
    D --> BA
    S -.-> BA
    G -.-> BA
    BA --> T
    R --> BA
    T --> A
    

Discord is the first adapter on the pattern. Slack and GitHub will follow the same shape — subclass BaseChannelAdapter, implement platform I/O, register with the registry.

Configuration

VariableRequiredDefaultPurpose
DISCORD_BOT_TOKENyesDiscord bot token
SMOLPAWS_RUNNER_URLnohttp://127.0.0.1:8788Agent server base URL
SMOLPAWS_RUNNER_TOKENnoAuth token for agent server
DISCORD_TRIGGERno@smolpawsText trigger pattern
DISCORD_ALLOWED_GUILDSnoComma-separated guild IDs
DISCORD_ALLOWED_CHANNELSnoComma-separated channel IDs
DISCORD_ALLOWED_USERSnoComma-separated usernames

Design Context

This adapter pattern is the first step toward a plugin system for channel extensibility. The goal: adding a new messaging platform should be “subclass BaseChannelAdapter, implement 3 methods, register with the registry” — not “copy an existing app and rewire everything.”

See smolpaws#115 for the full extensibility roadmap.

SmolPaws Slack →  ·  Hermes Plugin API →  ·  ← Home