Behaviour
How a role becomes a model. Read this when something bound to a route you did not expect,
when axon prepare refused, or before you rely on ordering to control spend.
Resolution happens once, at boot
Every role a cognet declares is bound before its first wake. Nothing is chosen lazily, so a call site can never block on resolution and can never resolve differently on the second tick than the first.
The steps, in order:
- Gather. Every declared provider is asked for its catalogue, concurrently. A source that cannot be reached contributes a visible failure, never a quietly shorter list.
- Reject. For each role, every candidate is tested against the role's structural requirements. Failures are kept with a reason.
- Rank. Whatever survives is ordered by preference.
- Bind. The top candidate becomes the role's engine.
Roles are resolved independently, so one model may fill several. A machine with one good local model runs a cognet declaring three roles — slower, still correct.
What can reject a candidate
Only structural facts — things that make a model unable to serve the role, never things that make it worse at it.
| Requirement | Rejected when |
|---|---|
type | The handle shape differs — a transform model cannot fill a generate role |
in / out | A modality the cognet will send or expects back is unsupported |
context | The model's window is smaller than the role's floor |
structured | The role needs parseable output and the model does not guarantee it |
Quality is deliberately absent. A weaker model still parses, still fits, and still runs, so which one to use is your tradeoff — never the cognet's to refuse.
Two rules about missing information, because they cut opposite ways. An absent context
window means unknown, and does not exclude the model: sources that report no window
would otherwise vanish from every role that names one. An absent structured flag
means no: a model that cannot be relied on to emit parseable blocks breaks the grammar,
and guessing optimistically fails at the first tick instead of at prepare.
How candidates are ranked
Four axes, in order. Each only decides when everything above it ties.
1. Local first. A model on your own machine outranks any hosted route. You already expressed the preference by installing it, and it is the choice that keeps the agent working with the network off.
2. Wider context. More headroom for the same job.
3. More slots. Same reasoning, for concurrency.
4. The order you declared your providers in.
That last one is what decides who pays. One model is often reachable several ways —
anthropic/claude-sonnet-4.6 might be available through Anthropic() with your key,
OpenRouter() with your key, and Axon() on your balance. Those three candidates are
identical on every axis above: same model, so the same context; all hosted, so none local;
no slots ceiling unless you set one.
So your list settles it:
providers: [Anthropic(), Axon()] // your key first, managed as the fallback
providers: [Axon(), Anthropic()] // managed first
Without that rule the winner would be whichever catalogue happened to be appended first — an arbitrary choice with a real bill attached.
Ordering is a tie-break, not an override. [Anthropic(), Ollama()] still binds the
local model when both can serve a role, because local ranks above declaration order. To
force a specific route, pin it: model: "anthropic:claude-sonnet-4-6".
Pins prefer, they never require
A model: string reorders candidates that already satisfy the role. It cannot promote one
that does not fit, and it cannot fail a boot.
A pin naming something you cannot supply is recorded rather than swallowed — the agent runs on whatever ranking chose, and tells you the pin was not honoured and why:
no "anthropic" provider is declared — the model exists on another routeno declared provider supplies "some-model"
The distinction is the actionable part: one means connect a provider, the other means the id is wrong.
This is what keeps a published agent runnable by someone who does not share its author's providers. An author's pin is a preference on their setup; on yours it degrades to a suggestion.
When a role cannot be filled
A role with no surviving candidate is unmet. What that means depends on the role:
- Required (the default) — the agent cannot run.
axon preparestops and reports it. - Optional — ordinary. The cognet checks
kernel.engine.has(name)and takes a cheaper path, which is how compression, filtering and attention degrade on a machine with one local model and no network.
An unmet role reports every near miss with its reason, so the message is
your models fit everything except the 100k context rather than unresolved.
When a catalogue is unreachable
A provider that cannot answer is a failure, not an empty list. This matters more than it sounds: a dead Ollama daemon and a machine with no local models produce identical catalogues, and only the failure entry tells them apart. Without it an agent silently falls back to the cloud for a role you intended to run locally — a billing surprise and a privacy one.
The public model catalogue behaves the same way. If it cannot be fetched, a small built-in list of widely available models stands in so that routes which never depended on it keep working — a provider being down must not empty routes that do not touch it. The gap is reported rather than hidden, and models served only from the fallback carry no managed route, because a price that was never fetched must not be invented.
Rebinding while running
The primary role is rebindable without a restart. Pressing * in the terminal and picking
a model rebuilds that role's engine in place: the new one is constructed before the old
reference is dropped, so a caller mid-dispatch never sees a gap.
Rebinding chooses among what you already declared. It never reaches for a provider you do not have.