Learning
Multi-armed and contextual bandits — Thompson sampling and LinUCB — that learn which recipient wins from outcomes.
Learning strategies don't score a fixed blend of signals — they learn which
recipient wins by trying candidates and observing outcomes. They balance
exploration (trying uncertain recipients to gather data) against exploitation
(routing to known winners). They derive their state from durable
outcome-learning evidence, and all of them live in the optional predicted
contrib module.
| Strategy | Type | Capabilities | Aliases |
|---|---|---|---|
| Thompson sampling | Bayesian bandit | explain, stateful | bandit |
| LinUCB | Contextual bandit | explain, stateful | — |
RegisterPredicted (modules/strategies/predicted/register.go) registers
predicted_value, thompson_sampling, linucb, and the bandit alias for
thompson_sampling; the factories themselves live in
pkg/strategy/builtin/predicted/.
Module-gated: enable it and promote a deployment
These strategies are not in the built-in pack. To use them the predicted
contrib module must be enabled and backed by a promoted deployment; until
then any recipe that references them reports missing_dependencies. They require
the outcome and quality repositories, and — because their learned posteriors must
survive restarts — a BanditStateStore.
Learned state lives in the state plane, not the snapshot
A bandit's posteriors are mutable strategy state, not decision inputs. They
live in the routing-managed strategy state plane
(Redis-backed via NewRedisBanditState for the fast path, Postgres as authority)
— the same plane that holds SWRR weights and fair-catchup windows. That is a
different plane from the read-only feature snapshot
they read as context. Selection reads a state snapshot and emits a post-commit
mutation intent; it never writes state inside Select.
Thompson sampling
Capabilities: explain, stateful · Aliases: bandit
A classic multi-armed bandit. It treats each recipient as an "arm" with a posterior distribution over its reward, samples from each posterior, and routes to the arm with the highest sample. Arms it is uncertain about have wide posteriors and therefore occasionally win the sample — that is exploration, and it is automatic rather than a fixed epsilon.
Why use it. When you don't know in advance which recipient is best and you want the system to find out with minimal regret. Thompson sampling is famously sample-efficient: it converges on winners quickly while still probing the field, and it self-corrects when a recipient's performance drifts. Ideal for lead routing, offer selection, and any assignment where "best" is discovered, not declared.
How to configure it. No product-surface sliders — behavior comes from the
per-arm posteriors it maintains. It is stateful: posteriors are updated from
committed outcomes and persisted through the bandit state store. It advertises
explain, so it can report the sampled values behind a decision.
Where it fits. The Select stage. Because it is deliberately
non-deterministic (its determinism profile is exploratory bandit), its
strategy contract requires a
learning-dataset governance requirement — the platform will not certify an
exploratory strategy without one.
LinUCB
Capabilities: explain, stateful
A contextual bandit. Where Thompson sampling learns one reward distribution per recipient, LinUCB learns a linear model per arm over the routable's features, plus a confidence bound. It answers a sharper question: not "who is best overall?" but "who is best for this kind of routable?"
Why use it. When the right recipient depends on the request. A recipient who excels at high-value enterprise leads may be mediocre on small self-serve ones; LinUCB learns those conditional patterns from features and routes accordingly, using its upper confidence bound to keep exploring arms it hasn't seen enough of under the current context.
How to configure it. No product-surface sliders. It consumes routable
features and maintains per-arm model state (also stateful, also durable through
the state store). It advertises explain.
Where it fits. The Select stage. It reads the typed
feature snapshot as its
context vector, which is also what makes its decisions replayable for audit and
offline evaluation.
Behavior change: contextual exploration now uses features
LinUCB's exploration step now receives each candidate's own feature vector as its context. Previously exploration was handed an empty vector, so the contextual model could not influence which arm was explored and selection silently degraded to uniform random. With per-candidate features flowing through, LinUCB's confidence bounds now actually steer exploration — expect its exploratory picks to reflect the routable's features rather than being effectively random.
Choosing between them
Rule of thumb
Use Thompson sampling when a recipient's quality is roughly the same across all requests — you just need to find the best. Use LinUCB when quality depends on the request's features and you want per-context winners. Both need enough traffic to learn; on a cold or tiny pool, a scoring strategy with a confidence gate is safer.