Skip to main content
A route’s profile: field keys memory and session state per tenant on every turn, so one gateway multiplexes many tenants and their conversation histories never cross.
gateway.yaml
Two channels, two tenants, one agent — acme’s chat history and globex’s chat history are stored under separate keys and never leak into each other.

Quick Start

1

Bind two tenants to the same agent

Give each route a different profile: value. The same support agent serves both tenants, but each route names its own memory namespace.
2

Read the resolved profile in code

resolve_route() copies the winning binding’s profile onto the RouteMatch, so the gateway knows which tenant namespace to enter for the turn.
3

Isolate a whole tenant server in one line

Bind profile: on a guild_id rule to isolate a whole tenant server — every current and future channel in the guild inherits the namespace.
A channel-level route also covers the threads beneath it — see Parent-chain matching.
4

Leave a route unscoped

Omit profile: (or leave it blank) and the route stays unscoped — match.profile is None and its memory is never namespaced.

How It Works

resolve_route() picks the most-specific matching binding and copies its profile onto the returned RouteMatch; the gateway then enters that namespace before the agent runs, and every storage key for the turn is prefixed with profile:<name>:. An unmatched route yields RouteMatch(profile=None) and stays unscoped.

What the Profile Isolates Today

Only memory/session state is namespaced by the profile today; the other dimensions named on the routing protocol are separable follow-ups.

Memory / session state

Shipped (PR #4343). Every storage key for the turn is prefixed with profile:<name>:, so two routes multiplexed on one process never share a transcript.

Secrets / model / instructions

Still follow-up. These dimensions are not yet consumed from the profile. Do not rely on the profile to scope secrets, override a model, or swap instructions.

Storage-key example

The tenant name appears as profile:<name>: at the head of the key, so you can reason about exactly what is isolated.

When To Use a Profile


Fail-Closed Contract

Tenant memory isolation is fail-closed at runtime — a misconfigured route can never silently share another tenant’s transcript.
  • Blank, whitespace-only, or None profiles stay unscoped and never borrow another tenant’s namespace — enforced turn-locally, so clearing a profile returns the storage key to its unscoped form (never sticky).
  • Concurrent turns — two routes overlapped on one process — each see their own tenant namespace; there is no cross-tenant bleed under load, even when turns interleave.
  • An unmatched route yields RouteMatch(profile=None) and stays unscoped, even when the fallback agent is the same.
Memory/session isolation shipped in PR #4343: the profile now keys memory per tenant on every routed turn. Secret-scope, model-override, and instruction-override remain separable follow-ups — the profile does not yet touch them.
Each concurrent turn keeps its own tenant scope for the duration of the turn and clears it afterward, so overlapping routes never share a namespace and a stale scope never leaks into a later turn.

Configuration Options

At runtime the resolved profile flows into BotSessionManager’s storage-key derivation on every routed turn — Discord and Slack via _routed_message_handler, Telegram via handle_message — so the memory namespace is applied before the agent runs.

Best Practices

Use one stable, tenant-specific value per route (acme, globex). Never reuse a profile name across tenants — that would merge their conversation histories.
When match.profile is None, do not fall back to a “default tenant” that another route also uses. Unscoped means isolated, not shared.
Route tenant profiles on channel_id or account — stable platform ids — rather than display names, which change.
Profile names come from your config, not from message content. Never derive a profile from a field a user can set.
Confirm a tenant is isolated by inspecting the head of its storage key — the tenant name must appear as profile:<name>: before the base id (e.g. profile:acme:u1). An unscoped route shows no prefix (u1).

Route Bindings

The full routing surface — match by peer, role, channel, account, and priority.

Session Persistence

How profile-scoped routes persist under a namespaced session key across restarts.

Scoped Approvals

Durable, agent-scoped approval grants that don’t leak across agents.