9.8 KiB
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: StringcreateConversation(temp: Boolean): Conversationsuspend 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: BooleanupdatedAt: Instantsend(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 | 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<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→ emitEvent.ToolCall→ execute (tool.invoke(argsJson)) → emitEvent.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.