refactor: migrate EventStore and MessageStore to :outbox-api and :journal-api
ci / JVM build + tests (push) Failing after 1m9s
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:
@@ -1,145 +1,16 @@
|
||||
package pw.binom.agentik.eventStore
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlin.time.Instant
|
||||
import pw.binom.agentik.proto.CommonEvent
|
||||
|
||||
/**
|
||||
* Bounded-tail event log с автоматическим управлением TTL.
|
||||
* @Deprecated
|
||||
* Перенесено в `:outbox-api`. Используй `pw.binom.agentik.outbox.OutboxStore`.
|
||||
*
|
||||
* **Архитектура двухуровневого хранилища событий**:
|
||||
* 1. **Этот store** = короткий bounded tail (live SSE + недавний replay).
|
||||
* События автоматически эвиктятся по TTL/cap (implementation-defined).
|
||||
* 2. **Message store (`:message-store-api`)** = полный audit log, никогда не
|
||||
* эвиктится. Source of truth для всего прошлого.
|
||||
* Backward-compat typealias. Существующие импорты `pw.binom.agentik.eventStore.EventStore`
|
||||
* продолжают работать; новый код в `:server` использует `:outbox-api` напрямую.
|
||||
*
|
||||
* **Паттерн reconnect** (caller'ы):
|
||||
* ```
|
||||
* val earliest = store.earliestEventDate()
|
||||
* if (client.lastSeen < earliest) {
|
||||
* // gap обнаружен — идём в message store за прошлым
|
||||
* val gap = messageStore.query(after = client.lastSeen, before = earliest)
|
||||
* applyAll(gap)
|
||||
* }
|
||||
* store.events(after = client.lastSeen).collect { apply(it) }
|
||||
* ```
|
||||
*
|
||||
* **Нет delete/cleanup методов** — TTL/cap eviction полностью на стороне
|
||||
* implementation. Это:
|
||||
* - Убирает single source of truth дублирование (caller не может забыть cleanup).
|
||||
* - Позволяет impl выбирать retention strategy (TTL, size cap, sliding window).
|
||||
* - Сохраняет контракт clean: интерфейс только о put/get.
|
||||
*
|
||||
* **Read-only**: этот интерфейс предоставляет только read-операции.
|
||||
* Для записи см. [MutableEventStore].
|
||||
*
|
||||
* **Подписки нереентрантные**: каждый вызов [events] создаёт **новую
|
||||
* подписку** (cold Flow). Один [events] НЕ видит события, добавленные до
|
||||
* его вызова, если [after] == null. Если нужен catchup — передавайте
|
||||
* `after = lastSeenDate` явно.
|
||||
*
|
||||
* **Multi-consumer**: разные [events] подписки видят одно и то же live
|
||||
* tail. Каждая подписка — независимая projection.
|
||||
* Удалить когда все импорты будут на `:outbox-api`.
|
||||
*/
|
||||
interface EventStore : AutoCloseable {
|
||||
|
||||
/**
|
||||
* Subscribe на events.
|
||||
*
|
||||
* **`after == null`** → только **live** (события с момента вызова
|
||||
* `events()`). Каждое новое событие от любого producer'а немедленно
|
||||
* появится в Flow. Буфер replay не отдаётся.
|
||||
*
|
||||
* **`after != null`** → сначала **catchup**: эмитт все буферизованные
|
||||
* события с `date > after`, порядок `date ASC` (ties по `id ASC`).
|
||||
* Затем **live** (как null-case).
|
||||
*
|
||||
* Cold Flow: каждый вызов — новая подписка. Вызов **после** append'а
|
||||
* не увидит этот конкретный event (если `after == null`); для catchup
|
||||
* передавайте явный `after`.
|
||||
*
|
||||
* ВАЖНО: `Flow` НЕ бросает ошибку при потере сети между producer и
|
||||
* store — такие события просто не дойдут до этого Flow. Для гарантии
|
||||
* полноты клиент обязан cross-check с [earliestEventDate] и fallback
|
||||
* в message store при gap'е (см. KDoc интерфейса).
|
||||
*/
|
||||
fun events(after: Instant?): Flow<CommonEvent>
|
||||
|
||||
/**
|
||||
* Subscribe на **только conversation events** (т.е. [CommonEvent.Conversation]).
|
||||
*
|
||||
* - [conversationId] == null → события **всех** диалогов.
|
||||
* - [conversationId] != null → события **только этого** диалога.
|
||||
*
|
||||
* Семантика `after` идентична [events] (catchup + live).
|
||||
* Возвращаемый тип — конкретный subtype [CommonEvent.Conversation].
|
||||
*/
|
||||
/**
|
||||
* **Default implementation** (читает все events + фильтрует).
|
||||
*
|
||||
* Простая реализация через [events] + filterIsInstance. Реализации
|
||||
* могут override'нуть для эффективности (например, добавить SQL
|
||||
* `WHERE conversation_id = ?` чтобы не тянуть всё в память), но
|
||||
* контракт корректен и без override.
|
||||
*/
|
||||
fun conversationEvents(after: Instant?, conversationId: String? = null): Flow<CommonEvent.Conversation> =
|
||||
events(after)
|
||||
.filterIsInstance<CommonEvent.Conversation>()
|
||||
.let { filtered ->
|
||||
if (conversationId == null) filtered
|
||||
else filtered.filter { it.conversationId == conversationId }
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe на **только agent events** ([CommonEvent.Agent] —
|
||||
* создание/удаление/переименование диалога).
|
||||
*
|
||||
* Семантика `after` идентична [events] (catchup + live).
|
||||
* Возвращаемый тип — конкретный subtype [CommonEvent.Agent].
|
||||
*
|
||||
* Полезно для admin-дашборда, который хочет видеть только lifecycle
|
||||
* диалогов без деталей ходов.
|
||||
*/
|
||||
/**
|
||||
* **Default implementation** (читает все events + фильтрует по типу).
|
||||
*
|
||||
* Простая реализация через [events] + filterIsInstance. Реализации
|
||||
* могут override'нуть для эффективности (например, читать только agent
|
||||
* row'ы из БД), но контракт корректен и без override.
|
||||
*/
|
||||
fun agentEvents(after: Instant?): Flow<CommonEvent.Agent> =
|
||||
events(after).filterIsInstance<CommonEvent.Agent>()
|
||||
|
||||
/**
|
||||
* Date **стартовой точки** буфера.
|
||||
*
|
||||
* - Если буфер не пуст → `date` самого старого буферизованного event'а.
|
||||
* - Если буфер пуст → текущее время (`Clock.System.now()` на момент вызова).
|
||||
*
|
||||
* **Семантика "now если пусто"** важна: позволяет клиенту безопасно
|
||||
* подписаться на [events](after = earliest) сразу — он получит только
|
||||
* новые live event'ы, без ложного catchup. Если бы возвращалось
|
||||
* `Instant.DISTANT_PAST` или `null` (с проверкой), клиент мог бы
|
||||
* ошибочно подписаться на несуществующий catchup и зависнуть в ожидании.
|
||||
*
|
||||
* **Используется клиентом для gap detection**:
|
||||
* - `lastSeen < earliest` → есть дыра в покрытии, нужен fallback
|
||||
* в message store за диапазоном `[lastSeen, earliest)`.
|
||||
* - `lastSeen >= earliest` → всё доступно через [events](after),
|
||||
* fallback не нужен.
|
||||
* - `lastSeen == earliest` → OK, первый live event будет > earliest.
|
||||
*
|
||||
* **Edge case**: клиент, подключившийся до того как store увидел хоть
|
||||
* один event, получает `earliest ≈ now`. Его `lastSeen` будет < earliest
|
||||
* — адаптируется в первом же poll'е и пойдёт через fallback если
|
||||
* сообщения audit log существуют (для consistency с прошлым).
|
||||
*
|
||||
* Suspend потому что в persistent impl'ах требует SQL query (`MIN(date)`
|
||||
* или `Clock.now()` для пустого буфера).
|
||||
*/
|
||||
suspend fun earliestEventDate(): Instant
|
||||
|
||||
override fun close()
|
||||
}
|
||||
@Deprecated(
|
||||
message = "Перенесено в :outbox-api. Используй pw.binom.agentik.outbox.OutboxStore.",
|
||||
replaceWith = ReplaceWith("OutboxStore", "pw.binom.agentik.outbox.OutboxStore"),
|
||||
)
|
||||
typealias EventStore = pw.binom.agentik.outbox.OutboxStore
|
||||
|
||||
@@ -1,52 +1,15 @@
|
||||
package pw.binom.agentik.eventStore
|
||||
|
||||
import pw.binom.agentik.proto.CommonEvent
|
||||
|
||||
/**
|
||||
* Mutable вариант [EventStore] — добавляет producer-операцию [append].
|
||||
* @Deprecated
|
||||
* Перенесено в `:outbox-api`. Используй `pw.binom.agentik.outbox.MutableOutboxStore`.
|
||||
*
|
||||
* Этот интерфейс предназначен **только для producer'ов** (ChatAgent,
|
||||
* sub-agents, A2A-bridge). Consumer'ы (server SSE endpoints, admin
|
||||
* dashboards, parent agents) должны принимать **read-only** [EventStore]
|
||||
* — тогда невозможно случайно писать в store из observer'а.
|
||||
* Backward-compat typealias. `MutableEventStore` == `MutableOutboxStore` (один тип).
|
||||
*
|
||||
* Типичное использование:
|
||||
* ```
|
||||
* // Producer
|
||||
* class ChatAgent(private val events: MutableEventStore) {
|
||||
* suspend fun doSomething() {
|
||||
* events.append(CommonEvent.Agent(date = now, event = AgentEvent.Created(...)))
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // Consumer
|
||||
* class EventStreamEndpoint(private val events: EventStore) {
|
||||
* fun stream() = events.events(after = null)
|
||||
* // Ошибка компиляции если раскомментировать:
|
||||
* // events.append(...) // ← нельзя, MutableEventStore нет в типе
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* **Append НЕ идемпотентен**: [CommonEvent] не имеет уникального id,
|
||||
* поэтому retry с тем же logical event (например, после network failure
|
||||
* между producer и store) приведёт к дубликату в tail'е. Это OK для
|
||||
* use case'a bounded-tail — клиент, делающий catchup через [events](after),
|
||||
* получит свой диапазон ровно один раз при подключении, а последующие
|
||||
* retry producer'а просто насытят tail повторами, не задевая уже
|
||||
* обработанные. Для гарантированной exactly-once — dedup через
|
||||
* [message-store] (там есть монотонный `id`).
|
||||
*
|
||||
* **Silently evicted**: implementation может выкинуть этот event сразу
|
||||
* после append (TTL/cap) без уведомления producer'а. Producer **не
|
||||
* должен** полагаться на то, что event дойдёт до клиента, если он
|
||||
* вне retention window.
|
||||
* Удалить когда все импорты будут на `:outbox-api`.
|
||||
*/
|
||||
interface MutableEventStore : EventStore {
|
||||
/**
|
||||
* Положить event в log.
|
||||
*
|
||||
* - **Не идемпотентно** — см. KDoc интерфейса.
|
||||
* - **Suspend** для KMP I/O impl'ов (SQLite через JNI).
|
||||
*/
|
||||
suspend fun append(event: CommonEvent)
|
||||
}
|
||||
@Deprecated(
|
||||
message = "Перенесено в :outbox-api. Используй pw.binom.agentik.outbox.MutableOutboxStore.",
|
||||
replaceWith = ReplaceWith("MutableOutboxStore", "pw.binom.agentik.outbox.MutableOutboxStore"),
|
||||
)
|
||||
typealias MutableEventStore = pw.binom.agentik.outbox.MutableOutboxStore
|
||||
|
||||
Reference in New Issue
Block a user