Перейти к основному содержимому

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).