Forge
markdowndeeb25a2
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
29The **`[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
34Readability 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
46In **`[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
54Semantics:
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
67Keys and exact nesting fixed at implementation; normative **skeleton**:
68
69```toml
70[ai]
71mode = "local" # local | acp | mcp_only | cloud
72
73[ai.local]
74backend = "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
93Pluggable 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
991. **`backend`** in **`[ai.local]`** — string discriminant: **`ollama`**, later **`openai_compatible`**, etc. Must **match** active **`[ai.local.<backend>]`** table.
1002. **`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
108Still **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
1241. **`AiSettings` and related VM** restructured for discriminant + nested types; Tomlyn serialization must reproduce nested tables predictably.
1252. **AI settings UI** maps to `mode` and subsections instead of flat `Ollama|…|CursorACP` list.
1263. **Docs and samples** — single example with `[ai].mode` and nested blocks.
1274. **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
146Implemented 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
View only · write via MCP/CIDE