refactor(storage): drop StorageBundle + legacy EventStore, add HttpEventStore
ci / JVM build + tests (push) Successful in 6m13s

- Remove legacy pw.binom.agentik.messageStore.events.EventStore (EventRecord,
  EventType) and all three impls (in-memory, sqlite, ksqlite) + tests + .sq
- Drop :storage-bundle module entirely; ChatAgent / ChatConversation /
  ConversationLoop / DebugRoutes now take stores individually
  (conversationStore, messageStore, workingMemoryStore, reflectionStore,
  eventStore) instead of StorageBundle
- Delete server endpoints /events/replay and /conversations/{id}/events/replay;
  Route.agentikAgent no longer takes eventStore param
- Add :client/HttpEventStore implementing :event-store/EventStore over HTTP:
  events() -> GET /events/all, agentEvents() -> GET /events,
  conversationEvents(convId) -> GET /conversations/{id}/events;
  exposed via AgentClient.eventStore
- :event-store: add macosX64/macosArm64/linuxArm64 targets to match :client KMP
- :working-memory-api: drop api dep on :message-store-api (no longer needed)
- :storage-{inmemory,sqlite,ksqlite}: drop deps on :storage-bundle
This commit is contained in:
2026-09-20 18:06:46 +03:00
parent 15f3952eba
commit d74af621d8
36 changed files with 313 additions and 1259 deletions
@@ -6,7 +6,6 @@ import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.routing.Route
import io.ktor.server.routing.route
import pw.binom.agentik.proto.Agent
import pw.binom.agentik.messageStore.events.EventStore
/**
* Встраивает HTTP/SSE-фасад протокола agentik в твой Ktor-роутинг.
@@ -30,18 +29,19 @@ import pw.binom.agentik.messageStore.events.EventStore
* - `POST /conversations/{id}/messages` — `send` (202 Accepted)
* - `POST /conversations/{id}/interrupt` — `interrupt`
* - `GET /conversations/{id}/messages` — история
* - `GET /conversations/{id}/events` — SSE: события хода
* - `GET /conversations/{id}/events/replay` — replay-after-disconnect (events after ?after_id=X)
* - `GET /events` — SSE: события агента
* - `GET /events/replay` — replay-after-disconnect (global)
* - `GET /conversations/{id}/events` — SSE: события хода (catchup + live)
* - `GET /events` — SSE: события агента (catchup + live)
* - `GET /events/all` — SSE: всё в одном потоке
* - `GET /health` — `"ok"`
*
* @param eventStore если null — `/events/replay` endpoints возвращают 503 (event
* persistence не настроен). Live-streaming работает как обычно.
* Event-эндпоинты сами по себе дают catchup + live в одном Flow: клиент
* передаёт `?after=<ISO8601>`, сервер сначала отдаёт буферизованные события
* с `date > after`, потом переключается на live. Отдельный replay-endpoint
* не нужен — для событий старше буфера клиент должен идти в
* `:message-log-api` (полный audit log).
*/
fun Route.agentikAgent(
agent: Agent,
eventStore: EventStore? = null,
path: String = "/agentik",
token: String? = null,
) {
@@ -54,6 +54,6 @@ fun Route.agentikAgent(
this.token = token
}
}
agentikRoutes(agent, eventStore)
agentikRoutes(agent)
}
}
@@ -24,10 +24,9 @@ import pw.binom.agentik.proto.CommonEvent
import pw.binom.agentik.proto.Conversation
import pw.binom.agentik.proto.Content
import pw.binom.agentik.proto.Event
import pw.binom.agentik.messageStore.events.EventStore
import kotlin.time.Instant
internal fun Route.agentikRoutes(agent: Agent, eventStore: EventStore? = null) {
internal fun Route.agentikRoutes(agent: Agent) {
get("/health") {
call.respondText("ok")
@@ -145,38 +144,6 @@ internal fun Route.agentikRoutes(agent: Agent, eventStore: EventStore? = null) {
val after = call.parseAfter() ?: return@get
call.streamJsonSse(agent.allEvents(after), CommonEvent.serializer())
}
// ---- Replay-after-disconnect (event log) ----
//
// Stream `/events` и `/conversations/{id}/events` — cold (no replay).
// Клиент, отвалившийся от SSE, при reconnect делает GET на replay-endpoint
// с `?after_id=X` чтобы получить events, которые произошли во время разрыва.
// paginated: делает несколько запросов пока `result.size < limit`.
//
// Если [eventStore] == null (не сконфигурирован), эти endpoints возвращают 503.
get("/events/replay") {
if (eventStore == null) {
call.respond(HttpStatusCode.ServiceUnavailable, "EventStore not configured on this agent")
return@get
}
val afterId = call.request.queryParameters["after_id"]?.takeIf { it.isNotBlank() }
val limit = (call.request.queryParameters["limit"]?.toIntOrNull() ?: 100).coerceIn(1, 1000)
val records = eventStore.query(conversationId = null, afterId = afterId, limit = limit)
call.respond(records)
}
get("/conversations/{id}/events/replay") {
if (eventStore == null) {
call.respond(HttpStatusCode.ServiceUnavailable, "EventStore not configured on this agent")
return@get
}
val id = call.parameters["id"]!!
val afterId = call.request.queryParameters["after_id"]?.takeIf { it.isNotBlank() }
val limit = (call.request.queryParameters["limit"]?.toIntOrNull() ?: 100).coerceIn(1, 1000)
val records = eventStore.query(conversationId = id, afterId = afterId, limit = limit)
call.respond(records)
}
}
// ---------- helpers ----------