Введение монотонного offset'а как персистентного состояния агента:
offset'ы переживают рестарт standalone-агента, клиент продолжает
синхронизацию инкрементально, без полной re-sync с нуля.
outbox-api:
- Cursor (offset: Long) — курсор в журнале событий агента.
- OffsetSequencer — интерфейс резервирования уникального offset.
- CursorStore — персистентное хранилище текущего offset'а.
- PersistentOffsetSequencer — декоратор над любым OutboxStore,
обновляет CursorStore на каждом append (atomic transaction).
- OutboxGapException — клиент запросил after < earliestCursor() →
сервер не может удовлетворить, клиент обязан делать full resync.
- DurableEvent переименован из Event.kt → DurableEvent.kt (Event.kt
был общим sealed-типом, теперь это термин из спеки).
- MutableOutboxStore и OutboxStore теперь читают offset через
CursorStore вместо in-memory counter'а.
outbox-inmemory:
- InMemoryOffsetSequencer — для тестов и dev-режима.
- InMemoryOutboxStore теперь принимает OffsetSequencer в конструкторе.
outbox-ksqlite (новый модуль):
- KsqliteCursorStore — таблица outbox_cursor (agent_id TEXT PK,
offset INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL).
- KsqliteCursorStoreTest — 4 теста (set/get, monotonic, concurrent).
proto + server:
- Snapshot.proto — server-state snapshot endpoint для клиентов,
которым нужна полная материализация (использование TBD).
- Routes.kt + SnapshotRouteTest — endpoint /agentik/snapshot (GET).
journal-ksqlite:
- KsqliteJournalStore.listFlow/append — без изменений по API,
нотации минимальные (codecs).
standalone:
- DurableLog (бывший ChatAgent-orchestration) — атомарный commit
события в OutboxStore + PersistentOffsetSequencer + materialization
(через Reducer) одной транзакцией.
- SqliteStores — добавляет KsqliteCursorStore в bundle, единая
shared-connection для всех ksqlite-сторов standalone-агента.
- ChatAgent / ConversationLoop / ConversationEvents / ReflectionScheduler /
ToolDispatcher — переход на новые абстракции.
- standalone/build.gradle.kts — implementation(project(':outbox-ksqlite'))
включено (раньше было закомментировано — модуль только создавался).
client:
- AgentikAgent / AgentClient / HttpEventStore / HttpJournalStore /
ReconnectingOutbox — используют Cursor через transport API.
- client/README.md — синхронизирован с новым поведением (468 строк
diff — это в основном оформление и примеры).
kotlinx-io: 0.8.0 → 0.9.1 в libs.versions.toml (см. sync-core tests).
SYNC-SYSTEM.md (в корне) — спецификация, на которую ссылается и
:sync-core (эта сессия), и эта Cursor-абстракция в outbox-api.
Тесты: standalone 132, journal-ksqlite 25, outbox-inmemory 20,
outbox-ksqlite 4, client 10, sync-core 74 — все зелёные на jvm;
sync-core linuxX64 74 тоже зелёный.
sync2/ (заброшенный stub с одним build.gradle.kts) удалён.
10 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: String,info: AgentInfo- read-only сторы:
journal: JournalStore,outbox: OutboxStore,onlineOutbox: OnlineOutbox,conversationStore: ConversationStore createConversation(temp: Boolean): Conversationsuspend getConversation(id): Conversation?suspend deleteConversation(id): Booleansuspend renameConversation(id, title): Instant?
Conversation:
isSupportImageInput / Output / isTemporal: Boolean,title: String?updatedAt: Instantsend(content: List<Content>, context: MessageContext? = null)— write-only, ничего не возвращает.interrupt()— отмена активного хода.getMessages(after, offset, limit): List<Message>— paging.rename(title)— мутация, бампитupdatedAt.AutoCloseable—close()идемпотентен.
Content = Text(body) | Image(data, mime) — из :content-api (там же MessageContext/MessageOrigin/TurnTokens).
Message = UserMessage | AssistantMessage | ToolCall | ToolResult | Error.
События разделены на два потока (оба в :outbox-api):
- durable
DurableEvent(outbox.conversationEvents(after, id)):UserMessage | AssistantMessage | ToolCall | ToolResult | ToolFailed | Interrupted | Error | ConversationClosing | CompactionTriggered— перезапрашиваются по курсору; - online
OnlineEvent(onlineOutbox.onlineEvents(id)):Working | End | StartReasoning | StartResponse | AppendText | AppendImage— live-only, никогда не сохраняются.
AgentEvent = Created(conversationId) | Deleted(id) | Renamed(id, title) | Touched(...).
Принцип: агент — источник истины для транскрипта и сессий. Клиент лишь рендерит 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.