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 → useper-peerfor production- AgentConfig — personality + model per agent → foundation for Chapter 6 (Intelligence)