ADR 0179 — Цикл спрашивает Jev, какой инструмент, а модель пользователя видит только оставшееся меню
Статус
Accepted. 2026-09-29 (AT-238, история HS-063, эпик EP-029). Реализует спецификацию AT-235 с шестнадцатью решениями мейнтейнера из AT-236 (2026-09-28) и решением от 2026-09-29 о критерии принятия, ниже. Не заменяет ни одного ADR: добавляет деталь вне контракта provider из ADR 0041 и каталога моделей из ADR 0042 и переиспользует metering, бюджет (ADR 0110) и учётные данные владельца (RN-058), не меняя их.
Контекст
Сегодня инструмент каждого шага агента выбирает чат-модель
пользователя, глядя на каталог из 7–19 определений при каждом вызове.
Измерения с 2026-09-27 по 29 (docs/explanation/medicao-do-jev.md) поставили Jev
— модель typesafe/jev-1.13 из Decisions API OpenRouter, отвечающую на
вопрос с множественным выбором с вероятностями, — против 328 реальных, уже
записанных шагов:
- top-1 Jev: 73% (66–79%), ниже 90%, которых AT-237 требовала, чтобы Jev ВЫБИРАЛ сам. Выбирать и принуждать — не годится.
- покрытие меню политики P3: 91% (85,1–94%) — доля шагов, в которых инструмент, который агент реально вызвал, находится ВНУТРИ меню {выбор Jev, предыдущий инструмент того же прогона}. Top-2 Jev покрывает 90%. Покрытие — ПОТОЛОК сквозной точности (точности чат-модели, теперь по 1,6 инструмента вместо 7,4), а не сама точность.
- P3 предлагает ОДИН-единственный инструмент в 98 из 160 шагов валидации, а определения, которые модель читает за вызов, падают с 746 до 138 токенов.
Jev, следовательно, годится для ограничения меню, а не для решения за него — и это то, что AT-236 уже решила (ответ 12: Jev только ограничивает).
Решение мейнтейнера о критерии (2026-09-29)
Критерием принятия AT-237 была нижняя граница доверительного интервала 90%. Ни одна политика её не достигла (P3: 85,1%). 2026-09-29 мейнтейнер снизил критерий до 80%, имея перед собой числа выше, и велел реализовать P3. Это решение владельца, а не находка: нижняя граница P3 (85,1%) превышает 80%, и ни один другой текст этого репозитория не должен говорить, что принятие «прошло 90%». То, чего измерение НЕ доказывает, остаётся объявленным:
- покрытие — потолок, а сквозная точность, измеренная в replay, лежала между 62% и 91% в зависимости от гипотезы о чат-модели;
- интервал Уилсона трактует 160 шагов как независимые, а это 10 прогонов — честный интервал шире;
- реальная проверка — AT-239, вживую, со сравнением шагов, задержки и стоимости хода с Jev и без. Этот ADR поставляет механизм, включённый по умолчанию, как велела AT-236, с выключателем по workspace под рукой.
Решение
1. Маршрутизатор живёт в api, внутри llm-turn, за собственным портом.
ToolRouter (apps/api/src/application/ports/tool-router.port.ts) — не
LLMProvider и не находится в LLMProviderRegistry: Jev решает, не разговаривает,
не проходит через набор контрактных тестов provider и не входит в каталог
models. Адаптер (JevToolRouter) обращается к
POST https://openrouter.ai/api/alpha/decisions в формате, ИЗМЕРЕННОМ в
scripts/jev/jev.ts, — questions является объектом с ключом по id вопроса
(заметка AT-235 писала список, и API ответил 400). Единственный use
case, DecidirFerramentaDoPassoUseCase, вызывается ОБОИМИ use cases
хода (RunLlmTurnUseCase и StreamLlmTurnUseCase) после того, как разрешённый provider
и gate бюджета пройдены, и до provider.chat. Семь
разговорных агентов, ToolLoop dev agents и gates и Infra Lead
все проходят через них (AT-236, ответ 2); вне остаются только Anamnese (приостановлена) и
суммаризатор сжатия.
2. Когда к нему обращаются — все условия, и только тогда есть вызов:
provider модели хода — openrouter (AT-236, ответ 1, прочтение (a));
запрос несёт два или более инструмента; в ходе есть агент; engine не запросил
весь каталог; workspace не выключил. Ключ Jev — ТОТ ЖЕ, уже
расшифрованный для чата, — ключ владельца workspace (RN-058), и никакого нового чтения
учётных данных не появляется. При provider ≠ openrouter ни один вызов не уходит, даже
чтение workspace.
3. Политика — P3, и это только ограничение. Меню шага —
{инструмент, выбранный Jev, предыдущий инструмент того же прогона}, в порядке
каталога. Если Jev ответит responder_sem_ferramenta или в каталоге шага не будет
предыдущего инструмента, меню — весь каталог
(AT-236, ответ 12: «catálogo inteiro, só registra»). tool_choice нет: лишь
ограничивается меню, модель всё ещё может ответить текстом (ответ 7).
В v1 нет порога уверенности — измеренная P3 его не использует, ответ AT-236
4 велел убрать его из измерения, а измерение P3 его не производит. Уверенность
ЗАПИСЫВАЕТСЯ на каждом шаге, и предлагать порог — дело AT-239 (по агенту, если кривые
различаются).
4. state — это то, что у продукта реально есть до шага — поля
варианта escopo, победителя второго раунда, вырезанные внутри api из
messages, которые engine уже отправляет, без чтения базы: agente (agentId),
contexto (начало первого сообщения system, 1.500 символов), pedido
(последнее сообщение user) и passos_recentes (6 последних вызовов
инструментов ПОСЛЕ этого сообщения, аргумент и результат обрезаны до 500). Потолок
8 тысяч токенов на запрос, четверть окна Jev; превышение — падение
estado_grande. Два отличия от измеренного, объявленные: contexto — это
начало настоящего system, а не «identidade do ator + instrução», как в
replay; а у dev agent в исправлении pedido — сообщение исправления,
последний user, а не kickoff.
5. Ход НИКОГДА не падает из-за Jev. Ошибка HTTP (включая 404/410 у
изменившегося alpha-endpoint), таймаут (потолок 2.000 мс, TOOL_ROUTER_TIMEOUT_MS;
0 из 328 запросов его превысили при измерении), сетевая ошибка, неожиданный JSON,
выбор вне вариантов, большое состояние, коллизия имени с зарезервированным вариантом:
provider получает весь каталог, а причина идёт в событие с источником
RN-059 (infra для трёх первых, modelo для двух о форме,
codigo для наших). Адаптер никогда не бросает, а use case ещё
защищает себя try/catch последней инстанции.
6. Политика не меняется. Jev не одобряет, не отказывает и не выбирает модель.
Инструмент, оставшийся в меню, по-прежнему становится Proposed Action и проходит через
decide() и абсолютные потолки как всегда; маршрутизатор даже не импортирует эти
модули, и тест engine утверждает, что его модуль их не знает. Если
модель вызовет инструмент, которого НЕ было в меню шага (вспомнив его из
истории), engine отправит его как сегодня, а событие лишь зафиксирует
foraDoCardapio (AT-236, ответ 13: Jev сужает, а не запрещает). Потолок
итераций остаётся нетронутым: вызов Jev находится ВНУТРИ того же llm-turn, а цикл
увеличивает счётчик только после отправки.
7. Запись по шагу, чтобы AT-239 могла измерить. Engine записывает
tool_router.decided в фасаде EngineApiClient (одно место для семи
серверов и ToolLoop) с меню до и после, выбором, уверенностью, второй
опцией, предыдущим инструментом, aplicado, motivoDaQueda, origemDaQueda,
задержкой, стоимостью и foraDoCardapio. Метрика берётся скриптом по логу и по
token_usage, никогда не записывается вручную. Api возвращает шаг в новом необязательном поле
toolRouting у llm-turn и у кадра final потока; старый engine его игнорирует.
8. Расходы на Jev — настоящие расходы. Одна строка token_usage через тот же
RecordLlmUsageUseCase (единственный путь metering, с бюджетом проекта,
сессии и ОБЛАСТИ): actor — сам агент (ответ 10 — с собственным актором
расходы ускользнули бы от бюджета области ADR 0110), provider = openrouter,
modelId = null, modelName = typesafe/jev-1.13 (именно по нему вкладка Gastos
отделяет Jev от чата, ответ 8), estimated = false и costMicros =
usage.cost ОТВЕТА, в микро-USD. Цена за миллион неявная
(custo ÷ tokens самого ответа, ответ 11), и строка говорит это в новой колонке
price_implicit, сохраняя tokens × preço = custo (RN-044), не притворяясь, что
существует каталожная цена. Gate бюджета не перепроверяется между Jev
и чатом (ответ 15). Стоимость Jev возвращается в engine в
toolRouting.custoMicros, и ToolLoop прибавляет её к своему локальному бюджету, не
трогая usage. Metering Brabo для ЧАТА регистрирует меньше, чем
берёт OpenRouter (каталожная цена, без кэша и рассуждения); metering Jev от этого не
страдает, потому что использует стоимость, которую возвращает ответ.
9. Выключатель принадлежит WORKSPACE (ответ 3), колонка
workspaces.tool_router_enabled, по умолчанию включено, миграция 0066, изменяется через
PUT workspaces/:workspaceId/tool-router (owner, владелец ключа, который платит).
Api читает флаг из собственной базы на пути хода; engine ничего не читает и не
делает лишних HTTP. При выключении ни один вызов Jev не уходит и ни одно событие не рождается.
Переменная установки TOOL_ROUTER=jev|off, которую предлагала AT-235, НЕ появилась:
решение мейнтейнера было по workspace. Появилась только TOOL_ROUTER_TIMEOUT_MS.
10. Путь, по которому агент может запросить полный каталог, — AT-281 показала, что
у 61% шагов было бы меню из ОДНОГО инструмента, а неверное меню не может стать
тупиком. Механизм — самый простой, не открывающий новой двери сдерживания: если
меню было ограничено (aplicado) и модель ответила без вызова инструмента
(даже текстом, восстановимым по ADR 0020) и без ошибки provider, фасад
повторяет шаг один раз с catalogoCompleto: true, что заставляет api пропустить
Jev. Повтор просит БОЛЬШЕ инструментов, никогда тот, которого у агента не было;
api не доверяет engine ни в чём, кроме «не ограничивай меня»; меню никогда
не было границей безопасности. В потоке разговорных агентов повтор происходит, только если НИЧЕГО
не было написано человеку (доставленный delta не отменить). Стоимость
повтора — два вызова чата, оба с metering; toolRouting первого
остаётся помеченным repetidoComCatalogoInteiro. Отброшено:
request_full_toolset как новый инструмент (это новая поверхность инструмента в
каждом агенте, а у модели с меню из одного инструмента нет причин его вызывать) и
report_blocked как сигнал (он завершает цикл).
Последствия
- Каждый подходящий шаг получает последовательный вызов (Jev, затем чат): +~0,2–0,4 с задержки (записанные строки измерения) и ~US$ 0,00005 за шаг. Выигрыш — меньше определений за вызов и, возможно, меньше шагов — гипотеза, и AT-239 его измеряет.
- Когда Jev ошибается и меню оставляет модель без нужного инструмента, цена —
лишний круг (пункт 10). Частота этого повтора есть в событии
(
repetidoComCatalogoInteiro), и это метрика того, что меню плохое. - Endpoint — ALPHA. Если он изменится, ручной smoke
(
jev-tool-router.smoke.spec.ts, потолок US$ 0,05, пропускается безOPENROUTER_TEST_KEY) провалится, а в production каждый шаг упадёт на весь каталог сerro_httpилиresposta_invalidaв событии — выключатель по workspace существует на случай, если падение окажется слишком шумным. - Таймаут на стороне Brabo мог быть оплачен OpenRouter без того, чтобы
мы увидели
usage.cost: эти расходы не попадают вtoken_usage. Объявлено, не измерено. - Ключ один на установку workspace, а не на агента и не на проект.
Чего этот ADR НЕ делает
- Не измеряет выигрыш (AT-239) и не калибрует порог; порога в v1 нет.
- Ничего не показывает на экране. AT-236 (ответ 3) хочет, чтобы экран показывал, что
шаг был маршрутизирован через Jev, с уверенностью, и (ответ 16), чтобы полоса
описывала «escolhendo ferramenta». Данные существуют (событие и кадр
tool_routing_startedпотока, который engine игнорирует как неизвестный тип); экран и полоса — работаapps/web, вне этой lane. - Не меняет серверы агентов сверх того, что уже покрывает фасад, не маршрутизирует Anamnese и не помещает Jev в каталог моделей.
- Не решает ни о повышении потолка 2.000 мс, ни об экосистеме Dependabot
для пина
typesafe/jev-1.13(подъём через PR, ответ 5).