Изменение выносит типы контента в :content-api и разделяет потоки событий.
This commit is contained in:
@@ -20,8 +20,10 @@ kotlin {
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
// :proto больше не нужен — AgentEvent/CommonEvent/Event перенесены
|
||||
// сюда, и они self-contained (Event ссылается только на kotlinx-serialization).
|
||||
// :proto не нужен (Event/OnlineEvent/AgentEvent/CommonEvent живут
|
||||
// здесь). Message-события несут общие типы содержимого из
|
||||
// низкоуровневого :content-api (Content/MessageContext/TurnTokens).
|
||||
api(project(":content-api"))
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
api(libs.kotlinx.serialization.core)
|
||||
api(libs.kotlinx.serialization.json)
|
||||
|
||||
@@ -2,29 +2,25 @@ package pw.binom.agentik.outbox
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import pw.binom.agentik.content.Content
|
||||
import pw.binom.agentik.content.MessageContext
|
||||
import pw.binom.agentik.content.TurnTokens
|
||||
import kotlin.time.Instant
|
||||
|
||||
/**
|
||||
* Элемент live-потока `Conversation.events(after)`.
|
||||
* Элемент **durable**-потока диалога.
|
||||
*
|
||||
* Каждое событие несёт [date] — момент эмиссии в UTC. Используется клиентом
|
||||
* для трекинга «где остановился» при обрыве/переподключении и для разрешения
|
||||
* как курсор («где остановился») при обрыве/переподключении и для разрешения
|
||||
* порядка при равных timestamps.
|
||||
*
|
||||
* Базовая структура хода:
|
||||
* `Working` → `End` | `Interrupted` | `Error`, между ними — целые события
|
||||
* ([ToolCall]/[ToolResult]/[ToolFailed]).
|
||||
* Это «целые», сохраняемые события: сообщения ([UserMessage]/[AssistantMessage]),
|
||||
* вызовы тулов ([ToolCall]/[ToolResult]/[ToolFailed]), терминаторы хода
|
||||
* ([Interrupted]/[Error]) и lifecycle ([ConversationClosing]/[CompactionTriggered]).
|
||||
* Их можно перезапросить по курсору (`after`).
|
||||
*
|
||||
* **Стриминг ответа (дельты текста/картинок и маркеры фаз) — НЕ здесь.**
|
||||
* Дельты токенов живут в [OnlineEvent] (live-only, не сохраняются).
|
||||
* [Event] — только «целые» (durable) события, пригодные к перезапросу
|
||||
* по курсору.
|
||||
*
|
||||
* `Working` — маркер «агент принял запрос и пошёл обрабатывать», эмитится
|
||||
* синхронно в `Conversation.send()` ДО старта LLM-цикла (и возможной долгой
|
||||
* очереди turnLock'а). Парный терминатор не нужен: `End`/`Interrupted`/`Error`
|
||||
* уже закрывают ход. UI использует `Working` чтобы показать спиннер ещё до
|
||||
* первого токена ответа.
|
||||
* **Стриминг ответа и маркеры фаз хода — НЕ здесь.** Дельты текста/картинок
|
||||
* и маркеры `Working`/`End` живут в [OnlineEvent] (live-only, не сохраняются).
|
||||
*
|
||||
* **История**: до 2026-09-21 жил в `:proto` как `pw.binom.agentik.proto.Event`;
|
||||
* при миграции в `:outbox-api` был оставлен typealias в `:proto` для
|
||||
@@ -39,24 +35,35 @@ sealed interface Event {
|
||||
val date: Instant
|
||||
|
||||
/**
|
||||
* Маркер «агент принял запрос и пошёл обрабатывать». Эмитится **до**
|
||||
* [End]/[Interrupted]/[Error], синхронно из `Conversation.send()`,
|
||||
* чтобы UI мог показать спиннер ещё до первого токена ответа.
|
||||
* Терминатор хода ([End]/[Interrupted]/[Error]) — парный.
|
||||
* Целое пользовательское сообщение хода. Эмитится в тот же момент, когда
|
||||
* запись попадает в журнал (для персистентных диалогов). [id] совпадает
|
||||
* с id соответствующего `MessageRecord.UserMessage`/`Message.UserMessage`.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("working")
|
||||
data class Working(override val date: Instant) : Event
|
||||
@SerialName("user_message")
|
||||
data class UserMessage(
|
||||
override val date: Instant,
|
||||
val id: String,
|
||||
val content: List<Content>,
|
||||
val context: MessageContext? = null,
|
||||
) : Event
|
||||
|
||||
/** Ход завершён нормально. Соответствующий `Message.AssistantMessage` появится в `getMessages`. */
|
||||
/**
|
||||
* Целое сообщение ассистента — итог хода. Эмитится при завершении хода,
|
||||
* после того как текст ответа полностью собран.
|
||||
*
|
||||
* [reasoning] — текст размышлений модели (chain-of-thought), если провайдер
|
||||
* его отдаёт; иначе `null`. [tokens] — расход токенов за ход.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("end")
|
||||
data class End(override val date: Instant) : Event
|
||||
|
||||
/** Ход прерван через `Conversation.interrupt`. Частичный ответ НЕ сохраняется в истории. */
|
||||
@Serializable
|
||||
@SerialName("interrupted")
|
||||
data class Interrupted(override val date: Instant) : Event
|
||||
@SerialName("assistant_message")
|
||||
data class AssistantMessage(
|
||||
override val date: Instant,
|
||||
val id: String,
|
||||
val content: List<Content>,
|
||||
val reasoning: String? = null,
|
||||
val tokens: TurnTokens? = null,
|
||||
) : Event
|
||||
|
||||
/**
|
||||
* Агент начал вызов тула. Аргументы приходят целиком — стриминга нет.
|
||||
@@ -96,6 +103,14 @@ sealed interface Event {
|
||||
val result: String?,
|
||||
) : Event
|
||||
|
||||
/**
|
||||
* Ход прерван через `Conversation.interrupt`. Частичный ответ НЕ сохраняется
|
||||
* в истории.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("interrupted")
|
||||
data class Interrupted(override val date: Instant) : Event
|
||||
|
||||
/**
|
||||
* Ошибка хода. После неё поток завершается; дальнейшие события могут
|
||||
* прийти, но ход считается проваленным.
|
||||
|
||||
@@ -5,8 +5,8 @@ import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Instant
|
||||
|
||||
/**
|
||||
* **Онлайн-события** диалога: стриминг ответа агента «в моменте» —
|
||||
* дельты текста/картинок и маркеры фаз хода.
|
||||
* **Онлайн-события** диалога: live-поток «в моменте» — маркеры фаз хода и
|
||||
* стриминг ответа агента (дельты текста/картинок).
|
||||
*
|
||||
* Принципиальное отличие от [Event] (durable):
|
||||
* - **Никогда и нигде не сохраняются** — ни в буфер [OnlineOutbox],
|
||||
@@ -14,13 +14,13 @@ import kotlin.time.Instant
|
||||
* - **Только онлайн-подписка**: события, эмитнутые до подписки
|
||||
* (или в момент обрыва соединения), не реплеятся и не восстанавливаются.
|
||||
* Потерянный фрагмент не страшен — целый результат хода приходит
|
||||
* durable-событием ([Event.End]) и/или лежит в журнале.
|
||||
* durable-событием ([Event.AssistantMessage]) и/или лежит в журнале.
|
||||
* - **Нет курсора**: у потока нет `after`/`lastSeen` — курсор там, где
|
||||
* есть что реплеить.
|
||||
*
|
||||
* Зачем разделять: дельты токенов — высокочастотный мусор, который,
|
||||
* попав в durable store, копится в RAM (standalone-outbox растёт unbounded)
|
||||
* и засоряет историю. В [Event] остаются только «целые» события,
|
||||
* Зачем разделять: маркеры фаз и дельты токенов — высокочастотный мусор,
|
||||
* который, попав в durable store, копится в RAM (standalone-outbox растёт
|
||||
* unbounded) и засоряет историю. В [Event] остаются только «целые» события,
|
||||
* пригодные к перезапросу по курсору.
|
||||
*/
|
||||
@Serializable
|
||||
@@ -34,6 +34,20 @@ sealed interface OnlineEvent {
|
||||
@SerialName("image") IMAGE
|
||||
}
|
||||
|
||||
/**
|
||||
* Маркер «агент принял запрос и пошёл обрабатывать». Эмитится **до**
|
||||
* [End]/[Event.Interrupted]/[Event.Error], синхронно из `Conversation.send()`,
|
||||
* чтобы UI мог показать спиннер ещё до первого токена ответа.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("working")
|
||||
data class Working(override val date: Instant) : OnlineEvent
|
||||
|
||||
/** Ход завершён (нормально либо оборван). Зеркало терминатора — см. [Event]. */
|
||||
@Serializable
|
||||
@SerialName("end")
|
||||
data class End(override val date: Instant) : OnlineEvent
|
||||
|
||||
/** Ассистент начал рассуждение (опциональный маркер; контент идёт через [AppendText]). */
|
||||
@Serializable
|
||||
@SerialName("start_reasoning")
|
||||
|
||||
@@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
*
|
||||
* Это осознанный компромисс: дельты токенов — высокочастотный мусор,
|
||||
* который в durable-сторе копился бы в RAM и засорял историю. Потеря
|
||||
* фрагмента при обрыве не критична — целый ответ приходит [Event.End]
|
||||
* фрагмента при обрыве не критична — целый ответ приходит [Event.AssistantMessage]
|
||||
* и/или лежит в [pw.binom.agentik.journal.JournalStore].
|
||||
*
|
||||
* Read-only view: запись — через [MutableOnlineOutbox].
|
||||
|
||||
Reference in New Issue
Block a user