ADR 0064 — Область как уровень каскада модели и глобальный binding агента
- Статус: принят
- Дата: 2026-08-09
- Предыдущий контекст: ADR 0041
(capabilities в два слоя и перепроверка
resolveBindingпри спуске на уровень), ADR 0053 (области как данные по проекту,agent_areas/agent_area_members), FASE 18 (область рождается вместе с проектом, RN-094)
Контекст
Запрос пользователя: "para leads de áreas e seu subgrupo estarem em conjunto, dando a possibilidade de escolher um mesmo modelo vigente para ambos." Его решение пришло вместе с запросом — модель области является наследуемым умолчанием: конкретный агент может расходиться, а UI показывает, кто наследует и кто разошёлся.
Каскад binding (domain/llm/binding-resolver.ts) сегодня разрешает
сессия > агент > проект > workspace, по одному binding на область видимости, побеждает самый
конкретный. agent_areas/agent_area_members (ADR 0053, FASE 18)
уже являются реальными данными по проекту — lead, субагенты и предел параллелизма. Не хватает
того, чтобы МОДЕЛЬ области вошла в этот же каскад.
Это поднимает вопрос позиции: между какими двумя уровнями входит область? А второй, который появляется только при попытке ответить на первый, — суть этого ADR.
Несогласованность: binding агента ГЛОБАЛЕН, область — ПО ПРОЕКТУ
SetModelBindingUseCase.execute('agent', agentSlug, ...) записывает с
scope_id = agentSlug — SLUG, без проекта. PUT /projects/:projectId/agent-bindings/:agentSlug получает :projectId в маршруте и
явно его отбрасывает: комментарий в коде уже говорил, что это
намеренно. Выбор модели Arquiteto на экране одного проекта меняет его
модель во ВСЕХ проектах, где он есть.
Область, напротив, существует по проекту со времён ADR 0053 — один и тот же qa двух
разных проектов может иметь разные пределы и (теперь) разные модели.
Помещение области видимости ПО ПРОЕКТУ над ГЛОБАЛЬНОЙ в одном каскаде — это противоречие по существу, а не только по стилю: один и тот же агент разрешал бы разные модели по проекту только там, где существует настроенная область, и одинаковые во всех остальных — поведение, зависящее от случайности данных, а не от правила. Есть два выхода, взаимоисключающих:
- Binding агента становится по проекту. Меняется поведение, которое существовало с Фазы 9a.
- Область стоит НИЖЕ агента в каскаде. Агент остаётся глобальным, но это противоречит "наследуемому умолчанию" — область никогда не смогла бы быть умолчанием для агента, у которого уже есть binding (а это обычный случай, потому что СЕЙЧАС почти каждый зарегистрированный binding — это binding агента, между ним и проектом нет другого уровня).
Решение
Выбран вариант 1: binding агента становится по проекту.
Каскад получает уровень area, между agent и project:
sessão > agente > área > projeto > workspace
Область — это УМОЛЧАНИЕ, которое разделяют lead и субагенты области; binding самого агента — это явное РАСХОЖДЕНИЕ, которое его перекрывает. Именно этот ПОРЯДОК — а не простое существование уровня — и есть выполняемое решение пользователя: если бы область стояла над агентом, она побеждала бы всегда, и "наследуемое умолчание" было бы на практике "навязанным умолчанием".
Чтобы позиция имела смысл без противоречия глобальной области видимости над
областью по проекту, binding agent должен был перестать быть глобальным.
scope_id для agent и area стал составным:
<projectId>:<slug агента | ключ области>. Без новой таблицы: UUID
проекта и slug/ключ никогда не содержат :, что делает первое :
однозначным разделителем (domain/llm/binding-scope-id.ts), а старый формат
(без :) отвергается при записи — его запись создала бы binding, который
каскад больше никогда не нашёл бы, невидимый вместо ошибки.
Миграция 0040 разворачивает каждый существующий глобальный binding агента в одну строку на проект, сохраняя модель, которую каждый проект разрешал до изменения, и удаляет старый формат. Именно разворачивает, а не выбирает проект- "владельца": глобальная строка никогда не хранила информацию о том, кому принадлежала.
Новый уровень входит в ту же уже существующую перепроверку capability (ADR
0041/RN-041/RN-043): модель области, исчезнувшая у provider или не умеющая tool
calling, пропускается и регистрируется в skipped, ровно как уже происходило с
agent. area также стала требовать supports_tool_calling
(assertModelFitsBindingScope) — её никогда не читает человеческий чат, только
агент, и пропуск её отложил бы тот же тихий сбой на уровень.
"Вернуться к наследованию" удаляет binding, никогда не копирует модель с нижнего уровня
на верхний. DELETE /projects/:id/agent-bindings/:slug и DELETE /projects/:id/area-bindings/:key, оба 204 и оба 404, когда область видимости
уже наследует. Запись в агента модели, решённой областью, на
экране выглядела бы так же, но не является тем же: она стала бы копией, и следующее изменение области оставило бы этого агента
позади молча — наследование есть ОТСУТСТВИЕ собственного решения.
Изменение модели ОБЛАСТИ требует роли maintainer, а не developer, как у
отдельного агента, — по той же причине, что и предел параллелизма (RN-083):
binding области достигает lead и всех субагентов сразу, а выбор
модели — это решение о том, сколько продукт тратит, без вопроса. Binding агента
остаётся в developer, как и было.
UI (AreaModelsSection в ProjectSettingsTab.tsx) показывает умолчание каждой
области рядом с таблицей агентов; колонка Origem таблицы агентов получает
"voltar a herdar", когда origin === 'agent' — агент расходится с областью,
если она у него есть, или с проектом/workspace, если нет.
Последствия
- Каскад растёт с четырёх до пяти уровней, и каждый потребитель, который
перечислял четыре (тесты, DTO,
ORIGIN_TONEв UI), потребовал пятого. Это разовая цена; структура (перепроверка capability,skipped, явный источник) уже существовала и только расширилась. - Binding агента перестал быть глобальным. Кто зависел от глобального slug
— три скрипта seed/demo — стал записывать по проекту
(
chaveDeAgente(projectId, slug)). Больше нет способа задать "одну модель для этого агента везде"; кому это нужно, настраивает по проекту или вworkspace(который остаётся глобальным и является fallback для всех). - У области нет собственной таблицы binding — она переиспользует
model_bindingsсscope = 'area'по той же причине, по которойagentвсегда переиспользовал: нет атрибута binding, зависящего от типа области видимости, только отscope_id. - Составной
scope_id— неявный формат, не подкреплённый ограничением базы (этоtext). Валидация живёт в одной функции (assertScopeIdBemFormado), и именно она — а не схема — препятствует фантомному binding. Если когда-нибудьarea/agentполучат собственную таблицу, этот формат станет еёid, аscope_idисчезнет. - Наследование по умолчанию области не достигает субагентов вне каталога:
область агента разрешается статическим каталогом (
agent-areas.ts), который уже покрывает динамическую областьdevпредикатомehDevDeModuloбез обращения к базе — ни одного нового запроса кagent_areasне понадобилось.