Files
agentik/server
subochev 4ad59d5f5d
ci / JVM build + tests (push) Has been cancelled
release / Publish KMP libraries → caffeine Nexus (release) Successful in 32s
feat(events): EventStore + AllEvent unified stream + replay endpoints
EventStore (persistent event log) и AllEvent (sealed wrapper для
третьего типа подписки — ВСЕ events в одном потоке). Touches 7 modules.

Архитектура:
  Producer (ChatAgent + ConversationEvents) → EventStore + SharedFlow
  ↓                                            ↓
  Live SSE (cold, no replay)         Replay endpoints (cursor-based)

(1) :storage-core — EventStore interface
  - append(record): idempotent по record.id (INSERT OR IGNORE)
  - query(conversationId?, afterId?, limit): пагинированный catchup
  - pruneOlderThan(instant): TTL cleanup
  - count(): maintenance метрика
  - @Serializable EventRecord(id, conversationId?, createdAt, type, payload)
  - enum EventType: AGENT_*/CONVERSATION_* (forward-compat fallback)
  - StorageBundle дополнен eventStore: EventStore? = null (backward-compat)

(2) :storage-inmemory — InMemoryEventStore
  - Thread-safe (Mutex), binarySearch для упорядоченной вставки
  - Записи сортируются по createdAt ASC, ties по id ASC (стабильно)
  - Idempotency по id (повторный append no-op)

(3) :storage-sqlite — SqliteEventStore
  - sqldelight schema: agent_event (id PK, conversation_id?, created_at,
    type, payload BLOB) + 2 индекса (conversation_id+created_at,
    created_at)
  - Миграция v3: CREATE TABLE IF NOT EXISTS (additive)
  - 5 запросов: insert, queryGlobal, queryByConv, pruneOlderThan, count
  - Forward-compat: неизвестный EventType в БД → fallback AGENT_CREATED
    (чтобы старые клиенты не падали на новых enum values)
  - Добавлен в SqliteStores (open/inMemory + asBundle())

(4) :standalone — Producer wiring
  - ChatAgent.persistAgentEvent() — fire-and-forget append при каждом
    AgentEvent (Created/Deleted/Renamed)
  - ConversationEvents — персистит в EventStore при каждом tryEmit/emit
    (концертный случай от connect disconnect)
  - ChatAgent.allEvents() — merge agent-events + snapshot всех живых
    диалогов в единый Flow<AllEvent>

(5) :proto — AllEvent sealed interface
  - AllEvent.Agent(date, event: AgentEvent)
  - AllEvent.Conversation(date, conversationId, event: Event)
  - Agent.allEvents(after): Flow<AllEvent> — третий тип подписки
    (в дополнение к events() и Conversation.events)

(6) :server — Endpoints
  - GET /events/all — SSE поток AllEvent (cold)
  - GET /events/replay?after_id=&limit= — пагинированный catchup
    (503 если EventStore не сконфигурирован)
  - GET /conversations/{id}/events/replay?after_id=&limit= — то же per-conv
  - Module.kt принимает eventStore: EventStore? параметром

(7) :client — Client API
  - AgentClient.allEvents(after) — подписка на /events/all SSE
  - AgentClient.replayAllEvents(afterId, limit) — catchup /events/replay
  - AgentClient.replayConversationEvents(convId, afterId, limit)
  - EventRecordDto — wire-зеркало EventRecord (клиент не зависит
    от :storage-core, определяет DTO локально; формат совместим с
    серверным JSON)

Тесты: 22 новых теста (12 InMemory + 10 Sqlite), все зелёные.
Все три слоя синхронизированы: proto contract + standalone impl +
server endpoint + client API.
2026-09-20 02:51:58 +03:00
..

:server — HTTP/SSE фасад для :proto (KMP, JVM-only)

Что это

Ktor-маршрут, экспонирующий Agent из :proto в виде JSON-API: POST /agentik/conversations, POST /agentik/conversations/:id/send, GET /agentik/conversations/:id/events (SSE), GET /health, GET /agentik/conversations.

  • stateful — сервер не принимает полную историю, только новые сообщения. История хранится там, где развёрнут Agent.
  • декларативно — interface Agent → HTTP; никакой магии, никаких обёрток. Контракт и сериализация — тоже декларативные (kotlinx-json с snake_case-дискриминаторами).

Решает: позволяет собрать любой собственный front-end (CLI/TUI/Web/ IRC/MCP) общаясь с одним сервером по стабильному wire-контракту.

Где используется

  • :standalone подключает Route.agentikAgent(agent) в свой embedded Netty engine.
  • Любые клиенты (наши :client, :agentik-cli, или внешние web-фронтенды) идут через этот контракт.

Как подключить

// build.gradle.kts (KMP JVM target)
plugins { id("pw.binom.agentik.server-conventions") version "0.1.0" }
dependencies {
    api("pw.binom.agentik:server:0.1.0")
    api("pw.binom.agentik:proto:0.1.0")
}

// ваш код:
fun Application.module(agent: Agent) {
    install(ContentNegotiation) { json(agentikJson) }
    install(SSE)
    routing {
        route("/agentik") { agentikAgent(agent) }
    }
}

Версии

gradle/libs.versions.toml → [versions] agentik-server.

Эндпоинты (path по умолчанию /agentik, через agentikAgent(agent, "/my"))

Метод Путь Что делает
POST /conversations Создать диалог (body: {title?})
GET /conversations Список диалогов (по ?offset=&limit=)
GET /conversations/:id Снимок диалога + count
GET /conversations/:id/messages История сообщений (по ?after=)
POST /conversations/:id/rename Переименовать (body: {title})
DELETE /conversations/:id Удалить
POST /conversations/:id/send Send-флоу (body: {content:[…]} → SSE)
GET /conversations/:id/events Live подписка (SSE)
POST /conversations/:id/interrupt Прервать текущий send()

Content-Type: text/event-stream всегда для SSE, ноль-лишних заголовков. Сообщения: event: <name> (message, start_reasoning, start_response, append_text, append_image, end, interrupted, error) + data: <JSON>.

Тесты

./gradlew :server:jvmTest

Покрывают: маппинг JSON ↔ Event, SSE framing, error-handling, 404 / 400 ответы, корректную обработку Instant в kotlinx-datetime.

Чего здесь НЕТ

  • Никакого LLM-кода, tool-вызовов, прерываний. Только mapping Agent ↔ HTTP.
  • Никакой БД, никакого storage. Это задача Agent-имплементации.
  • Никакого CORS-конфига по умолчанию — добавляйте на свой engine.

Текущий статус

Используется продакшеном. Wire-контракт стабильный; новые Event'ы добавляются только с snake_case-дискриминаторами и строго обратно совместимо.