Изменения вводят 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
@@ -8,6 +8,7 @@ import pw.binom.agentik.proto.Agent
import pw.binom.agentik.proto.Content
import pw.binom.agentik.proto.Conversation
import pw.binom.agentik.outbox.Event
import pw.binom.agentik.outbox.OnlineEvent
import kotlin.coroutines.CoroutineContext
import kotlin.time.Instant
@@ -34,6 +35,9 @@ internal class TuiBackend(
/** Активная джоба подписки на [Conversation.events]. */
private var eventsJob: Job? = null
/** Активная джоба подписки на онлайн-поток (стриминг ответа). */
private var onlineJob: Job? = null
/** Последний виденный момент событий — для переподписки при reconnect. */
private var lastSeenAt: Instant = Instant.DISTANT_PAST
@@ -92,41 +96,42 @@ internal class TuiBackend(
}
/**
* Подписывается на `outbox.conversationEvents(after, conv.id)` и перенаправляет их в [state].
* Подписывается на durable-поток `outbox.conversationEvents(after, conv.id)`
* и live-поток `onlineOutbox.onlineEvents(conv.id)`; оба перенаправляет в [state].
*
* Онлайн-поток live-only (без catchup), поэтому подписку открываем ДО [Conversation.send]
* (см. [ensureConversation] → [onUserMessage]), чтобы не упустить начало хода.
*/
private fun subscribeEvents(conv: Conversation, from: Instant) {
eventsJob?.cancel()
eventsJob = scope.launch {
agent.outbox.conversationEvents(from, conv.id).collect { ce -> dispatch(ce.event) }
}
onlineJob?.cancel()
onlineJob = scope.launch {
agent.onlineOutbox.onlineEvents(conv.id).collect { ev -> dispatchOnline(ev) }
}
}
/**
* Маппинг [Event] → [AppState] (что показать в TUI).
* Маппинг [Event] (durable) → [AppState] (что показать в TUI).
*
* - AppendText → дописывает в последний ассистентский чанк
* - StartReasoning / StartResponse → новый streaming-чанк
* - End → закрывает streaming
* - Interrupted → закрывает streaming + системное сообщение
* - ToolCall / ToolResult → сообщения в историю
* - Error → системное сообщение
*
* Стриминг ответа (дельты текста/картинок) приходит отдельным потоком —
* см. [dispatchOnline].
*/
private fun dispatch(ev: Event) {
lastSeenAt = ev.date
when (ev) {
is Event.AppendText -> state.appendAssistant(ev.body)
is Event.StartReasoning -> {
state.postSystem("… думаю")
}
is Event.StartResponse -> state.setStreaming(true)
is Event.End -> state.finishAssistant()
is Event.Interrupted -> {
state.finishAssistant()
state.postSystem("прервано")
}
is Event.AppendImage -> {
state.postSystem("[картинка: ${ev.mime}, ${ev.body.size} байт]")
}
is Event.ToolCall -> {
state.postToolCall(toolName = ev.toolName, title = null, args = ev.toolArgs)
}
@@ -139,4 +144,20 @@ internal class TuiBackend(
}
}
}
/**
* Маппинг [OnlineEvent] (стриминг ответа, live-only) → [AppState].
*
* - AppendText → дописывает в последний ассистентский чанк
* - StartReasoning / StartResponse → новый streaming-чанк
* - AppendImage → системное сообщение-заглушка
*/
private fun dispatchOnline(ev: OnlineEvent) {
when (ev) {
is OnlineEvent.AppendText -> state.appendAssistant(ev.body)
is OnlineEvent.StartReasoning -> state.postSystem("… думаю")
is OnlineEvent.StartResponse -> state.setStreaming(true)
is OnlineEvent.AppendImage -> state.postSystem("[картинка: ${ev.mime}, ${ev.body.size} байт]")
}
}
}