1134e32ea2
ci / JVM build + tests (push) Failing after 1m16s
Документирует:
- Три принципа дизайна (TTL внутри, catchup+live в одном Flow,
read-only контракт для observer'ов)
- Архитектуру двухуровневого хранилища событий со схемой
- Reconnect pattern с gap detection
- API EventStore + MutableEventStore (когда какой использовать)
- Таблица: какой caller принимает какой интерфейс
- Текущее состояние: interfaces готовы, implementations в работе
- Зависимости (минимальные: :proto + kotlinx-coroutines)
В том же стиле что и :agent-toolsets/README.md.
125 lines
5.5 KiB
Markdown
125 lines
5.5 KiB
Markdown
# `:event-store` — bounded-tail event log (KMP)
|
||
|
||
## Что это
|
||
|
||
Двухуровневое хранилище событий агента. Этот модуль — **короткий
|
||
bounded tail** для live-SSE и недавнего replay. Полный audit log
|
||
живёт в `:message-store-api` (никогда не эвиктится, source of truth).
|
||
|
||
Три принципа:
|
||
|
||
1. **Tail управляет TTL сам.** Никаких `prune`/`cleanup` методов наружу —
|
||
implementation решает, когда выкинуть старый event. Caller'ы не
|
||
могут забыть cleanup.
|
||
2. **Catchup + live в одном Flow.** `events(after)` сначала отдаёт
|
||
буферизованный диапазон, потом переключается на live tail — клиент
|
||
не должен знать, где у него "разрыв".
|
||
3. **Read-only контракт для consumer'ов.** Запись через
|
||
[MutableEventStore], чтение через [EventStore]. Compile-time
|
||
гарантия что observer не сможет писать в store.
|
||
|
||
## Где используется
|
||
|
||
- `:standalone` ChatAgent — append через `MutableEventStore` (заменяет
|
||
текущий `agentEvents: MutableSharedFlow` + `persistAgentEvent`).
|
||
- `:server` Routes.kt — `/events/all` SSE endpoint читает через
|
||
`EventStore.events(after)`.
|
||
- Будущий `:android-agent` core — same интерфейс для локального
|
||
bounded tail без dedicated server connection.
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
┌─ :event-store (этот модуль) ────────────────────────┐
|
||
│ Bounded tail с auto-TTL: │
|
||
│ • append(event) ← producer │
|
||
│ • events(after): Flow ← consumer │
|
||
│ • earliestEventDate() для gap detection │
|
||
│ TTL/cap eviction — внутри impl │
|
||
└───────────────────────────────────────────────────┘
|
||
▲ gap detected
|
||
│
|
||
┌─ :message-store-api (полный audit log) ───────────┐
|
||
│ MessageStore: query(after, before, limit) │
|
||
│ Никогда не эвиктится. Source of truth. │
|
||
└───────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Reconnect pattern** (caller делает):
|
||
|
||
```kotlin
|
||
val earliest = eventStore.earliestEventDate()
|
||
if (client.lastSeen < earliest) {
|
||
// gap: догоняем через :message-store-api
|
||
val gap = messageStore.query(after = client.lastSeen, before = earliest)
|
||
applyAll(gap)
|
||
client.lastSeen = gap.last().createdAt
|
||
}
|
||
eventStore.events(after = client.lastSeen).collect { apply(it) }
|
||
```
|
||
|
||
## API
|
||
|
||
### `EventStore` (read-only, для consumer'ов)
|
||
|
||
```kotlin
|
||
interface EventStore : AutoCloseable {
|
||
fun events(after: Instant?): Flow<CommonEvent>
|
||
suspend fun earliestEventDate(): Instant // non-null: now() для пустого буфера
|
||
override fun close()
|
||
}
|
||
```
|
||
|
||
### `MutableEventStore : EventStore` (для producer'ов)
|
||
|
||
```kotlin
|
||
interface MutableEventStore : EventStore {
|
||
suspend fun append(event: CommonEvent)
|
||
}
|
||
```
|
||
|
||
**Append НЕ идемпотентен**: [CommonEvent] не имеет уникального id,
|
||
retry даст дубликат. Для exactly-once — dedup через
|
||
`:message-store-api` (там есть монотонный `id`).
|
||
|
||
**Silently evicted**: implementation может выкинуть event сразу после
|
||
append по TTL/cap. Producer не должен полагаться на то, что event
|
||
дойдёт до клиента, если он вне retention window.
|
||
|
||
## Когда использовать какой интерфейс
|
||
|
||
| Caller | Interface |
|
||
|---|---|
|
||
| ChatAgent (producer) | `MutableEventStore` |
|
||
| Sub-agents (producer) | `MutableEventStore` |
|
||
| Server SSE endpoint | `EventStore` |
|
||
| Admin dashboard | `EventStore` |
|
||
| Parent orchestrator | `EventStore` |
|
||
| Тесты | `EventStore` (read-only) |
|
||
|
||
## Как добавить новый implementation
|
||
|
||
1. Создать класс с конструктором и lifecycle (`close()` обязан
|
||
освободить ресурсы).
|
||
2. Реализовать минимум: append (с TTL eviction), events (Flow с
|
||
catchup + live), earliestEventDate (non-null Instant, now() если
|
||
буфер пуст).
|
||
3. Для persistent impl: SQL/ksqlite таблица с индексом по date,
|
||
вставка = `INSERT OR IGNORE` для дедупликации на уровне БД
|
||
(если в схеме будет id).
|
||
|
||
## Текущее состояние
|
||
|
||
- ✅ Interface дизайн (`EventStore` + `MutableEventStore`)
|
||
- ✅ KMP build (jvm + linuxX64 + mingwX64)
|
||
- ⏳ Нет implementations (next: `InMemoryEventStore` для тестов)
|
||
- ⏳ Не интегрирован в `:standalone`/`:server`
|
||
|
||
## Зависимости
|
||
|
||
- `:proto` (api) — тип `CommonEvent` (3 AgentEvent + 9 Conversation.Event вариантов).
|
||
- `kotlinx-coroutines-core` (api) — `Flow`.
|
||
|
||
Никаких `kotlinx-serialization`, `kotlin-logging`, platform-specific
|
||
зависимостей — этот модуль намеренно minimal.
|