Перейти к содержанию

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, опционально путь и текст текущего файла, флаг минимизации контекста. Делегирует выбор реализации провайдера и имени модели через AiProviderResolverMainWindowViewModel.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 не участвует в автономном контуре.

Направление (идеи, чтобы не потерялись)

Эти пункты не приняты как обязательства; приоритет задаётся дорожной картой и фокусом на кокпит/внимание.

  1. Единый слой оркестрации (имя условное: Agent Orchestrator): вынести из AutonomousAgentService и из чата общие правила «один шаг», логирование, лимиты токенов, политику инструментов — чтобы не плодить расхождения между режимами.
  2. Явный контракт tool-calling там, где провайдер его поддерживает (OpenAI/Anthropic и т.д.): постепенно уменьшать долю «JSON в свободном тексте»; для Ollama оставить fallback или отдельный путь «структурированный вывод» с валидацией схемы.
  3. Ollama как транспорт, не как смысловой «тип агента»: различать локальная модель (через Ollama HTTP/API) и политику (что разрешено вызывать); избегать смешения «Ollama = весь агент» в UX и настройках.
  4. Каталог IDE-команд для промпта — генерировать из того же источника правды, что и MCP-док (IdeMcpToolCatalog / реестр), чтобы автономный режим не отставал от новых ide_*.
  5. Связка с 0031: структурированные пакеты уточнений и автономный режим могут делить один формат «шаг диалога» (на уровне данных), даже если UI разный.
  6. Наблюдаемость (0020): явные слои «ответ пользователю / трасс шагов / сырой лог провайдера» для автономного режима — по тем же принципам, что и для чата.
  7. Тесты без сети: контрактные тесты парсера решений и маршрутизации scope с подставными IIdeMcpActions и McpClientService — укрепить 0008.

Отклонённые или отложенные альтернативы (кратко)

  • Только внешний агент (ACP) для всего — отвергнуто как единственный путь: нужен встроенный чат и автономность без обязательного Cursor CLI.
  • Один протокол для чата и MCP — преждевременно: чат остаётся текстовым; MCP — для инструментов и отдельных сценариев (0016).