Forge
markdowndeeb25a2
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]
70mode = "local" # local | acp | mcp_only | cloud
71
72# Поля, общие для любого режима (пример): размещение UI настроек, флаги истории — по мере необходимости.
73
74# Локальный режим: явный backend и вложенная таблица с тем же именем (v1 — ollama).
75[ai.local]
76backend = "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
1051. **`backend`** в **`[ai.local]`** — строковый дискриминант: **`ollama`**, позже **`openai_compatible`** и т.д. Должен **совпадать** с активной подтаблицей **`[ai.local.<backend>]`** (например `backend = "ollama"` ↔ `[ai.local.ollama]`). Так читаемость в корне локальной секции не зависит только от пути; вложенная таблица по-прежнему несёт короткие ключи (`model`, …).
1062. **`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
1301. **`AiSettings` и связанный VM** перестраиваются под дискриминант + вложенные типы; сериализация Tomlyn должна воспроизводить вложенные таблицы предсказуемо.
1312. **UI настроек AI** (ComboBox провайдера и т.д.) маппится на `mode` и подсекции вместо плоского списка `Ollama|…|CursorACP`.
1323. **Документация и сэмплы** — единый пример с `[ai].mode` и вложенными блоками.
1334. **Тесты** — контракт десериализации 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
View only · write via MCP/CIDE