One gateway, many agents. The binding table decides who handles what — without touching the agent logic.

The Big Picture

Chapter 4 (Channels) can receive messages from many channels. Chapter 5 answers: “which agent handles this message?”

InboundMessage (channel, peer_id, text)
       │
 BindingTable.resolve()
   T1: peer_id      ← most specific
   T2: guild_id     // guild_id = Discord's term for a server
   T3: account_id
   T4: channel
   T5: default      ← catch-all
       │ first match wins
       ▼
   agent_id
       │
build_session_key() ──> AgentManager (personality, model)
       │
    LLM API

Three Key Concepts

1. BindingTable — Routing Rules

bt.Add(Binding{AgentID: "luna", Tier: 5, MatchKey: "default", MatchValue: "*"})
bt.Add(Binding{AgentID: "sage", Tier: 4, MatchKey: "channel", MatchValue: "telegram"})
bt.Add(Binding{AgentID: "sage", Tier: 1, MatchKey: "peer_id", MatchValue: "discord:admin-001"})
Input Tier Agent
channel=cli, any peer T5 default Luna
channel=telegram, any peer T4 channel Sage
channel=discord, peer=admin-001 T1 peer_id Sage
channel=discord, other peer T5 default Luna

The most specific rule wins. This lets you route a specific user to a specific agent, entire channels to another agent, and fall back to a default for everything else.

2. dm_scope — Session Isolation

Session scope controls whether two different users talking to the same agent share one conversation history or each get their own.

"main"                     → all conversations share 1 session
"per-peer"                 → isolated per user ← recommended
"per-channel-peer"         → different session per platform
"per-account-channel-peer" → maximum isolation; useful when you run the same agent on multiple bot accounts — e.g., two different Telegram bots — and want each bot's users to be fully separated

Use per-peer for production: each person gets their own session, but the knowledge base is shared per agent.

3. AgentConfig — Personality per Agent

type AgentConfig struct {
    ID          string
    Name        string
    Personality string // default: ""
    Model       string // default: ""; empty = use global model
    DMScope     string // default: "per-peer"
}

This gets expanded into an 8-layer dynamic system prompt in Chapter 6 (Intelligence).

Real-World Pattern: Route by User Role

SSO (Single Sign-On) gives you the user’s identity. RBAC (Role-Based Access Control) tells you their role. Together, they let you route different users to different agents in a web app.

For web-based deployments with authentication:

SSO (Google, etc.) → user_id, email
    ↓
RBAC lookup → division, role, permissions
    ↓
resolve_agent(profile) → agent_id

Simple implementation:

var divisionAgentMap = map[string]string{
    "ops":   "ops-agent",
    "infra": "infra-agent",
}
var roleOverride = map[string]string{
    "vp":       "executive-agent",
    "director": "executive-agent",
}

func resolveAgent(userProfile map[string]string) string {
    if agent, ok := roleOverride[userProfile["role"]]; ok {
        return agent
    }
    if agent, ok := divisionAgentMap[userProfile["division"]]; ok {
        return agent
    }
    return "general-agent"
}

This does the same job as a BindingTable but expressed as plain Go maps — easier to read for a web app where roles and divisions come from your user database.

Takeaway

  • BindingTable — routing rules, 5 tiers, first match wins
  • Tier 1 (peer_id) is most specific, Tier 5 (default) is catch-all
  • dm_scope — how isolated sessions are → use per-peer for production
  • AgentConfig — personality + model per agent → foundation for Chapter 6 (Intelligence)