ADR 0038: Фасад агента — провайдеры LLM, чат и оркестрация инструментов¶
Статус: Accepted · Implemented (текущий код); раздел «Направление» — черновик идей, не обязательства
Дата: 2026-04-11
Связанные ADR¶
| ADR | Роль |
|---|---|
| 0008 | контракты MCP и тестируемость |
| 0016 | внешний агент по ACP |
| 0020 | видимость рассуждения и ограничения провайдеров |
| 0031 | эволюция UI чата |
Вне ADR¶
| Документ | Роль |
|---|---|
| MCP-PROTOCOL.md | канон IdeCommands |
Контекст¶
В продукте одновременно нужны: интерактивный чат с моделью, отдельный контур внешнего агента (Cursor CLI по ACP) и экспериментальный автономный режим, где модель выбирает инструменты IDE и опционально внешние MCP. Развитие ветки агента отставало от работы по кокпиту, зонам внимания (PFD/MFD/CDS) и инфраструктуре команд — при этом код уже фиксирует полезные границы, которые стоит описать, чтобы не потерять направление.
Решение (как сейчас)¶
1. Единая точка стриминга чата — AiProviderManager¶
Services/AiProviderManager.cs — фасад для текстового чата: принимает ключ провайдера, историю ChatMessage, опционально путь и текст текущего файла, флаг минимизации контекста. Делегирует выбор реализации провайдера и имени модели через AiProviderResolver (в MainWindowViewModel.ResolveProvider: Ollama, Anthropic, OpenAI-совместимые, DeepSeek; для CursorACP провайдер чата не создаётся — см. п. 3).
При включённой минимизации контекста для текущего файла подмешивается блок из ContextMinimizer (диагностики и сигнатуры для .cs), затем вызывается IAiChatProvider.StreamChatAsync.
2. Контракт провайдера — только стрим текста¶
IAiChatProvider задаёт минимальный контракт: IAsyncEnumerable<string> по списку сообщений. Отдельного протокола нативного tool-calling на уровне провайдера в коде нет; автономный режим строится поверх этого контракта (п. 4).
3. Три входа пользователя к «агенту» в широком смысле¶
| Контур | Где | Транспорт |
|---|---|---|
| Чат (LLM из настроек) | Features/Chat/ChatPanelViewModel |
AiProviderManager.StreamChatAsync, кроме ветки Cursor ACP |
| Cursor ACP | Тот же ChatPanelViewModel, если активный провайдер CursorACP |
CursorAcpChatConnection (stdio к внешнему агенту), не через AiProviderManager |
| Автономный агент | Features/AutonomousAgent/AutonomousAgentService |
Тот же AiProviderManager для получения сырого ответа модели по промпту; парсинг JSON и вызовы тулов — в сервисе |
ACP ортогонален встроенному фасаду LLM; это согласуется с 0016.
4. Автономный цикл: «модель → JSON → исполнение»¶
AutonomousAgentService реализует простой цикл шагов: промпт с целью, уровнем безопасности и историей; модель должна вернуть только JSON (type: tool_call или final). Парсинг — извлечение первого JSON-объекта из ответа (ExtractFirstJsonObject), без полноценного function-calling API со стороны провайдера — прямо отмечено в XML-доке класса.
scope: "ide"— вызовIIdeMcpActions.ExecuteCommandAsyncсide_command_idи аргументами; соответствует канонуIdeCommands/ MCP-PROTOCOL.md.scope: "external"— вызовMcpClientService.CallToolAsyncпо ключуprefix.toolName; разрешено только при уровне L3; при L1/L2 внешние MCP блокируются с пояснением в трассу.
5. Безопасность и подтверждения в автономном режиме¶
Уровни L1 / L2 / L3 ограничивают набор допустимых IDE-команд (L1 — без высокорисковых правок и git commit/push и т.д.). Для части команд при L2 возможен RequestConfirmationAsync перед исполнением; внешние MCP — только L3 (п. 4).
6. Внешние MCP как клиент процесса¶
McpClientService подключает серверы по stdio, индексирует тулы как {ToolPrefix}.{tool.Name}, отдаёт список в автономный промпт и выполняет CallToolAsync. Это клиент к внешним процессам; отдельно от сценария «IDE сама выступает MCP-сервером» для внешнего агента (0008, MCP-протокол в репозитории).
Последствия¶
- Плюсы: мало абстракций; один стриминговый путь для чата и для «сырого» ответа в автономном режиме; явное разделение IDE-команд и внешних MCP по
scopeи по уровню L3. - Минусы / техдолг: хрупкий парсинг JSON из произвольного текста модели; нет единого оркестратора «план → инструменты → наблюдения» на уровне домена; список разрешённых
ide_command_idв промпте зашит строкой вBuildPrompt, а не генерируется из каталога команд;CursorACPне участвует в автономном контуре.
Направление (идеи, чтобы не потерялись)¶
Эти пункты не приняты как обязательства; приоритет задаётся дорожной картой и фокусом на кокпит/внимание.
- Единый слой оркестрации (имя условное: Agent Orchestrator): вынести из
AutonomousAgentServiceи из чата общие правила «один шаг», логирование, лимиты токенов, политику инструментов — чтобы не плодить расхождения между режимами. - Явный контракт tool-calling там, где провайдер его поддерживает (OpenAI/Anthropic и т.д.): постепенно уменьшать долю «JSON в свободном тексте»; для Ollama оставить fallback или отдельный путь «структурированный вывод» с валидацией схемы.
- Ollama как транспорт, не как смысловой «тип агента»: различать локальная модель (через Ollama HTTP/API) и политику (что разрешено вызывать); избегать смешения «Ollama = весь агент» в UX и настройках.
- Каталог IDE-команд для промпта — генерировать из того же источника правды, что и MCP-док (
IdeMcpToolCatalog/ реестр), чтобы автономный режим не отставал от новыхide_*. - Связка с 0031: структурированные пакеты уточнений и автономный режим могут делить один формат «шаг диалога» (на уровне данных), даже если UI разный.
- Наблюдаемость (0020): явные слои «ответ пользователю / трасс шагов / сырой лог провайдера» для автономного режима — по тем же принципам, что и для чата.
- Тесты без сети: контрактные тесты парсера решений и маршрутизации
scopeс подставнымиIIdeMcpActionsиMcpClientService— укрепить 0008.
Отклонённые или отложенные альтернативы (кратко)¶
- Только внешний агент (ACP) для всего — отвергнуто как единственный путь: нужен встроенный чат и автономность без обязательного Cursor CLI.
- Один протокол для чата и MCP — преждевременно: чат остаётся текстовым; MCP — для инструментов и отдельных сценариев (0016).