refactor: migrate EventStore and MessageStore to :outbox-api and :journal-api
ci / JVM build + tests (push) Failing after 1m9s

- Replaced usages of `:message-store-api` and `:working-memory-api` with `:journal-api`, `:outbox-api`, and `:context-api`.
- Deprecated legacy `EventStore` and `MessageStore` interfaces, added `typealias` for backward compatibility.
- Updated imports across all modules with references to `:journal-api` and `:outbox-api`.
- Introduced `journalRoutes` and `outboxRoutes` in `:server` for audit log and live event stream endpoints.
- Adjusted `Agent` to expose read-only `journal` and `outbox` stores for improved modularity and clarity.
- Removed legacy Event and AgentEvent definitions from `:proto`, migrated to `:outbox-api`.
- Storage-related modules have been updated to support the new APIs consistently.
This commit is contained in:
2026-09-21 02:38:40 +03:00
parent 499db812ef
commit aef5083801
64 changed files with 978 additions and 960 deletions
@@ -2,6 +2,8 @@ package pw.binom.agentik.proto
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import pw.binom.agentik.journal.JournalStore
import pw.binom.agentik.outbox.OutboxStore
import kotlin.time.Instant
/**
@@ -10,12 +12,46 @@ import kotlin.time.Instant
* Транспортно-агностично. [Agent] — фабрика stateful-диалогов:
* [createConversation] возвращает [Conversation], который сам хранит историю
* и которому отправляют ходы через [Conversation.send].
*
* **Хранилища вынесены в [Agent.journal] и [Agent.outbox]**: оба read-only.
* События больше НЕ часть [Agent] (раньше были `events()`/`allEvents()`) —
* они теперь живут в [outbox] как `OutboxStore.events(after)` /
* `outbox.agentEvents(after)`. Это даёт единый путь для всех read-операций
* по хранилищу и убирает дублирование между протоколом и хранилищем.
*/
public interface Agent {
/** Идентификатор агента. */
val id: String
/**
* Append-only audit log всех сообщений диалогов (read-only view).
*
* Используется HTTP-фасадом `:server` для endpoint'а
* `GET /{path}/journal/conversations/{id}/messages` — внешние клиенты
* (дашборды, parent-агенты, A2A-bridge) могут читать полный transcript
* диалога, включая tool-call/tool-result/error, без необходимости идти
* через `Conversation.getMessages` (который возвращает уже
* project'нутый proto-Message).
*
* **Read-only**: write-доступ только через `MutableJournalStore`
* внутри ChatAgent / ConversationLoop, не через [Agent] interface.
*/
val journal: JournalStore
/**
* Bounded-tail live event stream агента (read-only view).
*
* Используется HTTP-фасадом `:server` для endpoint'а
* `GET /{path}/outbox/events?after=` (SSE) — внешние клиенты подписываются
* на agent lifecycle + conversation events. Catchup+live контракт — см.
* KDoc `OutboxStore.events`.
*
* **Read-only**: write-доступ только через `MutableOutboxStore` внутри
* ChatAgent / ConversationLoop, не через [Agent] interface.
*/
val outbox: OutboxStore
/** Создаёт новый stateful-диалог с агентом. */
fun createConversation(temp: Boolean): Conversation
@@ -39,28 +75,6 @@ public interface Agent {
}
}
/**
* Live-подписка на изменения в множестве диалогов агента: создание,
* удаление, переименование (см. [AgentEvent]). События внутри конкретного
* диалога приходят через [Conversation.events].
*
* **Не реплеит** прошлое — для снимка множества используй [getConversations]
* или [getConversation].
*/
fun events(after: Instant): Flow<AgentEvent>
/**
* All events in one stream: agent lifecycle (Created/Deleted/Renamed) +
* all conversation turns. Useful for admin dashboards, debug tools,
* parent agents.
*
* For UI use [events] + [Conversation.events]. This one-feed variant is
* for cases where everything-in-one is preferred.
*
* Cold (no replay). For catchup use EventStore.
*/
fun allEvents(after: Instant): Flow<CommonEvent>
companion object {
const val PAGE_SIZE: Int = 100
@@ -1,43 +1,10 @@
package pw.binom.agentik.proto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.time.Instant
/**
* Live-события уровня [Agent]: изменения в множестве диалогов
* (создание, удаление, переименование). События, происходящие **внутри**
* конкретного диалога, приходят через [Conversation.events], а не сюда.
* Backward-compat typealias: `AgentEvent` теперь живёт в `:outbox-api`
* (логически принадлежит сущности outbox, не wire-протоколу `:proto`).
*
* Каждое событие несёт [date] — момент эмиссии в UTC. Семантика подписки
* идентична [Conversation.events]: поток **не реплеит** прошлое, для бэкфилла
* используются `getConversations`/`getConversation`.
* Существующие импорты `pw.binom.agentik.proto.AgentEvent` продолжают
* работать транспарентно. Использовать typealias в новом коде.
*/
@Serializable
sealed interface AgentEvent {
/** Момент эмиссии события в UTC. */
val date: Instant
/**
* Создан новый диалог. Передаётся его id — handle можно получить через
* [Agent.getConversation]. Подписчик после [Created] может сразу открыть
* live-подписку на этот диалог через [Conversation.events].
*/
@Serializable
@SerialName("created")
data class Created(override val date: Instant, val conversationId: String) : AgentEvent
/**
* Диалог удалён. Переданный [Conversation]-handle реализация обязана
* закрыть (`close()`) до эмиссии этого события — после [Deleted]
* пользоваться handle нельзя.
*/
@Serializable
@SerialName("deleted")
data class Deleted(override val date: Instant, val id: String) : AgentEvent
/** У диалога сменился заголовок. */
@Serializable
@SerialName("renamed")
data class Renamed(override val date: Instant, val id: String, val title: String?) : AgentEvent
}
typealias AgentEvent = pw.binom.agentik.outbox.AgentEvent
@@ -1,39 +1,9 @@
package pw.binom.agentik.proto
import kotlin.time.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Unified wrapper for all agent events in a single stream.
* Backward-compat typealias: `CommonEvent` теперь живёт в `:outbox-api`.
*
* Useful for admin dashboards, debug tools, parent agents: one subscription
* instead of N+1. For regular UI use two separate SSE feeds
* ([AgentEvent] via /events and [Event] via /conversations/{id}/events);
* [CommonEvent] is for those who need everything in one place.
*
* Server endpoint: GET /events/all (SSE), or replay via EventStore.
*
* Not used for persistence payload: EventStore stores AgentEvent and
* Conversation.Event natively (compact form); this wrapper is wire-format
* only.
* Существующие импорты `pw.binom.agentik.proto.CommonEvent` продолжают
* работать транспарентно.
*/
@Serializable
sealed interface CommonEvent {
val date: Instant
@Serializable
@SerialName("agent")
data class Agent(
override val date: Instant,
val event: AgentEvent,
) : CommonEvent
@Serializable
@SerialName("conversation")
data class Conversation(
override val date: Instant,
val conversationId: String,
val event: Event,
) : CommonEvent
}
typealias CommonEvent = pw.binom.agentik.outbox.CommonEvent
@@ -1,88 +1,11 @@
package pw.binom.agentik.proto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.time.Instant
/**
* Элемент live-потока [Conversation.events].
* Backward-compat typealias: `Event` теперь живёт в `:outbox-api`.
*
* Каждое событие несёт [date] — момент эмиссии в UTC. Используется клиентом
* для трекинга «где остановился» при обрыве/переподключении и для разрешения
* порядка при равных timestamps.
*
* Базовая структура хода:
* `StartReasoning?` → `StartResponse(TEXT|IMAGE)` → ...контент... → `End` | `Interrupted` | `Error`.
* `StartReasoning` может отсутствовать, если агент не показывал рассуждения.
* Существующие импорты `pw.binom.agentik.proto.Event` продолжают
* работать транспарентно. `Conversation.events(after): Flow<Event>` в
* `:proto.Conversation` теперь фактически возвращает
* `pw.binom.agentik.outbox.Event` — тот же тип, другое имя.
*/
@Serializable
sealed interface Event {
/** Момент эмиссии события в UTC. */
val date: Instant
@Serializable
enum class ResponseType {
@SerialName("text") TEXT,
@SerialName("image") IMAGE
}
/** Ассистент начал рассуждение (опциональный маркер; контент рассуждения приходит через [AppendText]). */
@Serializable
@SerialName("start_reasoning")
data class StartReasoning(override val date: Instant) : Event
/** Начало ответа ассистента заданного типа. После него идут соответствующие `Append*`/`Tool*`-события, потом [End]/[Interrupted]/[Error]. */
@Serializable
@SerialName("start_response")
data class StartResponse(override val date: Instant, val responseType: ResponseType) : Event
/** Ход завершён нормально. Соответствующий [Message.AssistantMessage] появится в `getMessages`. */
@Serializable
@SerialName("end")
data class End(override val date: Instant) : Event
/** Ход прерван через [Conversation.interrupt]. Частичный ответ НЕ сохраняется в истории. */
@Serializable
@SerialName("interrupted")
data class Interrupted(override val date: Instant) : Event
@Serializable
@SerialName("append_text")
data class AppendText(override val date: Instant, val body: String) : Event
@Serializable
@SerialName("append_image")
data class AppendImage(override val date: Instant, val body: ByteArray, val mime: String) : Event
/**
* Агент начал вызов тула. Аргументы приходят целиком — стриминга нет.
* [id] совпадает с id соответствующего [Message.ToolCall] в истории
* после завершения хода.
*/
@Serializable
@SerialName("tool_call")
data class ToolCall(
override val date: Instant,
val id: String,
val title: String?,
val toolName: String,
val toolArgs: String,
) : Event
/**
* Результат вызова тула. Приходит целиком после завершения исполнения.
* [id] совпадает с [ToolCall.id], к которому относится результат, и
* с id [Message.ToolResult] в истории.
*/
@Serializable
@SerialName("tool_result")
data class ToolResult(override val date: Instant, val id: String, val result: String?) : Event
/**
* Ошибка хода. После неё поток завершается; дальнейшие события могут
* прийти, но ход считается проваленным.
*/
@Serializable
@SerialName("error")
data class Error(override val date: Instant, val message: String, val code: String? = null) : Event
}
typealias Event = pw.binom.agentik.outbox.Event