From 639c7d1748740708bdd96eb8c1dced3613e71027 Mon Sep 17 00:00:00 2001 From: subochev Date: Tue, 22 Sep 2026 05:25:39 +0300 Subject: [PATCH] =?UTF-8?q?docs(client):=20add=20=C2=AB=D0=9A=D1=8D=D1=88?= =?UTF-8?q?=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B1=D0=B5=D1=81?= =?UTF-8?q?=D0=B5=D0=B4=C2=BB=20section=20+=20ios=20targets=20to=20:journa?= =?UTF-8?q?l-inmemory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :journal-inmemory — added iosX64/iosArm64/iosSimulatorArm64 to the target set so :storage-inmemory (which now depends on it) can build for iOS. Pure `MutableMap`+`Mutex` impl, no I/O, fully portable. - client/README.md — new «Кэш списка бесед» section: - shows `agent.conversationStore` as the read-only entry point; - demonstrates the «remote.listFlow → local.upsert + outbox.agentEvents → local apply» pattern (Created/Renamed/Touched/Deleted); - notes that the cache is built into `AgentikAgent` by default; - mentions `wrapWithLocalConversationCache` for custom stores (SQLite/JSON); - points to `agentikHttpClient(...).raw` as the escape-hatch for clients that want direct HTTP. Also adds `HttpConversationStore.kt` to the index (the file existed on disk but wasn't `git add`ed in the previous commit). --- client/README.md | 49 ++++++++++++++++ .../agentik/client/HttpConversationStore.kt | 58 +++++++++++++++++++ journal-inmemory/build.gradle.kts | 3 + 3 files changed, 110 insertions(+) create mode 100644 client/src/commonMain/kotlin/pw/binom/agentik/client/HttpConversationStore.kt diff --git a/client/README.md b/client/README.md index ce49c54..c9713f9 100644 --- a/client/README.md +++ b/client/README.md @@ -285,6 +285,55 @@ session.scope.launch { `rec is MessageRecord.UserMessage` для реплик пользователя, `rec is MessageRecord.ToolCall` для отрисовки tool-call баббла, и т.п. +## Кэш списка бесед + +`agent.conversationStore` — read-only view поверх `conversation`-таблицы +на сервере (`ConversationRecord` = id / title / isTemporal / createdAt / +updatedAt, без `Conversation` handle и без флагов image-support). + +**Сценарий клиента:** показать список диалогов («как в Telegram»), чтобы +при открытии UI уже знал названия, не дёргал сервер лишний раз, и +моментально реагировал на создание/удаление/переименование в другой +вкладке. + +Подход — тот же **«remote → local snapshot + live-events»**: + +```kotlin +import pw.binom.agentik.client.AgentikAgent +import pw.binom.agentik.journal.ConversationRecord +import pw.binom.agentik.outbox.AgentEvent +import io.ktor.client.engine.cio.CIO + +// `AgentikAgent` сам оборачивает HTTP-store в локальный кэш: +// remote.listFlow → local.upsert (snapshot) +// outbox.agentEvents → local.upsert / delete (live) +val agent = AgentikAgent( + id = "agentik", + baseUrl = "http://localhost:8080/agentik", + engineFactory = CIO, +) + +// Кэш уже наполняется в фоне, читать можно сразу: +val all = agent.conversationStore.list(0, Int.MAX_VALUE) +all.forEach { rec -> println("${rec.id} ${rec.title ?: "(no title)"} ${rec.updatedAt}") } + +// И наблюдать live-изменения (Created/Deleted/Renamed/Touched) +agent.outbox.agentEvents(kotlin.time.Instant.DISTANT_PAST).collect { ev -> + when (ev) { + is AgentEvent.Created -> println("+ ${ev.conversationId}") + is AgentEvent.Renamed -> println("~ ${ev.id} → ${ev.title}") + is AgentEvent.Touched -> println("↻ ${ev.id} (${ev.updatedAt})") + is AgentEvent.Deleted -> println("- ${ev.id}") + } +} +``` + +Если ты **не хочешь** встроенный кэш (например, тебе нужен прямой HTTP +для бэкенда-сервиса) — `agentikHttpClient(...).raw` оставлен как +escape-hatch. Сам `InMemoryMutableConversationStore` тоже доступен — +подмени его на свою реализацию через `wrapWithLocalConversationCache`, +если нужен SQLite/JSON-store. + ## Стриминг live-ответа Для streaming-рендера текущего хода подписывайся на `events()` и diff --git a/client/src/commonMain/kotlin/pw/binom/agentik/client/HttpConversationStore.kt b/client/src/commonMain/kotlin/pw/binom/agentik/client/HttpConversationStore.kt new file mode 100644 index 0000000..9c355f3 --- /dev/null +++ b/client/src/commonMain/kotlin/pw/binom/agentik/client/HttpConversationStore.kt @@ -0,0 +1,58 @@ +package pw.binom.agentik.client + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.http.HttpStatusCode +import pw.binom.agentik.journal.ConversationRecord +import pw.binom.agentik.journal.ConversationStore + +/** + * HTTP-реализация [ConversationStore] (read-only metadata view), + * ходящая в `:server`-фасад. + * + * **Endpoint**: `GET {baseUrl}/conversations?offset=&limit=` — + * возвращает `List` (id, title, isTemporal, createdAt, + * updatedAt) БЕЗ handle'ов и image-support флагов (это лёгкая проекция + * для UI-списка; handle берётся через `agent.getConversation(id)`). + * + * **Read-only**: запись в `conversation` table — только через команды + * `agent.createConversation / deleteConversation / renameConversation`. + * + * Клиентский кэш строится композицией `HttpConversationStore` (snapshot) + * + `agent.outbox.agentEvents(after)` (live deltas: Created/Deleted/ + * Renamed/Touched) — см. `client/README.md` секция + * «Кэш списка бесед». + */ +internal class HttpConversationStore( + private val httpClient: HttpClient, + private val baseUrl: String, +) : ConversationStore { + + private val agentUrl: String = baseUrl.trimEnd('/') + + override suspend fun get(id: String): ConversationRecord? { + val response = httpClient.get("$agentUrl/conversations/$id") + if (response.status == HttpStatusCode.NotFound) return null + check(response.status == HttpStatusCode.OK) { + "conversationStore.get($id): server returned ${response.status}" + } + return response.body() + } + + override suspend fun list(offset: Int, limit: Int): List { + val response = httpClient.get("$agentUrl/conversations") { + parameter("offset", offset) + parameter("limit", limit) + } + check(response.status == HttpStatusCode.OK) { + "conversationStore.list: server returned ${response.status}" + } + return response.body>() + } + + override fun close() { + // HttpClient закрывает владелец (AgentClient / AgentikAgent). + } +} diff --git a/journal-inmemory/build.gradle.kts b/journal-inmemory/build.gradle.kts index 41e9f7a..a83ac83 100644 --- a/journal-inmemory/build.gradle.kts +++ b/journal-inmemory/build.gradle.kts @@ -15,6 +15,9 @@ kotlin { jvm() macosX64() macosArm64() + iosX64() + iosArm64() + iosSimulatorArm64() linuxX64() linuxArm64() mingwX64()