AGUI: удалён полностью

Убрана зависимость 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) — зелёные.
This commit is contained in:
2026-09-13 19:10:20 +03:00
parent d5e3f2dcef
commit cad4d5fc7c
7 changed files with 166 additions and 118 deletions
+6 -4
View File
@@ -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)
+154 -102
View File
@@ -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<BaseEvent> }`.
## 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<Conversation>`
- `getConversations(offset = 0): Flow<Conversation>` — cold-flow paging через suspend-версию, `PAGE_SIZE = 100`.
- `events(after: Instant): Flow<AgentEvent>` — replay-free, бэкфилл через snapshot.
- `deleteConversation(id): Boolean`
`Conversation`:
- `isSupportImageInput / Output / isTemporal: Boolean`
- `updatedAt: Instant`
- `send(content: List<Content>)` — write-only, ничего не возвращает.
- `interrupt()` — отмена активного хода.
- `events(after): Flow<Event>` — live, replay-free.
- `getMessages(after, offset, limit)` + `getMessages(after): Flow<Message>` — 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<String, ChatConversation>` — 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<String, JsonElement>` | нестрогий 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<TextPart>().join("")`.
2. `RunAgentInput(threadId = contextId ?: new, runId = randomRunId(), messages = [Message(USER, text)])`.
3. `events = engine.run(input)` (синхронно, `runBlocking { ...toList() }`).
4. `answer = events.filterIsInstance<TextMessageContentEvent>().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`.
-6
View File
@@ -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" }
+1 -1
View File
@@ -6,7 +6,7 @@ plugins {
kotlin {
jvmToolchain(21)
// "Все возможные цели сборки": jvm + весь натив. Зеркалит набор AG-UI api.
// "Все возможные цели сборки": jvm + весь натив. Полный набор KMP-целей.
jvm()
macosX64()
macosArm64()
@@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.flow
import kotlin.time.Instant
/**
* Ядро собственного протокола agentik (замена AG-UI). Явно stateful.
* Ядро собственного протокола agentik. Явно stateful.
*
* Транспортно-агностично. [Agent] — фабрика stateful-диалогов:
* [createConversation] возвращает [Conversation], который сам хранит историю
+2 -2
View File
@@ -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")
+2 -2
View File
@@ -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-серверов