| 1 | <!-- English translation of adr/0083-ai-mode-and-nested-settings-toml.md. Canonical Russian: ../../adr/0083-ai-mode-and-nested-settings-toml.md --> |
| 2 | |
| 3 | # ADR 0083: `settings.toml` — `ai.mode` discriminant and nested sections (local / acp / mcp_only / cloud) |
| 4 | |
| 5 | **Status:** Accepted · Implemented |
| 6 | **Date:** 2026-04-20 |
| 7 | |
| 8 | ## Related ADRs |
| 9 | |
| 10 | | ADR | Role | |
| 11 | |-----|------| |
| 12 | | [0028](0028-user-settings-toml-localappdata-and-secrets.md) | path and secrets | |
| 13 | | [0038](0038-agent-facade-ai-provider-and-tool-orchestration.md) | provider facade and tools | |
| 14 | | [0048](0048-cursor-acp-chat-ide-parity-and-mcp-tool-surface.md) | ACP, MCP in session | |
| 15 | | [0016](0016-agent-client-protocol-external-agent.md) | ACP as external agent | |
| 16 | | [0081](0081-parametric-intent-melodies-editor-line-ranges.md) | parametric Intent Melody `:start:end` for editor — adjacent, not part of `ai.mode` | |
| 17 | |
| 18 | ## Summary |
| 19 | |
| 20 | - **`[ai]`** section in `settings.toml`: `mode` = local | acp | mcp_only | cloud. |
| 21 | - Nested sections; **no** backward compatibility with legacy `provider`. |
| 22 | |
| 23 | --- |
| 24 | |
| 25 | <a id="adr0083-context"></a> |
| 26 | |
| 27 | ## Context |
| 28 | |
| 29 | The **`[ai]`** section in user TOML currently mixes several independent axes: |
| 30 | |
| 31 | - **Which contour answers assistant text** (local Ollama, cloud API, Cursor ACP). |
| 32 | - **Flag “do not call built-in LLM, wait only for MCP”** (`chat_mcp_only`) — by meaning a **separate interaction mode**, not “another provider.” |
| 33 | |
| 34 | Readability of file and docs suffers: users must hold `provider` + boolean combinations in head. |
| 35 | |
| 36 | --- |
| 37 | |
| 38 | <a id="adr0083-decision"></a> |
| 39 | |
| 40 | ## Decision |
| 41 | |
| 42 | <a id="adr0083-mode-discriminant"></a> |
| 43 | |
| 44 | ### 1. Top-level discriminant |
| 45 | |
| 46 | In **`[ai]`** introduce mandatory (for canon) field: |
| 47 | |
| 48 | | TOML key | Meaning | |
| 49 | |----------|---------| |
| 50 | | `mode` | One of: **`local`**, **`acp`**, **`mcp_only`**, **`cloud`**. | |
| 51 | |
| 52 | **Why not bare `mcp`:** collides with **protocol and connected MCP servers** (tools in ACP session, external stdio servers, etc.). Chat mode value should read as **assistant reply source**, not “MCP enabled.” Canonical mode name — **`mcp_only`**: built-in LLM is **not** called after user send; assistant text comes **only** from external MCP contour (e.g. `send_chat` with assistant role). Same semantics as `assistant_via_mcp` or `no_builtin_provider`; TOML picks short **`mcp_only`**. |
| 53 | |
| 54 | Semantics: |
| 55 | |
| 56 | | Value | Purpose | |
| 57 | |-------|---------| |
| 58 | | **`local`** | Built-in local provider: **`[ai.local]`** with **`backend = "ollama"`** (and later values) plus **`[ai.local.ollama]`** for model/endpoint. Other local backends — separate `backend` and `[ai.local.<name>]`. | |
| 59 | | **`acp`** | Chat via **Cursor Agent / ACP** (`cursor-agent`): paths, `model_id`, MCP server **injection** policy in ACP subsection; **not** same as `mcp_only` mode. | |
| 60 | | **`mcp_only`** | **MCP-only reply** for assistant text: built-in provider (Ollama/cloud) **not** used in this sense. Connected MCP servers as tools may exist in `local` / `acp` / `cloud` — orthogonal. | |
| 61 | | **`cloud`** | Cloud providers with API keys (Anthropic, OpenAI-compatible, DeepSeek, etc.): provider choice and credentials in nested tables. | |
| 62 | |
| 63 | <a id="adr0083-nested-tables"></a> |
| 64 | |
| 65 | ### 2. Nested tables (direction) |
| 66 | |
| 67 | Keys and exact nesting fixed at implementation; normative **skeleton**: |
| 68 | |
| 69 | ```toml |
| 70 | [ai] |
| 71 | mode = "local" # local | acp | mcp_only | cloud |
| 72 | |
| 73 | [ai.local] |
| 74 | backend = "ollama" |
| 75 | |
| 76 | [ai.local.ollama] |
| 77 | # model, base_url, request_timeout |
| 78 | |
| 79 | [ai.acp] |
| 80 | # cursor_acp_path, cursor_acp_model_id, acp_auto_inject_ide_mcp |
| 81 | |
| 82 | [ai.mcp_only] |
| 83 | # limits/flags specific to “no built-in LLM, reply only from MCP” |
| 84 | |
| 85 | [ai.cloud] |
| 86 | # active_provider = "anthropic" | "openai" | "deepseek" |
| 87 | |
| 88 | [ai.cloud.anthropic] |
| 89 | [ai.cloud.openai] |
| 90 | [ai.cloud.deepseek] |
| 91 | ``` |
| 92 | |
| 93 | Pluggable cloud provider catalog (dynamic `[[...]]` arrays, etc.) **out of scope**; v1 — fixed nested tables above. |
| 94 | |
| 95 | **Naming:** TOML **`snake_case`**, aligned with `CascadeTomlSerializer` / [0028](0028-user-settings-toml-localappdata-and-secrets.md). Nesting **`[ai.local.ollama]`** / **`[ai.cloud.*]`** gives context: leaf keys **`model`**, **`base_url`** without long prefixes. For local **service/API** choice in **`[ai.local]`** canonical field — **`backend`**; do not use synonym **`engine`**. |
| 96 | |
| 97 | **Local mode: “type” vs model.** Weight format type (GGUF, SafeTensors, …) **not** in user **`settings.toml`** — not IDE contract. Two levels: |
| 98 | |
| 99 | 1. **`backend`** in **`[ai.local]`** — string discriminant: **`ollama`**, later **`openai_compatible`**, etc. Must **match** active **`[ai.local.<backend>]`** table. |
| 100 | 2. **`model`** — **API model id** for chosen engine (e.g. `qwen2.5-coder:7b` for Ollama), not file format type. |
| 101 | |
| 102 | **Beyond Ollama (product, not necessarily v1):** same abstract **OpenAI-compatible HTTP** on localhost — LM Studio, llama.cpp OpenAI API mode, vLLM, LocalAI — e.g. **`[ai.local.openai_compatible]`** with `model`, `base_url`, optional key ref in `ai-keys.toml`. |
| 103 | |
| 104 | <a id="adr0083-secrets"></a> |
| 105 | |
| 106 | ### 3. Secrets |
| 107 | |
| 108 | Still **do not** store API keys in `settings.toml`: separate **`ai-keys.toml`** ([0028](0028-user-settings-toml-localappdata-and-secrets.md)). This ADR only defines **which fields** reference keys by name/slot. |
| 109 | |
| 110 | --- |
| 111 | |
| 112 | <a id="adr0083-backcompat"></a> |
| 113 | |
| 114 | ## Backward compatibility |
| 115 | |
| 116 | **Not required.** Legacy keys `provider`, `chat_mcp_only`, flat `default_ollama_model`, cloud URLs at `[ai]` root **removed** from canon: migration — manual file edit and sample updates (`docs/samples/settings*.toml`). Optional one-time script/doc “how to rewrite old file” outside mandatory runtime automigration. |
| 117 | |
| 118 | --- |
| 119 | |
| 120 | <a id="adr0083-consequences"></a> |
| 121 | |
| 122 | ## Consequences |
| 123 | |
| 124 | 1. **`AiSettings` and related VM** restructured for discriminant + nested types; Tomlyn serialization must reproduce nested tables predictably. |
| 125 | 2. **AI settings UI** maps to `mode` and subsections instead of flat `Ollama|…|CursorACP` list. |
| 126 | 3. **Docs and samples** — single example with `[ai].mode` and nested blocks. |
| 127 | 4. **Tests** — TOML deserialization contract for new shape; remove tests on old keys. |
| 128 | |
| 129 | --- |
| 130 | |
| 131 | <a id="adr0083-rejected"></a> |
| 132 | |
| 133 | ## Rejected / deferred alternatives |
| 134 | |
| 135 | - **Keep only `provider` + flags** — rejected as less readable for user and ADR. |
| 136 | - **Mode named `mcp`** — rejected: ambiguous with MCP protocol and connected servers; canon — **`mcp_only`**. |
| 137 | - **Automigration on load** — not done by requirement; reduces hidden surprises. |
| 138 | - **Plugins and extensible cloud provider registry** — deferred; v1 canon — fixed `[ai.cloud.anthropic]` / `openai` / `deepseek`. |
| 139 | |
| 140 | --- |
| 141 | |
| 142 | <a id="adr0083-implementation"></a> |
| 143 | |
| 144 | ## Implementation status |
| 145 | |
| 146 | Implemented in code (models, Tomlyn serialization, VM, “AI and chat” panel, samples). Extensions (e.g. `openai_compatible` in `[ai.local]`, cloud model editing from UI) as needed. |
| 147 | |