Files
agentik/proto
subochev 5f0e0da361 feat(client): client-side caching for conversation list via agent.outbox events
Adds `agent.conversationStore` (read-only view on `conversation` table) to
the :proto Agent interface, plus `agent.renameConversation(id, title?)`
command. Client-side cache in :client is built from a snapshot
(`remote.listFlow(0)` → `local.upsert(...)`) + live updates via
`outbox.agentEvents()` (Created/Deleted/Renamed/Touched).

Changes:
- :journal-api — split `ConversationStore` (read-only: get/list) and
  `MutableConversationStore` (CRUD: upsert/delete/rename/touch);
  `ConversationStore` gained `listFlow` (cold-flow paging via `list`).
- :outbox-api — `AgentEvent.Touched(date, id, updatedAt)` event so
  client cache stays fresh after `send()` (which bumps `updatedAt`).
- :proto.Agent — added `conversationStore: ConversationStore` property,
  added `renameConversation(id, title?): Instant?` command, removed
  `getConversations(offset, limit)` (now: `conversationStore.list(...)`).
- :server — `GET /conversations` now returns `List<ConversationRecord>`
  (lightweight metadata, no handle/image-support flags); `PATCH
  /conversations/{id}` uses `agent.renameConversation` and returns
  the updated `ConversationRecord`.
- :journal-inmemory — expanded targets to jvm+macos+linux+mingw (matches
  :client); moved `InMemoryMutableConversationStore` here from
  :storage-inmemory so :client can use it without pulling ios targets.
- :storage-inmemory — depends on :journal-inmemory.
- :storage-ksqlite — pre-staged rename `KsqliteConversationStore` →
  `KsqliteMutableConversationStore` to match the new interface split.
- :standalone — `ChatAgent` exposes `conversationStore` as a read-only
  view of its `mutableConversationStore`; emits `AgentEvent.Touched`
  after each `send()` (after `conversationStore.touch(id, ts)`).
- :client — new `HttpConversationStore` (read-only HTTP impl);
  `AgentikAgent` wraps the agent with `wrapWithLocalConversationCache`
  so the client sees an in-memory cache (snapshot + outbox events)
  instead of direct HTTP. Cache scope + HttpClient + background job
  all cancelled in `agent.close()`.
- :client/README — new «Кэш списка бесед» section with the
  `listFlow → upsert` / `agentEvents → apply` pattern and a note that
  `conversationStore` is read-only (writes only via Agent commands).

All 96 jvmTest tasks green.
2026-09-22 05:21:01 +03:00
..

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

Что это

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

  • stateful — сервер сам владеет диалогом; клиент шлёт только новые сообщения, а не всю историю (в отличие от AG-UI, где клиент обязан повторять messages[] каждый раз).
  • declarative история vs. события — Message это то, что уже легло в БД, Event это 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/Event напрямую через 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.

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

interface Agent {
    fun id: String
    suspend fun createConversation(title: String? = null): Conversation
    suspend fun getConversation(id: String): Conversation?
    suspend fun getConversations(offset: Int = 0): Flow<Conversation>
    suspend fun events(after: Instant): Flow<AgentEvent>   // created/deleted/renamed
}

interface Conversation : AutoCloseable {
    val id: String
    val updatedAt: Instant
    val isSupportImageInput: Boolean
    val isSupportImageOutput: Boolean
    suspend fun send(content: List<Content>): Flow<Event>     // write+read вместе, как раньше
    suspend fun events(after: Instant): Flow<Event>           // отдельная live-подписка
    suspend fun getMessages(offset: Int = 0): Flow<Message>
    suspend fun rename(title: String): Boolean
    fun interrupt()
}

sealed interface Content {
    class Text(val body: String) : Content
    class Image(val data: ByteArray, val mime: String) : Content
}

sealed interface Message {
    val id: String
    val date: Instant
    interface Body : Message { val content: List<Content> }
    interface System : Message
    class UserMessage(...) : Body
    class AssistantMessage(...) : Body
    class ToolCall(...) : System
    class ToolResult(...) : System
}

sealed interface Event {
    enum ResponseType { TEXT, IMAGE }
    class StartReasoning(...) : Event
    class StartResponse(val type: ResponseType) : Event
    class AppendText(val body: String) : Event
    class AppendImage(val body: ByteArray, val mime: String) : Event
    class End(...) : Event
    class Interrupted(...) : Event
    class Error(val message: String, val code: Int? = null) : Event
    class ToolCall(...) : Event
    class ToolResult(...) : Event
}

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

  • Никакого 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.