docs(client): add «Кэш списка бесед» section + ios targets to :journal-inmemory
- :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).
This commit is contained in:
@@ -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()` и
|
||||
|
||||
@@ -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<ConversationRecord>` (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<ConversationRecord>()
|
||||
}
|
||||
|
||||
override suspend fun list(offset: Int, limit: Int): List<ConversationRecord> {
|
||||
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<List<ConversationRecord>>()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
// HttpClient закрывает владелец (AgentClient / AgentikAgent).
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ kotlin {
|
||||
jvm()
|
||||
macosX64()
|
||||
macosArm64()
|
||||
iosX64()
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
linuxX64()
|
||||
linuxArm64()
|
||||
mingwX64()
|
||||
|
||||
Reference in New Issue
Block a user