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