Изменения вводят live-канал OnlineOutbox и фоновую синхронизацию диалогов.

This commit is contained in:
2026-09-30 11:43:49 +03:00
parent 509763cf36
commit bd7e079780
25 changed files with 491 additions and 102 deletions
@@ -25,6 +25,7 @@ import pw.binom.agentik.proto.Content
import pw.binom.agentik.outbox.AgentEvent
import pw.binom.agentik.outbox.CommonEvent
import pw.binom.agentik.outbox.Event
import pw.binom.agentik.outbox.OnlineEvent
import kotlin.time.Instant
internal fun Route.agentikRoutes(agent: Agent) {
@@ -167,6 +168,27 @@ internal fun Route.agentikRoutes(agent: Agent) {
call.streamJsonSse(agent.outbox.conversationEvents(after, id).map { it.event }, Event.serializer())
}
/**
* `GET /conversations/{id}/online` — **live-only** SSE поток
* онлайн-событий ([pw.binom.agentik.outbox.OnlineEvent]): дельты
* текста/картинок стриминга ответа.
*
* Отличия от `/conversations/{id}/events`:
* - нет `after` — catchup невозможен, события не хранятся;
* - подписка получает только то, что эмитится после подключения;
* - потерянные дельты восстанавливаются из durable-истории
* (`/events` + журнал), а не реплеятся здесь.
*/
get("/conversations/{id}/online") {
val id = call.parameters["id"]!!
val c = agent.getConversation(id)
if (c == null) {
call.respond(HttpStatusCode.NotFound)
return@get
}
call.streamJsonSse(agent.onlineOutbox.onlineEvents(id), OnlineEvent.serializer())
}
get("/events") {
val after = call.parseAfter() ?: return@get
// agent.outbox.agentEvents(after) возвращает Flow<CommonEvent.Agent>;
@@ -21,6 +21,7 @@ import pw.binom.agentik.journal.ConversationStore
import pw.binom.agentik.journal.JournalStore
import pw.binom.agentik.journal.MessageRecord
import pw.binom.agentik.outbox.CommonEvent
import pw.binom.agentik.outbox.OnlineOutbox
import pw.binom.agentik.outbox.OutboxStore
import pw.binom.agentik.proto.Agent
import pw.binom.agentik.proto.AgentInfo
@@ -74,6 +75,7 @@ class AgentInfoRouteTest {
override suspend fun earliestEventDate(): Instant = Instant.DISTANT_PAST
override fun close() {}
}
override val onlineOutbox: OnlineOutbox = emptyOnlineOutbox()
override val conversationStore: ConversationStore = object : ConversationStore {
override suspend fun get(id: String) = null
override suspend fun list(offset: Int, limit: Int) = emptyList<pw.binom.agentik.journal.ConversationRecord>()
@@ -143,6 +145,7 @@ class AgentInfoRouteTest {
override suspend fun earliestEventDate(): Instant = Instant.DISTANT_PAST
override fun close() {}
}
override val onlineOutbox: OnlineOutbox = emptyOnlineOutbox()
override val conversationStore: ConversationStore = object : ConversationStore {
override suspend fun get(id: String) = null
override suspend fun list(offset: Int, limit: Int) = emptyList<pw.binom.agentik.journal.ConversationRecord>()
@@ -17,6 +17,7 @@ import pw.binom.agentik.journal.ConversationStore
import pw.binom.agentik.journal.JournalStore
import pw.binom.agentik.journal.MessageRecord
import pw.binom.agentik.outbox.CommonEvent
import pw.binom.agentik.outbox.OnlineOutbox
import pw.binom.agentik.outbox.OutboxStore
import pw.binom.agentik.proto.Agent
import pw.binom.agentik.proto.AgentInfo
@@ -58,6 +59,7 @@ class BearerTokenTest {
override suspend fun earliestEventDate(): Instant = Instant.DISTANT_PAST
override fun close() {}
}
override val onlineOutbox: OnlineOutbox = emptyOnlineOutbox()
override val conversationStore: ConversationStore = object : ConversationStore {
override suspend fun get(id: String) = null
override suspend fun list(offset: Int, limit: Int) = emptyList<pw.binom.agentik.journal.ConversationRecord>()
@@ -20,6 +20,7 @@ import pw.binom.agentik.journal.JournalStore
import pw.binom.agentik.journal.inmemory.InMemoryJournalStore
import pw.binom.agentik.journal.inmemory.InMemoryMutableConversationStore
import pw.binom.agentik.outbox.CommonEvent
import pw.binom.agentik.outbox.OnlineOutbox
import pw.binom.agentik.outbox.OutboxStore
import pw.binom.agentik.proto.Agent
import pw.binom.agentik.proto.AgentInfo
@@ -168,6 +169,7 @@ class ConversationRoutesTest {
override suspend fun earliestEventDate(): Instant = Instant.DISTANT_PAST
override fun close() {}
}
override val onlineOutbox: OnlineOutbox = emptyOnlineOutbox()
override val conversationStore: ConversationStore = cs
override fun createConversation(temp: Boolean): pw.binom.agentik.proto.Conversation =
@@ -24,6 +24,7 @@ import pw.binom.agentik.journal.JournalStore
import pw.binom.agentik.journal.MessageRecord
import pw.binom.agentik.journal.inmemory.InMemoryJournalStore
import pw.binom.agentik.outbox.CommonEvent
import pw.binom.agentik.outbox.OnlineOutbox
import pw.binom.agentik.outbox.OutboxStore
import pw.binom.agentik.proto.Agent
import pw.binom.agentik.proto.AgentInfo
@@ -187,6 +188,7 @@ class JournalRoutesCountTest {
override suspend fun earliestEventDate(): Instant = Instant.DISTANT_PAST
override fun close() {}
}
override val onlineOutbox: OnlineOutbox = emptyOnlineOutbox()
override val conversationStore: ConversationStore = object : ConversationStore {
override suspend fun get(id: String) = null
override suspend fun list(offset: Int, limit: Int) = emptyList<pw.binom.agentik.journal.ConversationRecord>()
@@ -0,0 +1,14 @@
package pw.binom.agentik.server
import kotlinx.coroutines.flow.emptyFlow
import pw.binom.agentik.outbox.OnlineEvent
import pw.binom.agentik.outbox.OnlineOutbox
/**
* Пустой [OnlineOutbox] для тестовых [pw.binom.agentik.proto.Agent]-заглушек:
* онлайн-канал тестам не нужен, но интерфейс обязывает его отдать.
*/
internal fun emptyOnlineOutbox(): OnlineOutbox = object : OnlineOutbox {
override fun onlineEvents(conversationId: String) = emptyFlow<OnlineEvent>()
override fun close() {}
}