Files
orchestrator/docs/work-items/ORCH-074/06-adr/ADR-001-model-name-validation.md

13 KiB
Raw Permalink Blame History

ADR-001: Убрать мёртвый frontmatter model: + валидация имени модели через формат-чек claude-*

Work Item ID: ORCH-074 Эпик: ORCH-052 (слой 3), под-задача ORCH-52a Связан с: ORCH-041 (каркас resolve_agent_model/resolve_agent_effort), src/config.py, src/agents/launcher.py

Статус

Accepted

Контекст

Каркас выбора модели агентов (ORCH-041) работает корректно: launcher.resolve_agent_model(agent, project_id) резолвит модель по приоритету project-override → ORCH_AGENT_MODEL_<AGENT>agent_model_default → CLI-дефолт. Все 6 агентов резолвятся в claude-opus-4-8 (через agent_model_default).

Аудит кода (08.06) выявил два дефекта данных/валидации (не дефект механизма):

  • P1 — лживый/мёртвый model: во frontmatter. Все 6 промптов .openclaw/agents/*.md содержат model: (claude-sonnet-4-6 у analyst/developer/tester/deployer, claude-opus-4-7 у architect/reviewer). launcher не читает frontmatter model: — это мёртвая декларация, которая лжёт о реально используемой модели и нарушает принцип «документация = golden source». Мина: если кто-то «починит» launcher читать frontmatter → все агенты молча уедут на устаревшие модели.
  • P2 — нет валидации имени модели. В отличие от effort (VALID_EFFORTS-гард в resolve_agent_effort), имя модели не валидируется. Опечатка в agent_model_* / project-override → --model <мусор> → CLI падает или тихо деградирует. Нарушение принципа never-break.

Скоуп зафиксирован стейкхолдером (Слава, 08.06): G1 + G2 + опц. G4. G3 routing НЕ включаем (все 6 агентов остаются claude-opus-4-8). Эффорт не трогаем. rev.2 BRD подтвердила скоуп без изменений. Код-факт (TRZ §4): agent_fallback_model читается напрямую на launcher.py:374, минуя resolve_agent_model.

Архитектор должен зафиксировать три решения: (а) форма G1, (б) предикат валидации G2, (в) судьба G4 (fallback) и G3 (routing).

Решение

Решение 1 (G1): убрать model: из frontmatter, НЕ учить launcher его читать

Из YAML-frontmatter всех 6 файлов .openclaw/agents/*.md удаляется только строка model: …. Ключи name/description/tools сохраняются; frontmatter остаётся валидным YAML и описательным. config (agent_model_* / agent_model_default) остаётся единственным источником правды о модели.

Отвергнутая альтернатива — научить launcher читать frontmatter model: — отвергнута: она вводит второй источник правды (frontmatter ⊕ config), усложняет резолв, и моментально активировала бы устаревшие значения (sonnet-4-6 / opus-4-7) для всех агентов. «Убрать» проще, безопаснее и устраняет мину раз и навсегда.

Решение 2 (G2): предикат валидации — формат-чек claude-*, оформленный отдельным helper

Добавляется чистый helper is_valid_model(name: str) -> bool рядом с VALID_EFFORTS в src/agents/launcher.py. Предикат — формат-чек, а не allowlist имён:

strip → непустая строка → соответствует ^claude-[a-z0-9.-]+$

То есть: имя после strip() непусто, начинается с claude- и состоит только из строчных букв/цифр/точек/дефисов. Регэксп оформляется модульной константой (напр. _MODEL_NAME_RE).

Почему формат-чек, а не allowlist VALID_MODELS: allowlist (по образцу VALID_EFFORTS) воссоздаёт ровно ту мину, которую мы убиваем в G1 — статичный список имён, который врёт при устаревании. Когда Anthropic выпустит claude-opus-4-9, оператор, корректно прописавший новую модель, получит её молчаливый дроп на устаревший default (never-break сработает против пользователя). Это хуже, чем пропустить структурно-корректное, но опечатанное имя: финальный авторитет о существовании модели — сам Claude CLI, а не наш код. Формат-чек forward-compatible (новые версии проходят без правки кода) и ловит реальные классы отказов: чужой провайдер (gpt-4), пустая строка/пробелы, мусор с недопустимыми символами, неверный префикс (claud-opus-typo). Признанное ограничение: формат-чек НЕ ловит опечатку, которая всё ещё выглядит как валидное claude-имя (claude-opus-typo) — такие отсекает CLI на запуске (контракт never-break

  • exit-code обработка в _monitor_agent это покрывают). Задача валидатора — не быть реестром моделей, а не дать структурному мусору уехать в --model.

Применение (контракт never-break):

  • В resolve_agent_model: резолвенное имя валидируется перед возвратом. Невалидное → logger.warning(...) + откат на следующий валидный уровень. Реализация: helper применяется внутри каскада приоритетов так, что невалидный уровень пропускается (project-override невалиден → пробуем env → default), а если итог всё равно невалиден → возврат "" (без флага --model, CLI-дефолт). Никогда не возвращается мусор и никогда не бросается исключение.
  • Контракт уровней резолва ORCH-041 сохраняется: валидация добавляется поверх, порядок приоритетов и сигнатуры не меняются. Все ныне используемые валидные имена (claude-opus-4-8, валидный enduro per-project override) проходят без изменения поведения.
  • Поведенческая аналогия с resolve_agent_effort (VALID_EFFORTS): валидный → как есть, невалидный → лог + дроп. Разница только в форме предиката (формат-чек vs множество) по причинам выше.

Решение 3 (G4): fallback НЕ включаем; но валидатор применяем к точке чтения fallback

agent_fallback_model остаётся "" (флаг --fallback-model не прокидывается). AC-5 помечается N/A. Обоснование отказа:

  • G3 выключен ради детерминизма: все агенты на claude-opus-4-8. Fallback вернул бы скрытую вариативность модели под нагрузкой (агент молча отработал бы на другой модели) — это противоречит духу зафиксированного скоупа.
  • Нет наблюдаемой проблемы доступности, мотивирующей fallback. Принцип минимального изменения.
  • Self-hosting: новое рантайм-поведение под нагрузкой трудно наблюдать; не вводим без нужды.

При этом helper is_valid_model применяется ТАКЖЕ на месте чтения fallback (launcher.py:374, fb = settings.agent_fallback_model) — независимо от того, что значение сейчас пустое. Причина — код-факт TRZ §4: fallback читается напрямую, мимо resolve_agent_model, поэтому валидация только внутри резолва его НЕ покрывает. Защитный гард на месте чтения навсегда закрывает дыру never-break: если кто-то позже задаст ORCH_AGENT_FALLBACK_MODEL с опечаткой, мусор будет залогирован и сброшен (fb_flag = ""), а не уедет в --fallback-model. Для текущего пустого значения регрессии нет: is_valid_model("") == Falsefb_flag = "" — то же поведение, что и сейчас (if fb). Это делает TC-11 проверяемым (мусорный fallback дропается) при выключенном G4.

Решение 4 (G3): routing НЕ включаем

Подтверждается отказ от model-routing как осознанное решение стейкхолдера (Слава, 08.06). Все 6 агентов резолвятся в claude-opus-4-8. AC-4 = N/A.

Размещение и форма (для разработчика)

  • is_valid_model(name) + _MODEL_NAME_RE — в src/agents/launcher.py рядом с VALID_EFFORTS (один валидатор, два места вызова: резолв модели и чтение fallback — оба в этом модуле, без кросс-модульного импорта).
  • Префикс claude- хардкодится в launcher: оркестратор привязан к Claude CLI (CLAUDE_BIN), конфигурировать предикат не нужно (не over-engineering). Каноничная версия модели по-прежнему живёт ТОЛЬКО в config.py::agent_model_default — в launcher версия не хардкодится.
  • frontmatter: удалить только model:-строку; не вносить генератор, возвращающий её обратно.

Последствия

Плюсы:

  • frontmatter перестаёт лгать; config — единственный источник правды о модели (golden source цел).
  • Опечатка/чужой провайдер/мусор в имени модели больше не роняет и не деградирует запуск агента (never-break соблюдён в обеих точках: резолв и fallback).
  • Forward-compatible: будущие модели Claude не требуют правки кода (в отличие от allowlist).
  • Минимальное изменение: механизм ORCH-041, API, схема БД, структура CLI-команды не меняются.

Минусы / ограничения:

  • Формат-чек пропускает структурно-валидную опечатку вида claude-opus-typo (отсекается CLI на запуске + never-break обработкой exit-code). Принятый компромисс ради forward-compat.
  • Префикс claude- зашит — при гипотетической смене CLI-провайдера потребуется правка (приемлемо: оркестратор Claude-специфичен по дизайну).

Не затрагивается:

  • API (HTTP) — нет. Схема БД — нет миграций. Стадии/QG — без изменений (это runtime-гард в launcher, не quality-gate). Топология/инфра — без изменений (07/08 артефакты не требуются).
  • Эффорт (agent_effort_*) и VALID_EFFORTS-гард — не трогаются (регрессия покрыта TC-10).
  • enduro per-project override — валидные имена проходят без изменения поведения (AC-8 / TC-08).

Соответствие принципам

Всё в Docker / один сервер — да. Минимум зависимостей — новых нет. Без ORM/очередей/облака — да. Self-hosting: изменение применяется к БУДУЩИМ запускам агентов, прод-контейнер не перезапускается в рамках задачи; прод-деплой орка — только через staging-гейт (8501) и Plane-статус «Confirm Deploy».