Изменения вводят 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
@@ -2,6 +2,8 @@ package pw.binom.agentik.proto
import pw.binom.agentik.journal.ConversationStore
import pw.binom.agentik.journal.JournalStore
import pw.binom.agentik.outbox.OnlineEvent
import pw.binom.agentik.outbox.OnlineOutbox
import pw.binom.agentik.outbox.OutboxStore
import kotlin.time.Instant
@@ -12,9 +14,10 @@ import kotlin.time.Instant
* [createConversation] возвращает [Conversation], который сам хранит историю
* и которому отправляют ходы через [Conversation.send].
*
* **Хранилища вынесены в [Agent.journal], [Agent.outbox] и
* [Agent.conversationStore]**: все три read-only views. События живут
* в [outbox] как `OutboxStore.events(after)` / `outbox.agentEvents(after)`.
* **Хранилища вынесены в [Agent.journal], [Agent.outbox],
* [Agent.onlineOutbox] и [Agent.conversationStore]**: read-only views.
* Durable-события живут в [outbox] как `OutboxStore.events(after)` /
* `outbox.agentEvents(after)`; live-стриминг ответа — в [onlineOutbox].
* Это даёт единый путь для всех read-операций по хранилищу и убирает
* дублирование между протоколом и хранилищем.
*
@@ -83,6 +86,24 @@ interface Agent : AutoCloseable {
*/
val outbox: OutboxStore
/**
* Live-канал **онлайн-событий** ([OnlineEvent] — стриминг ответа агента).
*
* Отделён от [outbox] принципиально:
* - [outbox] — durable: «целые» события, перезапрашиваемые по курсору
* (`after`), с catchup + live;
* - [onlineOutbox] — live-only: дельты токенов, **никогда не сохраняются**,
* без catchup/курсора, подписка работает только «онлайн».
*
* Используется HTTP-фасадом `:server` для endpoint'а
* `GET /{path}/conversations/{id}/online` (SSE). Клиент рендерит ответ
* из этого потока в реальном времени, а durable-историю берёт из [journal].
*
* **Read-only**: write-доступ только через `MutableOnlineOutbox` внутри
* ChatAgent / ConversationLoop, не через [Agent] interface.
*/
val onlineOutbox: OnlineOutbox
/**
* Read-only view на `conversation` table (id + title + timestamps).
*
@@ -9,15 +9,21 @@ import kotlin.time.Instant
* [send] агенту не нужно пересылать транскрипт — он уже живёт внутри
* [Conversation].
*
* **Live-события** диалога (turn stream: StartReasoning / AppendText / End /
* ToolCall / ToolResult / ...) НЕ часть этого интерфейса — единственный
* источник live-событий это [pw.binom.agentik.outbox.OutboxStore].
* Подписаться на события конкретного диалога:
* ```
* agent.outbox.conversationEvents(after = lastSeen, conversationId = id)
* .map { it.event }
* .collect { e -> ... }
* ```
* **Live-события** диалога НЕ часть этого интерфейса. Их два независимых
* потока:
* - **durable** ([pw.binom.agentik.outbox.Event]: Working / End / Interrupted /
* Error / ToolCall / ToolResult / ToolFailed) — из
* [pw.binom.agentik.outbox.OutboxStore], перезапрашивается по курсору:
* ```
* agent.outbox.conversationEvents(after = lastSeen, conversationId = id)
* .map { it.event }
* .collect { e -> ... }
* ```
* - **online** ([pw.binom.agentik.outbox.OnlineEvent]: StartReasoning /
* StartResponse / AppendText / AppendImage) — live-only стриминг ответа
* из [pw.binom.agentik.outbox.OnlineOutbox], без catchup/курсора:
* `agent.onlineOutbox.onlineEvents(id)`.
*
* Для cross-conversation view (admin / parent-agent / debug):
* `agent.outbox.events(after)`. Для lifecycle агента (created/deleted/renamed):
* `agent.outbox.agentEvents(after)`.