The model router in a language package is selected by a special identifier and a mandatory optimization goal parameter, and that is not decoration of the call but part of the contract. Automatic selection without parameters means something else: the server picks a model on its own, as a fallback, and nowhere is it declared what it does so for. The router requires naming the goal, and the selection becomes a function of that declaration. The difference shows in behavior rather than in the description: an unavailable router raises an error when the agent is created, while the fallback automatic choice quietly succeeds and leaves the impression that everything is configured as intended.
Hence a mandatory step before creating an agent - fetch the model catalog and make sure the capability you need is in it. The router may be disabled by a team administrator or restricted by an allowlist of models; in both cases your code is formally correct and the environment is not. It is worth checking more than the identifier: the goal parameter has a set of permitted values, and the value you need may be absent even when the router itself is available. One catalog call turns an obscure refusal in the middle of a production process into a clear message about unavailability at the start.
It helps to see such a check in full once. Below are fetching the catalog, looking for the required identifier and the required parameter value, an explicit error if it is missing, and only then creating an agent with the router and isolation enabled. Note the order: first find out that the capability exists, then use it. The reverse order fails in the most inconvenient place - when the task queue has been drained, the inputs are prepared, and one step remains before the model's first answer. Declaring the agent through the construct with automatic disposal is not a matter of style either: it closes the run's resources even when an exception is thrown inside the block.
It is worth understanding what you buy with that parameter and what you pay. The optimization goal shifts the tradeoff between answer quality, latency and cost, and the router applies it to each request separately. The gain is that you do not have to maintain a list of models by hand and rewrite code when new ones appear. The price is that you stop knowing who exactly answered a particular request. For a stream of uniform production tasks that is acceptable. For an investigation after the fact it is not: there will simply be nothing to explain a divergence between two runs identical in wording.
There is a subtlety that catches people in long scenarios: overriding the model for a specific run remains in effect. The mechanism is simple - the override changes the agent's current selection rather than the parameters of one message, so subsequent sends without an explicit choice keep working with the selected model. While the override sits in linear code, that is convenient. When it is hidden in a conditional branch, the agent starts behaving differently after some step, and the cause is not visible in the text of the conversation. If behavior must be predictable, the model is specified on every send or a separate agent is created for the other mode.
For the same reason the router is unsuitable for measurements and comparisons. It optimizes every request for a goal, and the pool's composition and the chosen model may differ between calls, including between two adjacent runs of the same scenario. In a measurement that means the difference in results mixes two sources: your change and a change of performer. For a reproducible experiment a specific model identifier is pinned - then a difference means a difference in the changes. The same rule applies to regression suites and to comparing the wording of requests.
After that ordinary operation begins, and its requirements are worth laying out in a table with a minimal implementation of each. Below is that map: timeouts and cancellation, retries only at an idempotent boundary, storing identifiers and event offsets, accounting for cost and capping concurrency, isolation and narrow keys, a typed result with evidence, upgrades with a pinned version and contract tests. Every row answers the question of what happens on failure, and the right column sets a lower bound rather than an ideal.
| Production concern | Minimal implementation |
|---|---|
| Timeout and cancellation | A deadline on the run and explicit cancellation |
| Retries | Only at an idempotent boundary with growing delay |
| State | Agent and run identifiers, event offsets in durable storage |
| Cost | Per-run usage, a budget and a concurrency cap |
| Security | The sandbox, event handlers, a narrow key, an egress policy |
| Quality | A typed result with evidence and independent checks |
| Upgrades | A pinned version, catalog discovery, contract tests |
The row about retries deserves unpacking, because it is broken more often than the rest. Repeating an agent run is neither free nor harmless: the agent may already have created a branch, left a comment, called an external system and spent budget. An idempotent boundary means that what is repeated is not the whole job but an operation with an external key and protection against duplication. That is why durable storage keeps the agent and run identifiers together with the offset of processed events: after a failure the process continues reading from a known point rather than starting over. The sign by which the mistake is recognized in real work is always the same - duplicates: two branches, two comments, doubled spend.
The engineering conclusion is simple: orchestrating agents is no different from orchestrating any long-running external operations. Once you accept that, the set of decisions becomes familiar - the same timeouts, the same state storage, the same accounting for money - and the agent stops being a special case in the architecture. It remains special in only one respect: its result must be verified rather than accepted, because a run finishing successfully says nothing about the quality of the changes.
The typical failures are predictable. Treating automatic selection without parameters as the same thing as the router. Not checking a capability's availability before launch. Comparing models through the router and getting incomparable runs. Repeating a whole run instead of an idempotent operation. And forgetting that a model override stays in effect for subsequent sends.
import { Cursor, Agent } from "@cursor/sdk";
const models = await Cursor.models.list();
const router = models.find((model) => model.id === "auto-smart");
const parameter = router?.parameters?.find((item) => item.id === "optimize_for");
if (!router || !parameter?.values.some((v) => v.value === "balanced")) {
throw new Error("Balanced Cursor Router is unavailable");
}
await using agent = await Agent.create({
apiKey: process.env.CURSOR_API_KEY!,
model: {
id: "auto-smart",
params: [{ id: "optimize_for", value: "balanced" }],
},
local: { cwd: process.cwd(), sandboxOptions: { enabled: true } },
});