Your agent doesn’t need to know whether a message came from Telegram, Discord, or Slack. That’s the channel layer’s job.

The Big Picture

In Chapter 1–3 (The Agent Loop, Tool Use, Sessions & Context Guard), the agent only works over CLI. Chapter 4 adds multi-platform support — but the agent loop doesn’t change. Every platform is normalized into one format before reaching the agent.

Telegram ──┐
Discord  ──┼──> InboundMessage ──> Agent Loop (unchanged)
Slack    ──┤
CLI      ──┘

Agent Loop ──> reply ──> channel.send() ──> original platform

Three Key Concepts

1. InboundMessage — Universal Format

type InboundMessage struct {
    Text     string   // message content
    SenderID string   // who sent it
    Channel  string   // "telegram", "discord", "slack", "cli"
    PeerID   string   // where to send the reply (DM vs group)
    IsGroup  bool     // is this a DM or group message?
    Media    []any    // attachments if any
}

The peerID tells the agent where the reply should go — for a DM it’s the user, for a group it’s the chat. This is how one bot can run multiple separate conversations at once.

peer_id determines which session to load:

Telegram has three conversation types, each needing a different ID so threads don’t mix.

  • Telegram DM: user_id
  • Telegram group: chat_id
  • Telegram topic: chat_id:topic:thread_id

2. Channel Interface — A 2-Method Contract

Adding a new platform = implement two methods:

type Channel interface {
    Receive() (*InboundMessage, error) // receive a message
    Send(to, text string) bool         // send a reply
}

Analogy: All platforms speak different languages. InboundMessage is the universal translator. The agent speaks only one language.

3. Session Key — One Agent, Many Conversations

sessionKey := fmt.Sprintf("%s:%s:%s", channel, accountID, peerID)
// "telegram:bot123:user456"   → DM from user 456
// "discord:bot001:channel111" → Discord channel

conversations[session_key] = messages[] for that conversation. Same pattern as Chapter 3 (Sessions & Context Guard), but per-channel per-user.

Platform-Specific Constraints

Each channel has different constraints your agent needs to respect:

Platform Max message length Supports markdown? Threading
Telegram 4096 chars Yes (limited) Topics
Discord 2000 chars Yes Threads
Slack ~3000 chars Yes (mrkdwn) Threads
CLI unlimited Terminal N/A

This is why chunking messages (Chapter 8 — Delivery) matters — chunking = splitting a long reply into smaller pieces that fit within the platform’s character limit — the delivery layer needs to know the platform limits.

Takeaway

  • InboundMessage — normalize all platforms into one format
  • Channel Interface — 2 methods: receive() + send()
  • Session key — channel + account + peer_id → each conversation is isolated
  • The agent loop doesn’t change — the channel layer wraps around it