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:
+154
-102
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user