| 1 | # ADR 0083: `settings.toml` — дискриминант `ai.mode` и вложенные секции (local / acp / mcp_only / cloud) |
| 2 | |
| 3 | **Статус:** Accepted · Implemented |
| 4 | **Дата:** 2026-04-20 |
| 5 | |
| 6 | ## Связанные ADR |
| 7 | |
| 8 | | ADR | Роль | |
| 9 | |-----|------| |
| 10 | | [0028](0028-user-settings-toml-localappdata-and-secrets.md) | путь и секреты | |
| 11 | | [0038](0038-agent-facade-ai-provider-and-tool-orchestration.md) | фасад провайдеров и инструментов | |
| 12 | | [0048](0048-cursor-acp-chat-ide-parity-and-mcp-tool-surface.md) | ACP, MCP в сессии | |
| 13 | | [0016](0016-agent-client-protocol-external-agent.md) | ACP как внешний агент | |
| 14 | | [0081](0081-parametric-intent-melodies-editor-line-ranges.md) | параметрические Intent Melody `:start:end` для редактора — смежная тема, не часть `ai.mode` | |
| 15 | ## Резюме |
| 16 | |
| 17 | - Секция **`[ai]`** в `settings.toml`: `mode` = local | acp | mcp_only | cloud. |
| 18 | - Вложенные секции; **без** обратной совместимости со старым `provider`. |
| 19 | |
| 20 | |
| 21 | --- |
| 22 | |
| 23 | <a id="adr0083-context"></a> |
| 24 | |
| 25 | --- |
| 26 | ## Контекст |
| 27 | |
| 28 | Секция **`[ai]`** в пользовательском TOML сейчас смешивает несколько независимых осей: |
| 29 | |
| 30 | - **Какой контур отвечает за текст ассистента** (локальный Ollama, облачный API, Cursor ACP). |
| 31 | - **Флаг «не звать встроенный LLM, ждать ответ только из MCP»** (`chat_mcp_only`) — по смыслу это **отдельный режим взаимодействия**, а не «ещё один провайдер». |
| 32 | |
| 33 | Читаемость файла и документации страдает: приходится держать в голове комбинации `provider` + булевых флагов. |
| 34 | |
| 35 | --- |
| 36 | |
| 37 | <a id="adr0083-decision"></a> |
| 38 | |
| 39 | ## Решение |
| 40 | |
| 41 | <a id="adr0083-mode-discriminant"></a> |
| 42 | |
| 43 | ### 1. Дискриминант верхнего уровня |
| 44 | |
| 45 | В **`[ai]`** вводится обязательное (для канона) поле: |
| 46 | |
| 47 | | TOML-ключ | Смысл | |
| 48 | |-----------|--------| |
| 49 | | `mode` | Один из: **`local`**, **`acp`**, **`mcp_only`**, **`cloud`**. | |
| 50 | |
| 51 | **Почему не голое `mcp`:** слово совпадает с **протоколом и подключёнными MCP-серверами** (инструменты в сессии ACP, внешние stdio-серверы и т.д.). Значение режима чата должно читаться как **источник ответа ассистента**, а не как «MCP включён». Каноническое имя режима — **`mcp_only`**: встроенный LLM после отправки пользователя **не** вызывается; текст ассистента приходит **только** из контура внешнего MCP (например `send_chat` с ролью assistant). Семантически то же выражали бы `assistant_via_mcp` или `no_builtin_provider`; в TOML выбираем одно короткое **`mcp_only`**, синонимы в коде/доках не обязательны. |
| 52 | |
| 53 | Семантика: |
| 54 | |
| 55 | | Значение | Назначение | |
| 56 | |----------|------------| |
| 57 | | **`local`** | Встроенный локальный провайдер: **`[ai.local]`** с **`backend = "ollama"`** (и далее другие значения) плюс **`[ai.local.ollama]`** с полями модели/endpoint. Вложенная таблица даёт **короткие ключи** на листе (`model`, `base_url`). Другие локальные бэкенды — отдельные `backend` и `[ai.local.<name>]`. | |
| 58 | | **`acp`** | Чат через **Cursor Agent / ACP** (`cursor-agent`): пути, `model_id`, политика **подмешивания** MCP-серверов в сессию — в подсекции ACP; это **не** то же самое, что режим `mcp_only`. | |
| 59 | | **`mcp_only`** | Режим **только ответа через MCP** для текста ассистента: встроенный провайдер (Ollama/облако) в этом смысле **не** используется. Подключённые MCP-сервера как инструменты могут существовать и в `local` / `acp` / `cloud` — это ортогонально. | |
| 60 | | **`cloud`** | Облачные провайдеры с API-ключами (Anthropic, OpenAI-совместимые, DeepSeek и т.д.): выбор провайдера и учётные данные — во вложенных таблицах. | |
| 61 | |
| 62 | <a id="adr0083-nested-tables"></a> |
| 63 | |
| 64 | ### 2. Вложенные таблицы (направление) |
| 65 | |
| 66 | Ключи и точная вложенность фиксируются при реализации; нормативный **скелет**: |
| 67 | |
| 68 | ```toml |
| 69 | [ai] |
| 70 | mode = "local" # local | acp | mcp_only | cloud |
| 71 | |
| 72 | # Поля, общие для любого режима (пример): размещение UI настроек, флаги истории — по мере необходимости. |
| 73 | |
| 74 | # Локальный режим: явный backend и вложенная таблица с тем же именем (v1 — ollama). |
| 75 | [ai.local] |
| 76 | backend = "ollama" # ollama | openai_compatible | … (канон значений фиксируется при реализации) |
| 77 | |
| 78 | [ai.local.ollama] |
| 79 | # model — идентификатор модели в API Ollama; base_url, request_timeout — при необходимости. |
| 80 | |
| 81 | [ai.acp] |
| 82 | # cursor_acp_path, cursor_acp_model_id, политика acp_auto_inject_ide_mcp (подмешивание MCP-серверов в сессию агента). |
| 83 | |
| 84 | [ai.mcp_only] |
| 85 | # Лимиты/флаги, специфичные для режима «без встроенного LLM, ответ ассистента только из MCP». |
| 86 | |
| 87 | [ai.cloud] |
| 88 | # Поле active_provider = "anthropic" | "openai" | "deepseek" (или иной канон списка). |
| 89 | |
| 90 | # Предпочтительно: фиксированные вложенные таблицы по провайдеру (не массив [[...]]): |
| 91 | [ai.cloud.anthropic] |
| 92 | # model, base_url (если отличается), ссылки на ключ в ai-keys.toml — при реализации. |
| 93 | |
| 94 | [ai.cloud.openai] |
| 95 | |
| 96 | [ai.cloud.deepseek] |
| 97 | ``` |
| 98 | |
| 99 | Плагинируемый каталог облачных провайдеров (массив `[[...]]`, динамическая загрузка и т.п.) **не входит в scope** этого ADR и **отложена**; v1 — только фиксированный набор вложенных таблиц выше. |
| 100 | |
| 101 | **Именование:** в TOML — **`snake_case`**, согласовано с `CascadeTomlSerializer` / [0028](0028-user-settings-toml-localappdata-and-secrets.md). Вложенность **`[ai.local.ollama]`** / **`[ai.cloud.*]`** задаёт контекст: на листе достаточно **`model`**, **`base_url`**, без длинных префиксов в имени ключа. Для выбора локального **сервиса/API** в **`[ai.local]`** каноническое поле — **`backend`**; синоним **`engine`** не используем (путается с рантаймом инференса и с **`model`**). |
| 102 | |
| 103 | **Локальный режим: «тип» vs модель.** Поле вроде **`model_type`** в смысле *формата весов* (GGUF, SafeTensors, …) в пользовательский **`settings.toml` не выносим** — это не контракт IDE. Нужны два уровня: |
| 104 | |
| 105 | 1. **`backend`** в **`[ai.local]`** — строковый дискриминант: **`ollama`**, позже **`openai_compatible`** и т.д. Должен **совпадать** с активной подтаблицей **`[ai.local.<backend>]`** (например `backend = "ollama"` ↔ `[ai.local.ollama]`). Так читаемость в корне локальной секции не зависит только от пути; вложенная таблица по-прежнему несёт короткие ключи (`model`, …). |
| 106 | 2. **`model`** — строковый **идентификатор модели в API выбранного движка** (например `qwen2.5-coder:7b` для Ollama), не путать с «типом файла модели». |
| 107 | |
| 108 | **Что кроме Ollama (продуктово, не обязательно v1):** тот же абстрактный **OpenAI-compatible HTTP** (как у облака), но с `base_url` на localhost — типично **LM Studio**, **llama.cpp** в режиме совместимости с OpenAI API, **vLLM**, **LocalAI** и т.д. В схеме это отдельная подтаблица, например **`[ai.local.openai_compatible]`** с `model`, `base_url`, при необходимости ссылкой на ключ в `ai-keys.toml` (редко для локали). Реализация и тесты — после v1/Ollama; плагинируемый реестр движков — по-прежнему отложен. |
| 109 | |
| 110 | <a id="adr0083-secrets"></a> |
| 111 | |
| 112 | ### 3. Секреты |
| 113 | |
| 114 | По-прежнему **не** хранить ключи API в `settings.toml`: отдельный **`ai-keys.toml`** (см. [0028](0028-user-settings-toml-localappdata-and-secrets.md)). В ADR только **какие поля** ссылаются на ключи по имени/слоту. |
| 115 | |
| 116 | --- |
| 117 | |
| 118 | <a id="adr0083-backcompat"></a> |
| 119 | |
| 120 | ## Обратная совместимость |
| 121 | |
| 122 | **Не требуется.** Старые ключи вида `provider`, `chat_mcp_only`, плоские `default_ollama_model`, облачные URL в корне `[ai]` **снимаются** с канона: миграция — ручная правка файла и обновление сэмплов в репозитории (`docs/samples/settings*.toml`). При желании в коде допускается **одноразовый** скрипт/док «как переписать старый файл» вне обязательной автомиграции в рантайме. |
| 123 | |
| 124 | --- |
| 125 | |
| 126 | <a id="adr0083-consequences"></a> |
| 127 | |
| 128 | ## Последствия |
| 129 | |
| 130 | 1. **`AiSettings` и связанный VM** перестраиваются под дискриминант + вложенные типы; сериализация Tomlyn должна воспроизводить вложенные таблицы предсказуемо. |
| 131 | 2. **UI настроек AI** (ComboBox провайдера и т.д.) маппится на `mode` и подсекции вместо плоского списка `Ollama|…|CursorACP`. |
| 132 | 3. **Документация и сэмплы** — единый пример с `[ai].mode` и вложенными блоками. |
| 133 | 4. **Тесты** — контракт десериализации TOML для новой формы; удаление тестов на старые ключи. |
| 134 | |
| 135 | --- |
| 136 | |
| 137 | <a id="adr0083-rejected"></a> |
| 138 | |
| 139 | ## Отклонённые / отложенные альтернативы |
| 140 | |
| 141 | - **Оставить только `provider` + флаги** — отклонено как менее читаемое для пользователя и для ADR. |
| 142 | - **Режим с именем `mcp`** — отклонено: неоднозначно с MCP как протоколом и списком подключённых серверов; канон — **`mcp_only`** (см. пояснение выше; `assistant_via_mcp` / `no_builtin_provider` — смысловые синонимы, не отдельные значения в TOML). |
| 143 | - **Автомиграция при загрузке** — не делаем по требованию; снижает сложность кода и скрытые сюрпризы. |
| 144 | - **Плагины и расширяемый реестр облачных провайдеров** — отложено; канон v1 — фиксированные `[ai.cloud.anthropic]` / `openai` / `deepseek` (см. скелет выше). |
| 145 | |
| 146 | --- |
| 147 | |
| 148 | <a id="adr0083-implementation"></a> |
| 149 | |
| 150 | ## Статус реализации |
| 151 | |
| 152 | Реализовано в коде (модели, сериализация Tomlyn, VM, панель «AI и чата», сэмплы). Расширения (например `openai_compatible` в `[ai.local]`, редактирование облачных моделей из UI) — по мере необходимости. |
| 153 | |