Files
agentik/proto/README.md
T
subochev 5bdc517988 Добавляет Cursor/OffsetSequencer в :outbox-api и интегрирует PersistentOffsetSequencer через :outbox-ksqlite.
Введение монотонного 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) удалён.
2026-10-02 01:16:15 +03:00

7.4 KiB

:proto — протокол общения с агентом (KMP, jvm + native)

Что это

Типы и контракт in-house протокола agentik, заменившего AG-UI:

  • stateful — сервер сам владеет диалогом; клиент шлёт только новые сообщения, а не всю историю (в отличие от AG-UI, где клиент обязан повторять messages[] каждый раз).
  • declarative история vs. события — Message это то, что уже легло в БД, DurableEvent это live-стрим от агента во время send() или events().
  • чистые интерфейсы — никаких сетевых и storage зависимостей внутри :proto; это контракт.

Решает проблему: AG-UI клиент вынужден каждый раз знать и пересобирать полную историю, а его серверная часть (AbstractAgent) — постоянно сериализовать-десериализовать всю переписку. В :proto сервер один, контракт тонкий, переписка персистится нативно (SQLite, файлы, что хотите). Можно подменить front-end или back-end, протокол остаётся.

Где используется

  • :server — Ktor-фасад, маппит Agent ↔ HTTP/SSE.
  • :client — Ktor-клиент, маппит HTTP/SSE ↔ Agent/Conversation.
  • :agentik-cli — работает поверх :client, а следовательно поверх :proto. (:agentik-tui был исключён из сборки 2026-09-17.)
  • :standalone — реализует Agent (через ChatAgent) и пишет/читает Message/DurableEvent напрямую через storage.

Как подключить

// build.gradle.kts
kotlin {
    sourceSets.commonMain.dependencies {
        api("pw.binom.agentik:proto:0.1.0")
    }
}

Артефакт pw.binom.agentik:proto:0.1.0 живёт в Nexus-репозитории caffeine (HTTP http://<your-nexus>/repository/caffeine/, plain-HTTP, credentials — через переменные binom.repo.user/password/url).

Версии

Каталог gradle/libs.versions.toml, секция [versions] → agentik-proto. Поднять версию → переопубликовать все KMP-таргеты через ./gradlew :proto:publish -Pversion=... (или триггернуть Gitea release).

Текущие KMP-таргеты: jvm + macosX64/macosArm64 + iosX64/iosArm64/iosSimulatorArm64 + linuxX64/linuxArm64 + mingwX64.

Публикация

Настройки в gradle.properties / env: binom.repo.url, binom.repo.user, binom.repo.password. ./gradlew :proto:publish публикует все target-specific артефакты + общий kotlinMultiplatform.

Основные типы

:proto — тонкий контракт: сами интерфейсы Agent/Conversation/Message, а общие типы содержимого и события живут в нижележащих модулях: Content/MessageContext/MessageOrigin/TurnTokens — в :content-api, DurableEvent/OnlineEvent/OutboxStore/OnlineOutbox — в :outbox-api.

interface Agent : AutoCloseable {
    val id: String
    val info: AgentInfo
    val journal: JournalStore         // append-only audit (read-only)
    val outbox: OutboxStore           // durable-события: catchup+live по курсору
    val onlineOutbox: OnlineOutbox    // live-only: стриминг, без курсора
    val conversationStore: ConversationStore
    fun createConversation(temp: Boolean): Conversation
    suspend fun getConversation(id: String): Conversation?
    suspend fun deleteConversation(id: String): Boolean
    suspend fun renameConversation(id: String, title: String?): Instant?
}

interface Conversation : AutoCloseable {
    val id: String
    val updatedAt: Instant
    val isTemporal: Boolean
    val title: String?
    val isSupportImageInput: Boolean
    val isSupportImageOutput: Boolean
    suspend fun send(content: List<Content>, context: MessageContext? = null) // fire-and-forget
    suspend fun getMessages(after: Instant, offset: Int, limit: Int): List<Message>
    suspend fun rename(title: String)
    suspend fun interrupt()
}

// :content-api
sealed interface Content {
    data class Text(val body: String) : Content
    data class Image(val data: ByteArray, val mime: String) : Content
}
enum class MessageOrigin { USER, SYSTEM, EVENT }
data class MessageContext(origin: MessageOrigin, description: String?, sourceId: String?, metadata: JsonElement?)

// :proto
sealed interface Message {
    val id: String
    val date: Instant
    class UserMessage(id, content: List<Content>, date, context: MessageContext?) : Message
    class AssistantMessage(id, content: List<Content>, date, tokens: TurnTokens?, reasoning: String?) : Message
    class ToolCall(...) : Message
    class ToolResult(...) : Message
    class Error(...) : Message
}

// :outbox-api — durable (перезапрашиваются по курсору `after`)
sealed interface Event {
    val date: Instant
    class UserMessage(date, id, content: List<Content>, context: MessageContext?) : Event
    class AssistantMessage(date, id, content: List<Content>, reasoning: String?, tokens: TurnTokens?) : Event
    class ToolCall(...) : Event
    class ToolResult(...) : Event
    class ToolFailed(...) : Event
    class Interrupted(date) : Event
    class Error(date, message, code) : Event
    class ConversationClosing(...) : Event
    class CompactionTriggered(...) : Event
}

// :outbox-api — online (live-only, НИКОГДА не сохраняются)
sealed interface OnlineEvent {
    val date: Instant
    enum ResponseType { TEXT, IMAGE }
    class Working(date) : OnlineEvent
    class End(date) : OnlineEvent
    class StartReasoning(date) : OnlineEvent
    class StartResponse(date, responseType: ResponseType) : OnlineEvent
    class AppendText(date, body: String) : OnlineEvent
    class AppendImage(date, body: ByteArray, mime: String) : OnlineEvent
}

Ход в терминах маркеров: онлайн Working → … → онлайн End; durable — UserMessage в начале и AssistantMessage/Interrupted/Error в конце.

Чего здесь НЕТ

  • Никакого HTTP/SSE/JSON. Это контракт. Сериализация живёт в :server и :client.
  • Никакого хранения. Реализации JournalStore живут в :storage-*.
  • Никакой логики прерывания / инструментов / LLM-вызовов. Это всё внутри :standalone (ChatAgent) и выше.

Тесты

./gradlew :proto:jvmTest
./gradlew :proto:allTests     # дополнительно linuxX64 (если Linux) / iosSimulator (если macOS)

Текущий статус

Используется продакшеном. Иммутабельный API (после рефакторинга из AG-UI). Возможные будущие расширения: typed tool-result, multi-modal contents, server-pushed references — все обсуждаются через общий IRC-QUESTIONS.md.