From cad4d5fc7c71dcb06cda907349d088bf44e13d82 Mon Sep 17 00:00:00 2001 From: subochev Date: Sun, 13 Sep 2026 19:10:20 +0300 Subject: [PATCH] =?UTF-8?q?AGUI:=20=D1=83=D0=B4=D0=B0=D0=BB=D1=91=D0=BD=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=BD=D0=BE=D1=81=D1=82=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Убрана зависимость pw.binom.agui:server из проекта — AGUI больше не нужен (см. Memory #3675; agentik переходит на свой протокол :proto и HTTP-фасад :server). Чистка: - standalone/build.gradle.kts: implementation(libs.agui.server) → удалено - gradle/libs.versions.toml: [versions] agui + agui-api/agui-client/ agui-server entries → удалены - settings.gradle.kts: убран 'AG-UI' из комментария к Nexus-репо - proto/build.gradle.kts: комментарий 'Зеркалит набор AG-UI api' → 'Полный набор KMP-целей' - proto/src/.../Agent.kt: KDoc '(замена AG-UI)' → удалено - settings.gradle.kts: то же - docs/ARCHITECTURE.md: переписан (описывал старую AGUI-centric архитектуру с AbstractAgent/SessionStore/ AgentEngine — ничего этого в коде уже нет; теперь отражает текущее состояние: :proto + :server + :standalone + LiteLlm + MCP + SqliteStores) - NATIVE-COMPATIBILITY.md: пункт 10 'agui-server KMP-готовность' → отменён (см. -); убран из 'Жёсткие блокеры' В коде не осталось ни одного обращения к AGUI. После чистки в репо больше нет ни одной зависимости от pw.binom.agui.*. Проверка: ./gradlew :server:assemble (9/9 KMP-целей) + :standalone:jvmTest 44/44 (ChatAgent 15, LlmConfig 6, McpConfig 8, McpRegistry 4, Persistence 11) — зелёные. --- NATIVE-COMPATIBILITY.md | 10 +- docs/ARCHITECTURE.md | 256 +++++++++++------- gradle/libs.versions.toml | 6 - proto/build.gradle.kts | 2 +- .../kotlin/pw/binom/agentik/proto/Agent.kt | 2 +- settings.gradle.kts | 4 +- standalone/build.gradle.kts | 4 +- 7 files changed, 166 insertions(+), 118 deletions(-) diff --git a/NATIVE-COMPATIBILITY.md b/NATIVE-COMPATIBILITY.md index 1eba75d..3df8915 100644 --- a/NATIVE-COMPATIBILITY.md +++ b/NATIVE-COMPATIBILITY.md @@ -85,10 +85,13 @@ JDBC `:memory:`). Объём: один `expect fun openInMemory()` в commonMain + два `actual` (JDBC `jdbc:sqlite::memory:` / native-driver нативный API). -## Блок 6. Внешние либы (`a2a-server`, `agui-server`) — наши +## Блок 6. Внешние либы (`a2a-server`) — наши -- 10. [ ] **`agui-server`: проверить KMP-готовность.** Per Memory #3561 — KMP - jvm+native. *Если по факту уже KMP — только подключить `linuxX64`-артефакт в `:standalone`; если JVM-only — пункт 11.* +- 10. [x] **AGUI убран из проекта.** catalog-entries `agui-api`/`agui-client`/`agui-server`, + `[versions] agui` и `implementation(libs.agui.server)` в `:standalone` + удалены — AGUI больше не нужен (см. Memory #3675 и переписанный + `docs/ARCHITECTURE.md`). AGUI-фасад был stateless request/response, + а agentik переходит на свой stateful протокол `:proto` / `:server`. - 11. [ ] **`a2a-server`: JVM-only → KMP.** Per Memory #3561 — JVM-only (client/server; shared уже KMP). Без native-таргета весь стек A2A недоступен на linuxX64. Объём: переписать на `multiplatform`, @@ -150,4 +153,3 @@ - `litertlm-jvm` → нужен `litertlm-native` от Google (см. п.5) - `a2a-server` JVM-only (см. п.11) — нужно переписать или явно не подключать -- `agui-server` KMP-готовность под вопросом (см. п.10) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c776f9f..5d87f4c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,113 +1,165 @@ -# Agentik — архитектура (на основе ограничений AGUI) +# Agentik — архитектура + +> Полевые заметки о структуре проекта на текущий момент. +> Подробности запуска — `docs/STANDALONE.md`, чек-лист native-сборки — +> `NATIVE-COMPATIBILITY.md`, открытые вопросы — `IRC-QUESTIONS.md`. ## 1. Контекст -agentik — runtime агента. Внешне он exposes два фасада: -- **AG-UI** — SSE-поток событий для UI-клиентов (`POST /agui`), транспорт Netty. -- **A2A** — JSON-RPC для agent↔agent (`POST /`, `message/send`), транспорт CIO. +agentik — runtime агента. Ядро на собственном протоколе (`:proto`), +HTTP/SSE-фасад в `:server`, runtime-контейнер в `:standalone`. Раньше +фасадов было два (AG-UI + A2A); AG-UI убран как устаревший, +`a2a-server` остаётся заготовкой в `libs.versions.toml` и подключён в +`:standalone` `build.gradle.kts`, но в `Main.kt` пока не монтируется. -Ядро (сессии, чат-цикл, тулы, LLM, MCP) **не зависит от транспорта**: фасады лишь сериализуют/десериализуют его. Библиотеки из каталога `caffeine`: `agui` 0.1.0, `a2a` 1.0.0-SNAPSHOT; LLM — `pw.binom.openai` (ktor-impl). +LLM — за интерфейсом `pw.binom.litert.LiteLlm`. Реализации: +`pw.binom.litert.openai` (HTTP/JSON поверх OpenAI-API, любая +совместимая endpoint) и `pw.binom.litert.google` (встроенный +LiteRT-LM движок для `.litertlm`/`.task` моделей). -## 2. Ограничения AGUI (что диктует протокол) +MCP — `io.modelcontextprotocol:kotlin-sdk-client` (KMP). Подключаются +stdio + streamable-HTTP серверы, тулы оборачиваются в `LiteTool`. -AGUI — push-протокол. Агент = `Agent { RunAgentInput -> Flow }`. +## 2. Модули -Ключевые типы (из `pw.binom.agui.api`): +``` +agentik +├─ :proto KMP jvm + native. Интерфейсы Agent/Conversation, Content, +│ Message, Event, AgentEvent. @SerialName +│ дискриминаторы snake_case на проводе. +├─ :server KMP jvm + native. HTTP/SSE фасад `Route.agentikAgent(agent, +│ path = "/agentik")`. Json DTO — свой +│ Snapshot-тип, чтобы протокол оставался +│ сериализационно-чистым. +├─ :client JVM-only (пока). Ktor-клиент к фасаду `:server`, +│ подключающий удалённый агент как +│ локальный `Agent`. +└─ :standalone KMP jvm (linuxX64 — в работе). Runnable-контейнер: + SqliteStores + ChatAgent + MCP + LiteLlm, + поднимает Ktor (CIO) на AGENTIK_PORT. +``` -| Тип | Смысл | +Каталог версий — `gradle/libs.versions.toml`. Из внешних — только +`pw.binom.litert.*`, `pw.binom.a2a.*`, `app.cash.sqldelight`, +`io.ktor:*`, `io.modelcontextprotocol:kotlin-sdk-client`, +`org.jetbrains.kotlinx:*`. + +## 3. `:proto` — интерфейсы + +`Agent` (см. `proto/src/commonMain/.../Agent.kt`): +- `id: String` +- `createConversation(temp: Boolean): Conversation` +- `suspend getConversation(id): Conversation?` +- `suspend getConversations(offset, limit): List` +- `getConversations(offset = 0): Flow` — cold-flow paging через suspend-версию, `PAGE_SIZE = 100`. +- `events(after: Instant): Flow` — replay-free, бэкфилл через snapshot. +- `deleteConversation(id): Boolean` + +`Conversation`: +- `isSupportImageInput / Output / isTemporal: Boolean` +- `updatedAt: Instant` +- `send(content: List)` — write-only, ничего не возвращает. +- `interrupt()` — отмена активного хода. +- `events(after): Flow` — live, replay-free. +- `getMessages(after, offset, limit)` + `getMessages(after): Flow` — paging. +- `rename(title)` — мутация, бампит `updatedAt`. +- `AutoCloseable` — `close()` идемпотентен. + +`Content = Text(body) | Image(data, mime)`. +`Message = UserMessage | AssistantMessage | ToolCall | ToolResult | Summary | System`. +`Event = StartReasoning | StartResponse | End | AppendText | AppendImage | ToolCall | ToolResult | Error`. +`AgentEvent = Created(conversationId) | Deleted(id) | Renamed(id, title)`. + +Принцип: **агент — источник истины** для транскрипта и сессий. Клиент +лишь рендерит Event-stream и кэширует историю. + +## 4. `:standalone` — runtime-контейнер + +Точка входа `pw.binom.agentik.standalone.MainKt`: + +``` +Main.kt + ├─ LlmConfig.fromEnv() → LlmConfig(backend, openai, google, systemPrompt) + ├─ llmConfig.createLlm() → LiteLlm (рефлексия для google) + ├─ SqliteStores.open(dbPath) → ConversationStore + MessageStore + WorkingMemoryStore + ├─ McpRegistry.fromConfig(...) → список NamedTool из всех MCP-серверов + ├─ ChatAgent(stores, llm, llmConfig, tools) + └─ embeddedServer(CIO, port) { agentikAgent(agent, "/agentik") } +``` + +**`ChatAgent`** — реализация `Agent`: +- `live: Map` — in-memory кэш активных сессий. +- `createConversation(temp)`: для temp — только кэш; для persistent — + `upsert(conversation)` + `workingMemory.append(System(systemPrompt))` + атомарно, потом `live[id] = ChatConversation(...)`. +- `getConversation(id)`: кэш → store (cold-load). Переоткрытие восстанавливает + `LiteConversation` из `workingMemory.list(id)` (см. ниже). +- `send`/`events`/`interrupt`/`delete`/`rename` проксируются в `ChatConversation`. + +**`ChatConversation`** — реализация `Conversation`: +- На каждый ход: `workingMemory.append(User)` → `liteConv.sendStreamContents(...)` + → стрим `LiteDelta` → клиенту (AppendText / ToolCall / ToolResult). +- На завершение стрима: `messageStore.append(Assistant)` + `workingMemory.append(Assistant)` + + `conversationStore.touch(id)`. +- Tool-loop: на `delta.toolCalls` → emit `Event.ToolCall` → execute + (`tool.invoke(argsJson)`) → emit `Event.ToolResult` → + `liteConv.addToolResult(callId, name, result)` → продолжение стрима. +- `LiteConversation` живёт **один на весь `ChatConversation`** для обоих + бэкендов: KV-cache движка сохраняется между ходами. Это контракт + litert-api, не только Google-specific. +- На `interrupt()` отменяется текущий `Job` и `liteConv.interrupt()`. + +**`WorkingMemory`** — read-write контекст, который видит LLM: +- `System(text, sourceMessageId=null)` — системный промпт. +- `User/Assistant(sourceMessageId, content)` — снимки реальных сообщений. +- `compact(dropFromOrderIdx, conversationId)` — v1: DELETE rows ≥ order_idx, + summarization-вставка отложена (нужен дизайн-проработка). + +**`MessageStore`** — append-only аудит. На каждый ход дописываются +`UserMessage`, `AssistantMessage`, `ToolCall`, `ToolResult`. Никаких +update/delete кроме каскада из `ConversationStore.delete`. + +## 5. Слои персистентности + +`SqliteStores` (jvmMain) — три интерфейса из commonMain, одна +SQLite-БД. Схема (`jvmMain/sqldelight/`): + +| таблица | поля | роль | +|---|---|---| +| `conversation` | id, title, is_temporal, created_at, updated_at | карточки диалогов | +| `message` | id, conversation_id, role, payload_json, created_at | аудит-хвост | +| `working_memory` | id, conversation_id, order_idx, source_message_id, payload_json, created_at | контекст LLM | + +`Content` (text/image) и per-kind payload сериализуются в +`payload_json` через `kotlinx.serialization`. Maps в каскадное удаление +(`ConversationStore.delete` — одна транзакция). + +## 6. Фасады + +- **`:server` (HTTP+SSE)** — основной, KMP, Ktor Route extension. + `Route.agentikAgent(agent, path = "/agentik")` монтирует весь CRUD + + live event-stream. Подробнее — `docs/STANDALONE.md`. +- **A2A (`pw.binom.a2a:server`)** — заготовка в `libs.versions.toml`, + подключён в `:standalone` для совместимости с зависимостями через + `:client`, но **не монтируется в `Main.kt`**. Если/когда понадобится — + `Route.a2aAgent(...)` (как у agui в старом дизайне). + +## 7. Конфигурация (env) + +| env | назначение | |---|---| -| `RunAgentInput { threadId, runId, state, messages, tools, context, forwardedProps }` | весь ввод хода. Клиент шлёт всё, сервер по умолчанию stateless. | -| `threadId` | **сессия/конверсация**. | -| `runId` | **один ход** в рамках сессии. | -| `Message { id, role: developer\|system\|assistant\|user\|tool, content, toolCalls?, toolCallId? }` | сообщение. У assistant — `toolCalls`; у tool — `toolCallId`. | -| `Tool { name, description, parameters: JSONSchema }` | тул (JSON Schema в `parameters`). | -| `State = Map` | нестрогий state хода (snapshot/delta). | -| `Context { description, value }` | кусок контекста. | +| `AGENTIK_PORT` | порт Ktor (default `8080`) | +| `AGENTIK_DB_PATH` | путь к SQLite (default `./agentik.db`) | +| `AGENTIK_SYSTEM_PROMPT` | текст системного промпта | +| `AGENTIK_LLM_BACKEND` | `openai` (default) или `google` | +| `OPENAI_BASE_URL` / `OPENAI_API_KEY` / `OPENAI_MODEL` | для backend=openai | +| `AGENTIK_GOOGLE_MODEL_PATH` / `AGENTIK_GOOGLE_THREADS` | для backend=google | +| `AGENTIK_MCP_CONFIG` | путь к `mcp.json` в формате Claude Desktop | -События (`BaseEvent`, дискриминатор `type`): -- lifecycle: `RUN_STARTED{threadId,runId}` → … → `RUN_FINISHED` / `RUN_ERROR{message,code}` -- steps: `STEP_STARTED/FINISHED{stepName}` -- text: `TEXT_MESSAGE_START{messageId,role}` → `TEXT_MESSAGE_CONTENT{messageId,delta}` → `TEXT_MESSAGE_END{messageId}` -- tool: `TOOL_CALL_START{toolCallId,toolCallName,parentMessageId?}` → `TOOL_CALL_ARGS{toolCallId,delta}` → `TOOL_CALL_END{toolCallId}` → `TOOL_CALL_RESULT{toolCallId,content}` -- state: `STATE_SNAPSHOT{snapshot}` / `STATE_DELTA{delta}` / `MESSAGES_SNAPSHOT{messages}` -- прочее: `RAW{event}` / `CUSTOM{name,value}` +## 8. Что осталось за рамками v1 -Свободные варианты на стороне либы: -- stateless: `Agent { input -> flow }` (использует `input.messages/state/tools`). -- stateful: `AbstractAgent(agentId, threadId)` — держит `state` + `messages`, переопределяешь `runAgent(RunAgentParameters)`. - -Сервер (`aguiAgent(agent, path="/agui")`) не хранит ничего между запросами: декодирует `RunAgentInput`, вызывает `agent.run`, стримит события, оборачивает сбой в `RUN_ERROR`. - -## 3. Как вписываемся (принципы) - -1. **`threadId` = наша сессия.** Держим `SessionStore[threadId]` на сервере даже при stateless-сервере AGUI: на каждом ходе реконсилируем `input.messages` с сохранёнными и дописываем новые. Это даёт «настоящие сессии» с историей. -2. **`runId` = ход.** Один `RunAgentInput` → один цикл ядра → один поток событий. -3. **Ядро говорит на AGUI-событиях** — это и есть наш протокол вывода; фасады не переопределяют его. -4. **LLM — за интерфейсом.** Ядро не знает, какая LLM. Точка входа `LlmClient.complete(...)`. -5. **Тулы: внутренний реестр + MCP.** `ToolRegistry` объединяет «родные» тулы и тулы MCP-серверов (через bridge). LLM вызывает их; ядро исполняет и возвращает `TOOL_CALL_RESULT`. -6. **А2A = request/response.** Переиспользуем то же ядро, но собираем финальный текст и отдаём одним A2A `Message`. - -## 4. Пакеты / модули - -``` -pw.binom.agentik -├─ core -│ ├─ session Session(threadId, messages, state) + SessionStore (in-memory, by threadId) -│ ├─ tool ToolSpec, Tool, ToolRegistry (InMemoryToolRegistry, builtin tools) -│ ├─ llm LlmClient (interface) + LlmMessage/LlmToolCall/LlmResponse + EchoLlmClient (stub) -│ ├─ mcp McpClient (interface: listTools/callTool) + McpTool (bridge) + ToolRegistry.registerMcpClient -│ └─ engine AgentEngine: prompt -> LLM -> tool-loop -> AGUI events (RunStarted..RunFinished) -└─ fronts - ├─ agui AgentikAgent : Agent { input -> engine.run(input) } [вместо EchoAgent] - └─ a2a EngineA2aHandler : A2A handle(msg, contextId) -> Message [собирает финальный текст] -``` -Wiring (в `standalone`): `Main` собирает `AgentEngine(llm, tools+MCP, sessions)`, отдаёт его `AgentikAgent` (AGUI) и `EngineA2aHandler` (A2A). Конфигурация (LLM, MCP-серверы, тулы) — в одном месте (см. §8). - -## 5. Поток данных (AGUI) - -``` -client ──POST /agui {RunAgentInput}──> AgentikAgent - │ engine.run(input) - ▼ - SessionStore.getOrCreate(threadId) - merge input.messages, input.tools - │ - ┌─> llm.complete(system, history, tools, state) - │ └─ toolCalls? -> TOOL_CALL_* + ToolRegistry.call + TOOL_CALL_RESULT -> llm.complete (loop) - └─> text? -> TEXT_MESSAGE_* (последнее) - │ - persist final assistant msg in session - │ - client <──SSE [BaseEvent]──────┘ (RunStarted .. RunFinished / RunError) -``` - -## 6. A2A-фасад (request/response) - -`A2A handle(request: Message, contextId): Message`: -1. `text = request.parts.filterIsInstance().join("")`. -2. `RunAgentInput(threadId = contextId ?: new, runId = randomRunId(), messages = [Message(USER, text)])`. -3. `events = engine.run(input)` (синхронно, `runBlocking { ...toList() }`). -4. `answer = events.filterIsInstance().join { it.delta }`. -5. вернуть `Message(role=AGENT, parts=[TextPart(answer)])`. - -`contextId` ↔ `threadId` — общая сессия. A2A не стримит, поэтому события ядра сворачиваются в финальный ответ. - -## 7. Зоны ответственности - -**Ядро (скелет уже закладываем):** -`Session/SessionStore`, `Tool/ToolRegistry`, `LlmClient`(iface)+stub, `McpClient`(iface)+bridge, `AgentEngine`, `AgentikAgent`, `EngineA2aHandler`, `Main`. - -**Ты (поверх ядра):** -- реализация `LlmClient` (через `pw.binom.openai:ktor-impl`), -- конкретные «родные» тулы, -- транспорт `McpClient` (подключение к MCP-серверам). - -## 8. Открытые решения (нужны решения) - -1. **Персист сессий** — in-memory или на диск/БД (сейчас in-memory). -2. **Конфигурация** — MCP-серверы, LLM endpoint/ключ, системный промпт: env / yaml / файл (сейчас env-заготовки: `AGENTIK_A2A_AGENTS`). -3. **Ограничения** — `maxToolRounds`, таймауты LLM/тулов, лимиты размера. -4. **Аутентификация** — Bearer на обоих фасадах (а2a/agui принимают `token`), пока не включена. -5. **Структура модулей** — держать всё в `standalone` (по пакетам) или вынести `core`/`mcp` в отдельные подмодули (для переиспользования). -6. **Поведение сессии** — «клиент источник правды» (replace) vs «сервер источник правды» (append) при реконсилии `messages`. -``` +- Суммаризация `WorkingMemory.compact()` (пункт `compact(dropFromOrderIdx)` пуст). +- Image input (Content.Image принимается, но `LiteContentPart.Image` + сейчас дропается в Chat-цикле с warn-логом — модель видит только текст). +- Auth на фасаде. +- A2A transport facade в `Main.kt`. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ce2cf23..ddd2244 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,6 @@ kotlin = "2.4.20" kotlinx-serialization = "1.11.0" kotlinx-coroutines = "1.11.0" ktor = "3.1.3" -agui = "0.1.0" a2a = "1.0.0-SNAPSHOT" litert = "7" sqldelight = "2.3.2" @@ -15,11 +14,6 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } [libraries] -# --- AG-UI (pw.binom.agui) — KMP: jvm + native --- -agui-api = { module = "pw.binom.agui:api", version.ref = "agui" } -agui-client = { module = "pw.binom.agui:client", version.ref = "agui" } -agui-server = { module = "pw.binom.agui:server", version.ref = "agui" } - # --- A2A (pw.binom.a2a) — shared: KMP (jvm + linuxX64); client/server: JVM-only --- a2a-shared = { module = "pw.binom.a2a:shared", version.ref = "a2a" } a2a-client = { module = "pw.binom.a2a:client", version.ref = "a2a" } diff --git a/proto/build.gradle.kts b/proto/build.gradle.kts index b33c027..554efd7 100644 --- a/proto/build.gradle.kts +++ b/proto/build.gradle.kts @@ -6,7 +6,7 @@ plugins { kotlin { jvmToolchain(21) - // "Все возможные цели сборки": jvm + весь натив. Зеркалит набор AG-UI api. + // "Все возможные цели сборки": jvm + весь натив. Полный набор KMP-целей. jvm() macosX64() macosArm64() diff --git a/proto/src/commonMain/kotlin/pw/binom/agentik/proto/Agent.kt b/proto/src/commonMain/kotlin/pw/binom/agentik/proto/Agent.kt index 3112a08..2265d71 100644 --- a/proto/src/commonMain/kotlin/pw/binom/agentik/proto/Agent.kt +++ b/proto/src/commonMain/kotlin/pw/binom/agentik/proto/Agent.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.flow import kotlin.time.Instant /** - * Ядро собственного протокола agentik (замена AG-UI). Явно stateful. + * Ядро собственного протокола agentik. Явно stateful. * * Транспортно-агностично. [Agent] — фабрика stateful-диалогов: * [createConversation] возвращает [Conversation], который сам хранит историю diff --git a/settings.gradle.kts b/settings.gradle.kts index 19a26af..2245dfd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,7 +12,7 @@ dependencyResolutionManagement { repositories { mavenCentral() google() - // Home Nexus, репо "caffeine": pw.binom.* (AG-UI, A2A, ...) + // Home Nexus, репо "caffeine": pw.binom.* (A2A, ...) maven { name = "caffeine" url = uri(caffeineRepo) @@ -24,7 +24,7 @@ dependencyResolutionManagement { rootProject.name = "agentik" include(":standalone") -// Собственный протокол agentik (замена AG-UI). Пока в нём пилим, потом вынесем. +// Собственный протокол agentik. Пока в нём пилим, потом вынесем. include(":proto") // Ktor-сервер, экспонирующий Agent по HTTP (SSE + JSON). include(":server") diff --git a/standalone/build.gradle.kts b/standalone/build.gradle.kts index 436700f..c270087 100644 --- a/standalone/build.gradle.kts +++ b/standalone/build.gradle.kts @@ -55,8 +55,8 @@ kotlin { implementation(libs.ktor.server.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) - // Транспортные фасады - implementation(libs.agui.server) + // Транспортные фасады (A2A остаётся заготовкой; на v1 не подключается в Main.kt — +// см. docs/ARCHITECTURE.md §3). implementation(libs.a2a.server) // MCP (Model Context Protocol) клиент — подключение внешних/внутренних MCP-серверов