Маршрутизатор моделей в наборе для языка выбирается специальным идентификатором и обязательным параметром цели оптимизации, и это не украшение вызова, а часть контракта. Автоматический выбор без параметров означает другое: сервер подбирает модель сам, как запасной вариант, и нигде не объявлено, ради чего он это делает. Маршрутизатор требует назвать цель, и подбор становится функцией от этого объявления. Разница видна не в описании, а в поведении: недоступный маршрутизатор даёт ошибку при создании агента, а запасной автоматический выбор молча срабатывает и оставляет впечатление, что всё настроено как задумано.
Отсюда обязательный шаг перед созданием агента - получить каталог моделей и убедиться, что нужная возможность в нём есть. Маршрутизатор может быть отключён администратором команды или ограничен списком разрешённых моделей; в обоих случаях ваш код формально корректен, а среда - нет. Проверять стоит не только идентификатор: у параметра цели есть набор допустимых значений, и нужное вам значение может отсутствовать даже тогда, когда сам маршрутизатор доступен. Один вызов каталога превращает непонятный отказ в середине рабочего процесса в понятное сообщение о недоступности на старте.
Полезно один раз увидеть такую проверку целиком. Ниже - получение каталога, поиск нужного идентификатора и нужного значения параметра, явная ошибка при отсутствии и только затем создание агента с маршрутизатором и включённой изоляцией. Обратите внимание на порядок: сначала выясняем, что возможность есть, потом ей пользуемся. Обратный порядок даёт сбой в самом неудобном месте - когда очередь задач разобрана, входные данные подготовлены, а до первого ответа модели остаётся один шаг. Объявление агента через конструкцию с автоматическим освобождением тоже не стилистика: она закрывает ресурсы запуска даже при исключении внутри блока.
Стоит понимать, что вы покупаете этим параметром и чем платите. Цель оптимизации сдвигает компромисс между качеством ответа, задержкой и стоимостью, и маршрутизатор применяет её к каждому запросу отдельно. Выигрыш в том, что вам не приходится вручную сопровождать список моделей и переписывать код при появлении новых. Плата в том, что вы перестаёте знать, кто именно ответил на конкретный запрос. Для потока однотипных продуктовых задач это приемлемо. Для разбирательства постфактум - нет: объяснить расхождение между двумя одинаковыми по формулировке запусками будет попросту нечем.
Есть тонкость, которая ловит при длинных сценариях: переопределение модели на конкретный запуск остаётся действующим. Механизм простой - переопределение меняет текущий выбор агента, а не параметры одного сообщения, поэтому следующие отправки без явного указания продолжают работать с выбранной моделью. Пока переопределение стоит в линейном коде, это удобно. Когда оно спрятано в условной ветке, агент после какого-то шага начинает вести себя иначе, и в тексте диалога причина не видна. Если поведение должно быть предсказуемым, модель указывают на каждой отправке или заводят под другой режим отдельного агента.
По той же причине маршрутизатор не годится для замеров и сравнений. Он оптимизирует каждый запрос под цель, а состав пула и выбранная модель могут различаться между вызовами, в том числе между двумя соседними прогонами одного сценария. В замере это означает, что разница результатов смешивает два источника: ваше изменение и смену исполнителя. Для воспроизводимого эксперимента фиксируют конкретный идентификатор модели - тогда разница означает разницу в изменениях. То же правило действует для регрессионных наборов и для сравнения формулировок запросов.
Дальше начинается обычная эксплуатация, и её требования полезно свести в таблицу с минимальной реализацией каждого. Ниже такая карта: таймауты и отмена, повторы только на идемпотентной границе, хранение идентификаторов и смещений событий, учёт стоимости и ограничение параллельности, изоляция и узкие ключи, типизированный результат с доказательствами, обновление с закреплённой версией и контрактными тестами. Каждая строка отвечает на вопрос о том, что произойдёт при сбое, а правая колонка задаёт нижнюю границу, а не идеал.
| Производственное требование | Минимальная реализация |
|---|---|
| Таймаут и отмена | Крайний срок на запуск и явная отмена |
| Повторы | Только на идемпотентной границе с нарастающей задержкой |
| Состояние | Идентификаторы агента и запуска, смещения событий в надёжном хранилище |
| Стоимость | Расход на запуск, бюджет и ограничение параллельности |
| Безопасность | Песочница, обработчики событий, узкий ключ, политика исходящего трафика |
| Качество | Типизированный результат с доказательствами и независимые проверки |
| Обновление | Закреплённая версия, каталог запросом, контрактные тесты |
Строка про повторы заслуживает расшифровки, потому что нарушается чаще прочих. Повтор запуска агента не бесплатен и не безобиден: агент уже мог создать ветку, оставить комментарий, обратиться во внешнюю систему и потратить бюджет. Идемпотентная граница означает, что повторяется не работа целиком, а операция с внешним ключом и защитой от дублирования. Ради этого в надёжном хранилище держат идентификаторы агента и запуска вместе со смещением обработанных событий: после сбоя процесс продолжает чтение с известной точки, а не начинает всё заново. Признак, по которому промах узнают в реальной работе, всегда один - дубликаты: две ветки, два комментария, удвоенный расход.
Инженерный вывод простой: оркестрация агентов ничем не отличается от оркестрации любых длительных внешних операций. Как только вы это принимаете, набор решений становится знакомым - те же таймауты, то же хранение состояния, тот же учёт денег, - и агент перестаёт быть особым случаем в архитектуре. Особым он остаётся только в одном: его результат надо проверять, а не принимать, потому что успешное завершение запуска ничего не говорит о качестве изменений.
Типичные провалы предсказуемы. Считать автоматический выбор без параметров тем же самым, что и маршрутизатор. Не проверить доступность возможности до запуска. Сравнивать модели через маршрутизатор и получить несравнимые прогоны. Повторять запуск целиком вместо идемпотентной операции. И забыть, что переопределение модели остаётся действующим для последующих отправок.
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 } },
});