# Agentik — архитектура > Полевые заметки о структуре проекта на текущий момент. > Подробности запуска — `docs/STANDALONE.md`, чек-лист native-сборки — > `NATIVE-COMPATIBILITY.md`, открытые вопросы — `IRC-QUESTIONS.md`. ## 1. Контекст 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 — за интерфейсом `pw.binom.litert.LiteLlm`. Реализации: `pw.binom.litert.openai` (HTTP/JSON поверх OpenAI-API, любая совместимая endpoint) и `pw.binom.litert.google` (встроенный LiteRT-LM движок для `.litertlm`/`.task` моделей). MCP — `io.modelcontextprotocol:kotlin-sdk-client` (KMP). Подключаются stdio + streamable-HTTP серверы, тулы оборачиваются в `LiteTool`. ## 2. Модули ``` 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 | Error`. `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)` → продолжение стрима. - На ошибку хода (init/стрим LLM): `failTurn` → `messageStore.append(Error)` (audit; в working_memory не пишется) + `Event.Error` + `Event.End`, живой `LiteConversation` сбрасывается и пересобирается на следующем `send`. - `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-вставка отложена (нужен дизайн-проработка). **`JournalStore`** — append-only аудит. На каждый ход дописываются `UserMessage`, `AssistantMessage`, `ToolCall`, `ToolResult`, `Error`. Никаких 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 | назначение | |---|---| | `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 | ## 8. Что осталось за рамками v1 - Суммаризация `WorkingMemory.compact()` (пункт `compact(dropFromOrderIdx)` пуст). - Image input (Content.Image принимается, но `LiteContentPart.Image` сейчас дропается в Chat-цикле с warn-логом — модель видит только текст). - Auth на фасаде. - A2A transport facade в `Main.kt`.