Files
agentik/server
subochev 9e888227a3 refactor: outbox становится live-only стримом, cursor вынесен в :cursor-api
Семантика outbox'а — теперь чистый live-канал:
  * OutboxStore.conversationEvents(id) — больше не принимает after-cursor.
    Catchup (replay) делает клиент: journal.listFlow(afterSeq) + подписка
    на live. Outbox ответственен только за уведомления «что-то произошло».
  * OutboxStore.oldestCursor()/currentCursor()/OutboxGapException — удалены.
    Эпоха и offset живут ТОЛЬКО в CursorHolder/CursorStore, переживают
    рестарт и инкремент для каждого commit.
  * DurableEvent: commit принимает блок { cursor -> MessageRecord }
    (cursor выдаёт CursorStore; клиент не вычисляет offset сам).
  * MessageRecord больше не несёт cursor — это не его ответственность.

Новые модули:
  * :cursor-api         — Cursor(epoch, offset) + CursorHolder / MutableCursorHolder
  * :cursor-ksqlite     — KsqliteCursorHolder (персистентный)
  * :cursor-inmemory    — для тестов
  * :client-sync        — LocalSyncAgent (мини-агент поверх :client для десктопа)

Удалены:
  * :sync-core          — старая референсная реализация, заменена
                           cursor-разделением и :client.
  * outbox-ksqlite      — CursorStore/Schema уехали в :cursor-ksqlite.
  * OffsetSequencer / PersistentOffsetSequencer / InMemoryOffsetSequencer.

standalone:
  * ChatAgent/ConversationLoop/DurableLog/ToolDispatcher/ReflectionScheduler/
    ConversationEvents — подписка через push-паттерн (collect событий).
  * A2aBridge — currentCursor() и conversationEvents(after=) убраны.
  * SqliteStores — cursor_offset удалён из schema v4; seedNextFromJournal
    читает MAX(created_at).
  * Main.kt — outboxSequencer → outboxCursorHolder; user→agent (:server)
    transport удалён; debug-routes удалены; A2A остался.
  * Тесты ChatAgentTest/PersistenceTest переписаны на push-паттерн
    (subscribe-before-act, snapshot∪live = итоговое состояние). 25/25 + 19/19 ✅

server / client:
  * Routes эпоху читают из CursorHolder; снимки несут Cursor? для catchup.
  * AgentikAgent и HttpEventStore — те же подписки, без after-параметра.
  * ReconnectingOutbox / ReconnectingOutboxTest — без изменений API.
  * JournalStore API расширен count(after=Instant?) для unread-badge.
2026-10-05 23:33:35 +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-дискриминаторами и строго обратно совместимо.