refactor: migrate EventStore and MessageStore to :outbox-api and :journal-api
ci / JVM build + tests (push) Failing after 1m9s

- Replaced usages of `:message-store-api` and `:working-memory-api` with `:journal-api`, `:outbox-api`, and `:context-api`.
- Deprecated legacy `EventStore` and `MessageStore` interfaces, added `typealias` for backward compatibility.
- Updated imports across all modules with references to `:journal-api` and `:outbox-api`.
- Introduced `journalRoutes` and `outboxRoutes` in `:server` for audit log and live event stream endpoints.
- Adjusted `Agent` to expose read-only `journal` and `outbox` stores for improved modularity and clarity.
- Removed legacy Event and AgentEvent definitions from `:proto`, migrated to `:outbox-api`.
- Storage-related modules have been updated to support the new APIs consistently.
This commit is contained in:
2026-09-21 02:38:40 +03:00
parent 499db812ef
commit aef5083801
64 changed files with 978 additions and 960 deletions
@@ -4,23 +4,17 @@ import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.delete
import io.ktor.client.request.get
import io.ktor.client.request.prepareGet
import io.ktor.client.request.parameter
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsChannel
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking
import pw.binom.agentik.eventStore.EventStore
import pw.binom.agentik.journal.JournalStore
import pw.binom.agentik.outbox.OutboxStore
import pw.binom.agentik.proto.Agent
import pw.binom.agentik.proto.AgentEvent
import pw.binom.agentik.proto.CommonEvent
import pw.binom.agentik.proto.Conversation
import kotlin.time.Instant
/**
* HTTP-реализация [Agent]. Ходит в `:server`-фасад, см. `agentikAgent(...)`.
@@ -30,6 +24,12 @@ import kotlin.time.Instant
* POST `/conversations`. Используем `runBlocking` — это одноразовая
* операция (открытие чата), не горячий путь. В UI-контексте вызывающий сам
* решает, что делать.
*
* **Storage handles** ([journal], [outbox]) — read-only views на серверные
* хранилища. [outbox] уже реализован ([HttpEventStore]); [journal] —
* заглушка, потому что соответствующий HTTP endpoint'ы (`/journal/...`)
* ещё не выставлены на стороне `:server`. После их добавления подменить
* `error(...)` на `HttpJournalStore(...)`.
*/
internal class AgentClient(
private val httpClient: HttpClient,
@@ -43,7 +43,16 @@ internal class AgentClient(
* Единый канал событий (lifecycle + per-conversation). Под капотом —
* [HttpEventStore]: каждый метод бьёт свой URL (см. KDoc).
*/
val eventStore: EventStore = HttpEventStore(httpClient = httpClient, baseUrl = agentUrl)
override val outbox: OutboxStore = HttpEventStore(httpClient = httpClient, baseUrl = agentUrl)
/**
* HTTP-фасад для journal пока не реализован: на стороне `:server` ещё
* не выставлены endpoint'ы `/journal/conversations/{id}/messages`.
* Как только появятся — заменить на `HttpJournalStore(httpClient, agentUrl)`.
*/
override val journal: JournalStore = error(
"HttpJournalStore ещё не реализован — дождаться :server endpoint'а /journal/...",
)
override fun createConversation(temp: Boolean): Conversation =
runBlocking {
@@ -73,35 +82,4 @@ internal class AgentClient(
}.body<List<ConversationSnapshot>>()
return snapshots.map { ConversationClient(httpClient, agentUrl, it) }
}
override fun events(after: Instant): Flow<AgentEvent> = flow {
httpClient.prepareGet("$agentUrl/events?after=$after") { noSseReadTimeout() }
.execute { response ->
check(response.status == HttpStatusCode.OK) {
"events: server returned ${response.status}"
}
readSse(response.bodyAsChannel())
.collect { payload ->
emit(agentikJson.decodeFromString(AgentEvent.serializer(), payload))
}
}
}
/**
* Подписка на ВСЕ события: agent lifecycle + все conversation events.
* Использует SSE endpoint /events/all.
*/
override fun allEvents(after: Instant): Flow<CommonEvent> = flow {
httpClient.prepareGet("$agentUrl/events/all?after=$after") { noSseReadTimeout() }
.execute { response ->
check(response.status == HttpStatusCode.OK) {
"allEvents: server returned ${response.status}"
}
readSse(response.bodyAsChannel())
.collect { payload ->
emit(agentikJson.decodeFromString(CommonEvent.serializer(), payload))
}
}
}
}
@@ -7,14 +7,15 @@ import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import pw.binom.agentik.eventStore.EventStore
import pw.binom.agentik.proto.AgentEvent
import pw.binom.agentik.proto.CommonEvent
import pw.binom.agentik.proto.Event
import pw.binom.agentik.outbox.AgentEvent
import pw.binom.agentik.outbox.CommonEvent
import pw.binom.agentik.outbox.Event
import kotlin.time.Clock
import kotlin.time.Instant
/**
* HTTP-реализация [EventStore], ходящая в `:server`-фасад.
* HTTP-реализация [EventStore] (= [pw.binom.agentik.outbox.OutboxStore]),
* ходящая в `:server`-фасад.
*
* **Хитрый план**: вместо того, чтобы все методы шли в один общий endpoint и
* фильтровали client-side ([EventStore.events]/[filterIsInstance]), эта
@@ -36,6 +37,12 @@ import kotlin.time.Instant
* (см. KDoc [EventStore.earliestEventDate] — для пустого буфера это и есть
* контрактное значение). Клиент, который полагался на gap detection через
* message store, продолжит работать — просто fallback никогда не сработает.
*
* **Импорты [CommonEvent]/[AgentEvent]/[Event] идут напрямую из
* `pw.binom.agentik.outbox`** — typealias'ы в `:proto.CommonEvent` и т.п.
* НЕ поддерживают nested-class access (`CommonEvent.Agent` через alias
* даёт "Unresolved qualified name"), поэтому приходится использовать
* конкретный пакет. Типы идентичны, alias только для удобства внешнего API.
*/
internal class HttpEventStore(
private val httpClient: HttpClient,