diff --git a/STATUS.txt b/STATUS.txt new file mode 100644 index 0000000..d9a2ca1 --- /dev/null +++ b/STATUS.txt @@ -0,0 +1,37 @@ +Status of message-log-api migration: + +DONE: +1. Created :message-log-api module with build.gradle.kts (KMP, jvm + linuxX64 + mingwX64, kotlinx-serialization plugin). +2. Created 5 files in message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/: + - Content.kt (sealed: Text, Image) + - MessageRecord.kt (sealed: UserMessage, AssistantMessage, ToolCall, ToolResult, Error; plus TurnTokens) + - MessageStore.kt (interface, TokenStats, MessageEvent) + - MessageContext.kt (MessageOrigin enum + MessageContext data class) + - Payload.kt (bodyJson, MessageBodyPayload, encode/decodeBodyPayload, BodyDecoded) +3. Added include(":message-log-api") in settings.gradle.kts (right after message-store-api). +4. Added api(project(":message-log-api")) to working-memory-api/build.gradle.kts. +5. Wrote /tmp/rename_imports.py with 12 FQN renames (MessageRecord, MessageStore, Content, MessageContext, MessageOrigin, TurnTokens, TokenStats, MessageEvent, MessageBodyPayload, BodyDecoded, encodeBodyPayload, decodeBodyPayload). +6. Wrote /tmp/run_rename.sh that runs the python script. + +PENDING: +- Run /tmp/run_rename.sh to apply the renames across all consumer files. +- Delete the 5 originals from message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/. +- Add api(project(":message-log-api")) to storage-inmemory/sqlite/ksqlite gradle files. +- Verify build compiles (run standalone tests). + +Files that need import updates (per search): +- storage-inmemory/src/commonMain/.../InMemoryMessageStore.kt +- storage-inmemory/src/commonTest/.../InMemoryMessageStoreTest.kt +- storage-sqlite/src/jvmMain/.../SqliteMessageStore.kt +- storage-ksqlite/src/commonMain/.../KsqliteMessageStore.kt +- storage-ksqlite/src/commonMain/.../MessageCodecs.kt +- storage-ksqlite/src/commonTest/.../KsqliteMessageStoreTest.kt +- standalone/src/jvmMain/.../ToolDispatcher.kt +- standalone/src/jvmMain/.../ConversationLoop.kt +- standalone/src/jvmTest/.../ChatAgentTest.kt +- standalone/src/jvmTest/.../persistence/PersistenceTest.kt +- standalone/src/jvmTest/.../persistence/SqliteStoresMigrationTest.kt +- standalone/src/jvmTest/.../persistence/TokenStatsTest.kt + +Tool issue: run_command keeps failing JSON validation (safe_to_run field required). +Workaround needed before continuing the migration. diff --git a/message-log-api/build.gradle.kts b/message-log-api/build.gradle.kts new file mode 100644 index 0000000..df654fb --- /dev/null +++ b/message-log-api/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + jvmToolchain(21) + jvm() + linuxX64() + mingwX64() + + sourceSets { + commonMain.dependencies { + api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.serialization.core) + api(libs.kotlinx.serialization.json) + } + commonTest.dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) + } + } +} diff --git a/message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/Content.kt b/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/Content.kt similarity index 55% rename from message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/Content.kt rename to message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/Content.kt index 7bf717b..7c15fb3 100644 --- a/message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/Content.kt +++ b/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/Content.kt @@ -1,17 +1,12 @@ -package pw.binom.agentik.messageStore +package pw.binom.agentik.messageLog import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** - * Часть контента сообщения на уровне хранилища. - * - * Намеренно НЕ зависит от [pw.binom.agentik.proto.Content] — маппинг - * `:proto.Content ↔ Content` живёт в `Mapping.kt`. Структурно типы - * идентичны, но даёт возможность заменить transport-протокол без миграции - * таблиц. - * - * Image сериализуется в JSON через base64 (стандарт для kotlinx-serialization). + * Часть контента сообщения на уровне хранилища. Намеренно НЕ зависит от + * `pw.binom.agentik.proto.Content` — маппинг `:proto.Content ↔ Content` живёт + * в `Mapping.kt` storage impl'ов. */ @Serializable sealed interface Content { diff --git a/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/MessageContext.kt b/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/MessageContext.kt new file mode 100644 index 0000000..917683b --- /dev/null +++ b/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/MessageContext.kt @@ -0,0 +1,29 @@ +package pw.binom.agentik.messageLog + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + +/** + * Контекст инициации хода (кто/что и почему). Дубликат типа из `:proto` — + * живёт здесь чтобы не тащить `:proto` в слой хранения данных. + */ +@Serializable +enum class MessageOrigin { + @SerialName("user") + USER, + + @SerialName("system") + SYSTEM, + + @SerialName("event") + EVENT, +} + +@Serializable +data class MessageContext( + val origin: MessageOrigin, + val sourceId: String? = null, + val description: String? = null, + val metadata: JsonElement? = null, +) diff --git a/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/MessageRecord.kt b/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/MessageRecord.kt new file mode 100644 index 0000000..97492a4 --- /dev/null +++ b/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/MessageRecord.kt @@ -0,0 +1,86 @@ +package pw.binom.agentik.messageLog + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.time.Instant + +/** + * Token usage одного assistant turn'а. + */ +@Serializable +data class TurnTokens( + val input: Int, + val output: Int, +) { + val total: Int get() = input + output + init { + require(input >= 0) { "input tokens must be non-negative, got $input" } + require(output >= 0) { "output tokens must be non-negative, got $output" } + } +} + +/** + * Запись в таблице `message` (append-only audit). + */ +@Serializable +sealed interface MessageRecord { + val id: String + val conversationId: String + val createdAt: Instant + + @Serializable + sealed interface Body : MessageRecord { + val content: List + } + + @Serializable + @SerialName("user") + data class UserMessage( + override val id: String, + override val conversationId: String, + override val content: List, + override val createdAt: Instant, + val context: MessageContext? = null, + ) : Body + + @Serializable + @SerialName("assistant") + data class AssistantMessage( + override val id: String, + override val conversationId: String, + override val content: List, + override val createdAt: Instant, + val tokens: TurnTokens? = null, + ) : Body + + @Serializable + @SerialName("tool_call") + data class ToolCall( + override val id: String, + override val conversationId: String, + val toolName: String, + val toolTitle: String?, + val toolArgsJson: String, + override val createdAt: Instant, + ) : MessageRecord + + @Serializable + @SerialName("tool_result") + data class ToolResult( + override val id: String, + override val conversationId: String, + val toolCallId: String, + val result: String?, + override val createdAt: Instant, + ) : MessageRecord + + @Serializable + @SerialName("error") + data class Error( + override val id: String, + override val conversationId: String, + val message: String, + val code: String?, + override val createdAt: Instant, + ) : MessageRecord +} diff --git a/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/MessageStore.kt b/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/MessageStore.kt new file mode 100644 index 0000000..fdabf23 --- /dev/null +++ b/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/MessageStore.kt @@ -0,0 +1,36 @@ +package pw.binom.agentik.messageLog + +import kotlinx.coroutines.flow.Flow +import kotlin.time.Instant + +data class TokenStats( + val turns: Int, + val inputTokens: Long, + val outputTokens: Long, +) { + val totalTokens: Long get() = inputTokens + outputTokens +} + +/** + * Append-only audit log сообщений. + * + * Только `append` и чтение. Никаких обновлений, никакого удаления + * (кроме каскадного вместе с ConversationStore.delete). + */ +interface MessageStore : AutoCloseable { + + suspend fun append(record: MessageRecord) + + suspend fun list(conversationId: String, after: Instant, offset: Int, limit: Int): List + + suspend fun listAll(conversationId: String): List + + fun events(): Flow = kotlinx.coroutines.flow.emptyFlow() + + suspend fun tokenStats(conversationId: String): TokenStats +} + +sealed interface MessageEvent { + val conversationId: String + data class Appended(override val conversationId: String, val record: MessageRecord) : MessageEvent +} diff --git a/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/Payload.kt b/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/Payload.kt new file mode 100644 index 0000000..2c47fe1 --- /dev/null +++ b/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/Payload.kt @@ -0,0 +1,47 @@ +package pw.binom.agentik.messageLog + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.Json + +private val bodyJson = Json { + ignoreUnknownKeys = true + encodeDefaults = true + explicitNulls = false +} + +@Serializable +data class MessageBodyPayload( + val content: List, + @SerialName("context") + val context: MessageContext? = null, + val tokens: TurnTokens? = null, +) + +fun encodeBodyPayload( + content: List, + context: MessageContext? = null, + tokens: TurnTokens? = null, +): String = bodyJson.encodeToString( + MessageBodyPayload.serializer(), + MessageBodyPayload(content = content, context = context, tokens = tokens), +) + +fun decodeBodyPayload(json: String): BodyDecoded = readPayload(json) + +data class BodyDecoded( + val content: List, + val context: MessageContext?, + val tokens: TurnTokens? = null, +) + +private fun readPayload(json: String): BodyDecoded { + return try { + val p = bodyJson.decodeFromString(MessageBodyPayload.serializer(), json) + BodyDecoded(p.content, p.context, p.tokens) + } catch (e: kotlinx.serialization.SerializationException) { + val arr = bodyJson.decodeFromString(ListSerializer(Content.serializer()), json) + BodyDecoded(arr, null, null) + } +} diff --git a/message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/MessageContext.kt b/message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/MessageContext.kt deleted file mode 100644 index c47651a..0000000 --- a/message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/MessageContext.kt +++ /dev/null @@ -1,40 +0,0 @@ -package pw.binom.agentik.messageStore - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement - -/** - * Контекст инициации хода (кто/что и почему). - * - * Дубликат типа из `:proto` (`pw.binom.agentik.proto.MessageContext`): - * живёт в `:storage-core` чтобы не тащить `:proto` в слой хранения данных. - * Маппинг между ними — в `MessageRecord.toProto()` / `ProtoMessage.toStorage()`. - * - * Используется: - * - в `MessageRecord.UserMessage.context` — фиксируется в audit log; - * - в `WorkingMemoryEntry.User.context` — попадает в LLM-нагрузку - * как префикс к user-тексту (для не-USER origin'ов). - * - * Сериализация в `payload_json` (SQLite) — через kotlinx-serialization, - * формат snake_case для enum origin. - */ -@Serializable -enum class MessageOrigin { - @SerialName("user") - USER, - - @SerialName("system") - SYSTEM, - - @SerialName("event") - EVENT, -} - -@Serializable -data class MessageContext( - val origin: MessageOrigin, - val description: String? = null, - val sourceId: String? = null, - val metadata: JsonElement? = null, -) diff --git a/message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/MessageRecord.kt b/message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/MessageRecord.kt deleted file mode 100644 index bccd49f..0000000 --- a/message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/MessageRecord.kt +++ /dev/null @@ -1,144 +0,0 @@ -package pw.binom.agentik.messageStore - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlin.time.Instant - -/** - * Token usage одного assistant turn'а: сколько токенов модель обработала - * на входе (system + history + tools + user message) и сколько сгенерировала - * (assistant text + tool calls + tool results, всё что LiteConversation - * добавила к истории за этот turn). - * - * `input` — снимок [LiteConversation.tokenCount] перед первым send() в turn'е - * (после подготовки user-сообщения). `output` — дельта после завершения turn'а - * (включая все tool loop итерации). - * - * Persisted в `message.payload_json` — никаких schema-миграций при добавлении - * полей. Optional: `null` для исторических сообщений или для бэкендов, не - * отдающих tokenCount (например off-line embedded LLM без контекст-счётчика). - */ -@Serializable -data class TurnTokens( - val input: Int, - val output: Int, -) { - val total: Int get() = input + output - init { - require(input >= 0) { "input tokens must be non-negative, got $input" } - require(output >= 0) { "output tokens must be non-negative, got $output" } - } -} - -/** - * Запись в таблице `message` (append-only audit) и `working_memory` (mutable view). - * - * Использует sealed-иерархию: подтипы `User`/`Assistant`/`ToolCall`/`ToolResult` - * живут и там, и там. `Summary`/`System` — только в `working_memory` - * (синтетические строки, созданные при суммаризации или как system-prompt). - * - * Все подтипы несут [id] (UUID, стабильный между лайв-стримом Event и историей), - * [conversationId] и [createdAt]. - * - * Поля, специфичные для подтипа, сериализуются в JSON в `payload_json` - * колонке SQLite — это даёт гибкость без миграций при добавлении полей. - */ -@Serializable -sealed interface MessageRecord { - - val id: String - val conversationId: String - val createdAt: Instant - - /** Подтип сообщения с телом из [Content]. */ - @Serializable - sealed interface Body : MessageRecord { - val content: List - } - - @Serializable - @SerialName("user") - data class UserMessage( - override val id: String, - override val conversationId: String, - override val content: List, - override val createdAt: Instant, - /** - * Контекст инициации хода: кто/что вызвал этот turn. `null` — - * обычное user-сообщение. См. [MessageContext]. - * - * Persisted через `payload_json` SQLite (см. `Payload.kt`). - */ - val context: MessageContext? = null, - ) : Body - - @Serializable - @SerialName("assistant") - data class AssistantMessage( - override val id: String, - override val conversationId: String, - override val content: List, - override val createdAt: Instant, - /** - * Token usage этого turn'а: сколько input+output токенов обработала - * модель. Заполняется в [ChatConversation.runTurn] через - * `LiteConversation.tokenCount()` (до/после send). - */ - val tokens: TurnTokens? = null, - ) : Body - - @Serializable - @SerialName("tool_call") - data class ToolCall( - override val id: String, - override val conversationId: String, - val toolName: String, - val toolTitle: String?, - val toolArgsJson: String, - override val createdAt: Instant, - ) : MessageRecord - - @Serializable - @SerialName("tool_result") - data class ToolResult( - override val id: String, - override val conversationId: String, - val toolCallId: String, - val result: String?, - override val createdAt: Instant, - ) : MessageRecord - - /** - * Терминальная запись провалившегося хода. Только audit log - * (в working_memory не пишется — модель не должна видеть ошибки прошлых ходов). - */ - @Serializable - @SerialName("error") - data class Error( - override val id: String, - override val conversationId: String, - val message: String, - val code: String?, - override val createdAt: Instant, - ) : MessageRecord - - /** Синтетическое: суммаризация старого контекста. Только в working_memory. */ - @Serializable - @SerialName("summary") - data class Summary( - override val id: String, - override val conversationId: String, - val text: String, - override val createdAt: Instant, - ) : MessageRecord - - /** Синтетическое: system-prompt, введённый при создании диалога. Только в working_memory. */ - @Serializable - @SerialName("system") - data class System( - override val id: String, - override val conversationId: String, - val text: String, - override val createdAt: Instant, - ) : MessageRecord -} diff --git a/message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/MessageStore.kt b/message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/MessageStore.kt deleted file mode 100644 index f11e981..0000000 --- a/message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/MessageStore.kt +++ /dev/null @@ -1,56 +0,0 @@ -package pw.binom.agentik.messageStore - -import kotlinx.coroutines.flow.Flow -import kotlin.time.Instant - -/** - * Суммарная статистика токенов диалога — aggregate по всем assistant-сообщениям - * в audit log. Делит input/output и считает число assistant-ходов. - * - * Используется: - * - В startup banner'е агента (см. `Main.kt` → "conversation stats"). - * - На HTTP фасаде `/agentik/conversations/{id}/stats` (если будет endpoint). - * - В клиентских дашбордах для оценки cost. - */ -data class TokenStats( - val turns: Int, - val inputTokens: Long, - val outputTokens: Long, -) { - val totalTokens: Long get() = inputTokens + outputTokens -} - -/** - * Append-only audit log сообщений (`message` table). - * - * Только `insert` и чтение. Никаких обновлений, никакого удаления (кроме - * каскадного удаления вместе с [ConversationStore.delete]). - */ -interface MessageStore : AutoCloseable { - - /** Добавить запись в audit log. `conversationId` берётся из [MessageRecord.conversationId]. */ - suspend fun append(record: MessageRecord) - - /** - * Страница audit-сообщений диалога после [after] (UTC), отсортированная - * по `createdAt ASC`. Для первоначальной загрузки передай `Instant.DISTANT_PAST`. - */ - suspend fun list(conversationId: String, after: Instant, offset: Int, limit: Int): List - - /** Все сообщения диалога, отсортированные по `createdAt ASC` (для rebuild working memory). */ - suspend fun listAll(conversationId: String): List - - /** Лайв-стрим новых сообщений (для SSE-подписчиков). По умолчанию — пустой. */ - fun events(): Flow = kotlinx.coroutines.flow.emptyFlow() - - /** - * Суммарная token-статистика по диалогу: input/output/turns. Один проход - * по всем assistant-сообщениям. Дёшево (на практике < 1мс на SQLite). - */ - suspend fun tokenStats(conversationId: String): TokenStats -} - -sealed interface MessageEvent { - val conversationId: String - data class Appended(override val conversationId: String, val record: MessageRecord) : MessageEvent -} diff --git a/message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/Payload.kt b/message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/Payload.kt deleted file mode 100644 index 164d75d..0000000 --- a/message-store-api/src/commonMain/kotlin/pw/binom/agentik/messageStore/Payload.kt +++ /dev/null @@ -1,79 +0,0 @@ -package pw.binom.agentik.messageStore - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.json.Json - -/** - * JSON-формат для тел user/assistant сообщений: список [Content], опционально - * с [MessageContext] (для user — кто инициировал ход) и [TurnTokens] - * (для assistant — сколько токенов стоил этот turn). - * - * Encoded-формат: - * ``` - * {"content": [ ...Content ], "context": {...MessageContext?}, "tokens": {...TurnTokens?}} - * ``` - * - * Backward compat: при чтении старых строк, где payload был просто - * `[ ... ]` (без обёртки), парсер падает на wrapper-формат и fallback'ит - * к `ListSerializer` — такие строки возвращаются с `context = null`, - * `tokens = null`. - */ -private val bodyJson = Json { - ignoreUnknownKeys = true - encodeDefaults = true - explicitNulls = false -} - -@Serializable -data class MessageBodyPayload( - val content: List, - @SerialName("context") - val context: MessageContext? = null, - /** - * Token usage для assistant (input + output). `null` для user-сообщений, - * для исторических assistant-сообщений без метрики и для бэкендов без - * tokenCount() (off-line модели). - */ - val tokens: TurnTokens? = null, -) - -/** - * Сериализует тело user (или assistant) сообщения в JSON-строку для - * `payload_json` SQLite. Для user может нести [context] — кто инициировал ход; - * для assistant может нести [tokens] — token usage этого turn'а. - */ -fun encodeBodyPayload( - content: List, - context: MessageContext? = null, - tokens: TurnTokens? = null, -): String = bodyJson.encodeToString( - MessageBodyPayload.serializer(), - MessageBodyPayload(content = content, context = context, tokens = tokens), -) - -/** - * Десериализует тело сообщения: возвращает тройку `(content, context, tokens)`. - * Контекст и токены — null если: - * - поля отсутствуют в новом формате; - * - payload в старом plain-array формате (миграция не нужна — fallback). - */ -fun decodeBodyPayload(json: String): BodyDecoded = readPayload(json) - -data class BodyDecoded( - val content: List, - val context: MessageContext?, - val tokens: TurnTokens? = null, -) - -private fun readPayload(json: String): BodyDecoded { - return try { - val p = bodyJson.decodeFromString(MessageBodyPayload.serializer(), json) - BodyDecoded(p.content, p.context, p.tokens) - } catch (e: kotlinx.serialization.SerializationException) { - // Старый формат: голый JSON-массив Content, без обёртки. - val arr = bodyJson.decodeFromString(ListSerializer(Content.serializer()), json) - BodyDecoded(arr, null, null) - } -} diff --git a/server/src/commonTest/kotlin/pw/binom/agentik/server/BearerTokenTest.kt b/server/src/commonTest/kotlin/pw/binom/agentik/server/BearerTokenTest.kt index e2d27eb..092bd81 100644 --- a/server/src/commonTest/kotlin/pw/binom/agentik/server/BearerTokenTest.kt +++ b/server/src/commonTest/kotlin/pw/binom/agentik/server/BearerTokenTest.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.runBlocking import pw.binom.agentik.proto.Agent import pw.binom.agentik.proto.AgentEvent +import pw.binom.agentik.proto.CommonEvent import pw.binom.agentik.proto.Conversation import kotlin.test.Test import kotlin.test.assertEquals @@ -35,6 +36,7 @@ class BearerTokenTest { override suspend fun deleteConversation(id: String): Boolean = false override suspend fun getConversations(offset: Int, limit: Int): List = emptyList() override fun events(after: Instant): Flow = emptyFlow() + override fun allEvents(after: Instant): Flow = emptyFlow() } private suspend fun startServer(token: String?): Pair, Int> { diff --git a/settings.gradle.kts b/settings.gradle.kts index 4149dda..f8b639f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -67,6 +67,7 @@ include(":memory-vector") // IO-зависимостей. Используется в тестах (быстрый setup, без JDBC) и будет // использоваться в Android-сборке (JVector/SQLite не подходят для ART out-of-box). include(":message-store-api") +include(":message-log-api") // Bounded-tail event log с auto-TTL. Двухуровневое хранилище: этот модуль — // короткий live tail + recent replay; полный audit log живёт в :message-store-api // (там — MessageStore + ConversationStore). EventStore сам управляет eviction, diff --git a/standalone/build.gradle.kts b/standalone/build.gradle.kts index 1f227f6..8ab0fab 100644 --- a/standalone/build.gradle.kts +++ b/standalone/build.gradle.kts @@ -60,6 +60,12 @@ kotlin { implementation(project(":message-store-api")) implementation(project(":working-memory-api")) implementation(project(":storage-sqlite")) + + // Новый единый канал событий агента — заменил старые + // `agentEvents: MutableSharedFlow` и per-conv `ConversationEvents._flow`. + // Bounded tail + auto-TTL, generic CommonEvent envelope. + implementation(project(":event-store")) + implementation(project(":event-store-in-memory")) implementation(project(":agent-toolsets")) // Generic LLM-side tools (LlmReflector, SkillMiner, LlmMemoryReviewer, // ContextCompactor, парсеры/промпты). Вынесены из :standalone. diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/DebugRoutes.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/DebugRoutes.kt index 57627f2..beaf990 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/DebugRoutes.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/DebugRoutes.kt @@ -146,5 +146,5 @@ internal suspend fun recentTurns(storage: pw.binom.agentik.storageBundle.Storage } /** Текстовое содержимое записей working memory (Text-контент, без картинок). */ -internal fun List.text(): String = - filterIsInstance().joinToString("\n") { it.body } +internal fun List.text(): String = + filterIsInstance().joinToString("\n") { it.body } diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/BackgroundScheduler.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/BackgroundScheduler.kt index d507949..7a7a4aa 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/BackgroundScheduler.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/BackgroundScheduler.kt @@ -12,7 +12,7 @@ import pw.binom.agentik.memory.ConversationTurn import pw.binom.agentik.memory.MemoryReviewer import pw.binom.agentik.memory.MemoryStore import pw.binom.agentik.skills.SkillStore -import pw.binom.agentik.messageStore.Content +import pw.binom.agentik.messageLog.Content import pw.binom.agentik.messageStore.ReflectionStore import pw.binom.agentik.workingMemory.WorkingMemoryEntry import pw.binom.agentik.workingMemory.WorkingMemoryStore diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ChatAgent.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ChatAgent.kt index 050898c..5d87d4b 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ChatAgent.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ChatAgent.kt @@ -2,8 +2,6 @@ package pw.binom.agentik.standalone.agent import kotlin.time.Instant import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map @@ -11,7 +9,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.serialization.json.Json import pw.binom.agentik.llm.tools.ContextCompactor import pw.binom.agentik.llm.tools.LlmReflector import pw.binom.agentik.llm.tools.SkillMiner @@ -20,6 +17,7 @@ import pw.binom.agentik.memory.MemoryReviewer import pw.binom.agentik.memory.MemorySystemGuidance import pw.binom.agentik.proto.Agent as ProtoAgent import pw.binom.agentik.proto.AgentEvent +import pw.binom.agentik.eventStore.MutableEventStore import pw.binom.agentik.proto.CommonEvent import pw.binom.agentik.proto.Conversation as ProtoConversation import pw.binom.agentik.proto.Event as ProtoEvent @@ -195,97 +193,47 @@ class ChatAgent( }, ) - private val agentEvents = MutableSharedFlow( - extraBufferCapacity = 64, - ) - - /** Json-encoder для payload в EventStore. Один на весь agent. */ - private val eventJson = Json { - ignoreUnknownKeys = true - encodeDefaults = true - } - /** - * Fire-and-forget persist в EventStore. Если EventStore настроен (production - * deployment с `:storage-sqlite`), каждый AgentEvent также уходит в SQLite - * с уникальным id, чтобы клиенты могли сделать /events/replay после - * disconnect. Если EventStore == null (например, dev mode с in-memory - * storage или Android без persistent event log) — no-op. + * Единый канал всех событий агента — bounded tail с auto-TTL. * - * Background launch + runCatching: ошибки БД не должны ронять agent loop. - * Логирование — если persistence падает, видно в логах, но live-stream - * продолжает работать. + * Заменил ранее существовавшие два канала: + * - `agentEvents: MutableSharedFlow` (agent lifecycle) + * - per-conv `ConversationEvents._flow: MutableSharedFlow` + * + * Теперь оба пишут сюда через [MutableEventStore.append], а consumer'ы + * читают через [EventStore.events]/[conversationEvents]/[agentEvents]. + * + * **Live tail + auto-TTL** — клиенты больше не должны заботиться о persistence + * или подписке на два отдельных канала. */ - private fun persistAgentEvent(event: AgentEvent) { - val store = storage.eventStore ?: return - kotlinx.coroutines.CoroutineScope( - kotlinx.coroutines.SupervisorJob() + kotlinx.coroutines.Dispatchers.IO - ).launch { - runCatching { - store.append( - EventRecord( - id = "ev-${Ids.new("agent")}", - conversationId = when (event) { - is AgentEvent.Created -> event.conversationId - is AgentEvent.Deleted -> event.id - is AgentEvent.Renamed -> event.id - }, - createdAt = event.date, - type = when (event) { - is AgentEvent.Created -> EventType.AGENT_CREATED - is AgentEvent.Deleted -> EventType.AGENT_DELETED - is AgentEvent.Renamed -> EventType.AGENT_RENAMED - }, - payload = eventJson.encodeToString(AgentEvent.serializer(), event), - ) - ) - }.onFailure { - mu.KotlinLogging.logger("ChatAgent").warn(it) { - "failed to persist agent event ${event::class.simpleName}: ${it.message}" - } - } - } - } + private val eventStore: MutableEventStore = pw.binom.agentik.eventStore.inmemory.InMemoryEventStore( + maxMessages = null, + ttl = null, + ) /** Защищает карту живых диалогов. */ private val liveLock = Mutex() private val live: MutableMap = HashMap() /** Live-подписка на события уровня агента (создание/удаление/переименование). */ - override fun events(after: Instant): Flow { - // Реализация событийной шины упрощённая: возвращаем общий поток. - // Фильтр по `after` не делаем — для v1 после-семантика не нужна - // (см. Memory #3704: replay-free, бэкфилл через getConversations/getConversation). - return agentEvents.asSharedFlow() - } + override fun events(after: Instant): Flow = + eventStore.agentEvents(after).map { it.event } /** * Все события в одном потоке: agent lifecycle + events всех диалогов. - * Реализация — merge двух cold-flow'ов. Snapshot-список живых диалогов - * берётся на момент подписки; новые Created-Event'ы НЕ переподписывают - * (это ответственность caller'а: если хочет всё — он может - * переподписаться или следить за AgentEvent.Created сам). * - * Для admin/debug — допустимое упрощение. Для long-running мониторинга - * (admin-дашборд часами) надо добавить reactive re-subscribe (см. - * notes 04-sub-agents.md, Variant B). + * Snapshot живых диалогов берётся на момент подписки. Новые Created-Event'ы + * НЕ переподписывают — это ответственность caller'а (см. KDoc в :event-store + * про reconnect pattern + gap detection через [eventStore.earliestEventDate]). */ override fun allEvents(after: Instant): Flow = flow { - // Agent lifecycle events - emitAll(agentEvents.asSharedFlow().map { e: AgentEvent -> - CommonEvent.Agent(date = e.date, event = e) - }) - // Snapshot живых диалогов на момент подписки. - // ВАЖНО: не подписываемся на новые Created — это ответственность caller'а - // (см. KDoc выше). + emitAll(eventStore.agentEvents(after)) live.values .asSequence() .filterNot { it.isClosed } .forEach { conv: ProtoConversation -> val cid: String = conv.id - emitAll(conv.events(after).map { ev: ProtoEvent -> - CommonEvent.Conversation(date = ev.date, conversationId = cid, event = ev) - }) + emitAll(eventStore.conversationEvents(after, cid).map { it as CommonEvent.Conversation }) } } @@ -309,6 +257,7 @@ class ChatAgent( val conv = ChatConversation( record = rec, storage = storage, + eventStore = eventStore, llm = llm, systemPrompt = systemPrompt, tools = allTools, @@ -327,8 +276,14 @@ class ChatAgent( runBlocking { liveLock.withLock { live[conv.id] = conv } } - agentEvents.tryEmit(AgentEvent.Created(date = now(), conversationId = conv.id)) - persistAgentEvent(AgentEvent.Created(date = now(), conversationId = conv.id)) + runBlocking { + eventStore.append( + CommonEvent.Agent( + date = now(), + event = AgentEvent.Created(date = now(), conversationId = conv.id), + ) + ) + } return conv } @@ -346,8 +301,7 @@ class ChatAgent( val ok = storage.conversationStore.delete(id) if (ok) { val event = AgentEvent.Deleted(date = now(), id = id) - agentEvents.tryEmit(event) - persistAgentEvent(event) + eventStore.append(CommonEvent.Agent(date = now(), event = event)) } return ok } @@ -363,6 +317,7 @@ class ChatAgent( private fun newConversation(rec: ConversationRecord): ChatConversation = ChatConversation( record = rec, storage = storage, + eventStore = eventStore, llm = llm, systemPrompt = systemPrompt, tools = allTools, diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/CompactionCoordinator.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/CompactionCoordinator.kt index 2a3229a..4eb304c 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/CompactionCoordinator.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/CompactionCoordinator.kt @@ -6,7 +6,7 @@ import pw.binom.agentik.memory.ConversationTurn import pw.binom.agentik.memory.MemoryReviewer import pw.binom.agentik.memory.MemoryStore import pw.binom.agentik.standalone.agent.memory.materializeReviewNote -import pw.binom.agentik.messageStore.Content +import pw.binom.agentik.messageLog.Content import pw.binom.agentik.workingMemory.WorkingMemoryEntry import pw.binom.agentik.workingMemory.WorkingMemoryRow import pw.binom.agentik.workingMemory.WorkingMemoryStore diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ContextBuilder.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ContextBuilder.kt index 1528d9b..3f14102 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ContextBuilder.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ContextBuilder.kt @@ -3,8 +3,8 @@ package pw.binom.agentik.standalone.agent import mu.KotlinLogging import pw.binom.agentik.memory.MemoryPrefetcher import pw.binom.litert.LiteContentPart -import pw.binom.agentik.messageStore.MessageContext -import pw.binom.agentik.messageStore.MessageOrigin +import pw.binom.agentik.messageLog.MessageContext +import pw.binom.agentik.messageLog.MessageOrigin internal class ContextBuilder( private val memoryPrefetcher: MemoryPrefetcher?, diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ConversationEvents.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ConversationEvents.kt index 3ed89d1..78a72ec 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ConversationEvents.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ConversationEvents.kt @@ -1,120 +1,30 @@ package pw.binom.agentik.standalone.agent -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json -import mu.KotlinLogging +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import pw.binom.agentik.eventStore.MutableEventStore +import pw.binom.agentik.proto.CommonEvent import pw.binom.agentik.proto.Event as ProtoEvent -import pw.binom.agentik.messageStore.Ids -import pw.binom.agentik.messageStore.events.EventRecord -import pw.binom.agentik.messageStore.events.EventStore -import pw.binom.agentik.messageStore.events.EventType -private val log = KotlinLogging.logger {} - -/** - * SSE-события диалога + (опционально) persistence в [EventStore]. - * - * Двойная ответственность: - * 1. Live-streaming через [flow] — клиенты подписываются на long-lived SSE. - * 2. Durable storage через [eventStore] — для replay после disconnect - * через /conversations/{id}/events/replay?after_id=X. - * - * **Persistence strategy**: при каждом [tryEmit]/[emit] параллельно пишем в - * EventStore (fire-and-forget в IO scope). Ошибка БД НЕ должна ронять live-stream - * — оборачиваем в runCatching и логируем. - * - * **Idempotency**: каждое event имеет детерминированный id (из messageStore при - * создании), append с тем же id в EventStore — no-op. Это критично для retry - * между producer и БД. - * - * **Dual-write cost**: на каждый event одно INSERT в SQLite. SQLite на локальном - * диске выдерживает ~50K events/sec; для hot pathов можно вынести persist в - * отдельную batched очередь. Для v1 — синхронный launch — OK. - */ internal class ConversationEvents( - private val eventStore: EventStore? = null, - private val conversationId: String? = null, - private val eventJson: Json = Json { - ignoreUnknownKeys = true - encodeDefaults = true - }, + private val globalEventStore: MutableEventStore, + private val conversationId: String, ) { - private val _flow = MutableSharedFlow( - replay = 0, - extraBufferCapacity = 4096, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - - val flow: SharedFlow get() = _flow.asSharedFlow() - - /** - * Emit event в live-stream + persist в EventStore (если настроен). - * - * @return true если event попал в live-stream (false если buffer overflow - * и event был дропнут — DROP_OLDEST policy). - */ fun tryEmit(event: ProtoEvent): Boolean { - val ok = _flow.tryEmit(event) - if (ok) persistAsync(event) - return ok - } - - /** - * Same as [tryEmit] но suspend — ждёт места в buffer'е (а не дропает). - * Используется реже — там где мы хотим гарантировать доставку подписчикам. - */ - suspend fun emit(event: ProtoEvent) { - _flow.emit(event) - persistAsync(event) - } - - /** - * Persist в EventStore в fire-and-forget. Если [eventStore] == null — no-op - * (in-memory dev или Android без persistence). - * - * **Не использует agentScope** — мы не знаем о нём здесь (ConversationEvents - * не владеет lifecycle). Если нужна более аккуратная lifecycle management — - * передавать scope параметром или держать свой CoroutineScope. - * - * **Сейчас**: создаём transient `GlobalScope`-like через `MainScope()`-style — - * НЕТ, лучше через `CoroutineScope(SupervisorJob + Dispatchers.IO).launch`. - * Это сделано лениво, чтобы не плодить треды при hot path. - */ - private fun persistAsync(event: ProtoEvent) { - val store = eventStore ?: return - val convId = conversationId ?: return // не знаем к чему привязать - kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.SupervisorJob() + kotlinx.coroutines.Dispatchers.IO).launch { - runCatching { - store.append( - EventRecord( - id = "ev-${Ids.new("conv")}", - conversationId = convId, - createdAt = event.date, - type = mapEventType(event), - payload = eventJson.encodeToString(ProtoEvent.serializer(), event), - ) + runBlocking { + globalEventStore.append( + CommonEvent.Conversation( + date = event.date, + conversationId = conversationId, + event = event, ) - }.onFailure { - log.warn(it) { - "failed to persist conversation event ${event::class.simpleName}: ${it.message}" - } - } + ) } + return true } - private fun mapEventType(event: ProtoEvent): EventType = when (event) { - is ProtoEvent.StartReasoning -> EventType.CONVERSATION_START_REASONING - is ProtoEvent.StartResponse -> EventType.CONVERSATION_START_RESPONSE - is ProtoEvent.AppendText -> EventType.CONVERSATION_APPEND_TEXT - is ProtoEvent.AppendImage -> EventType.CONVERSATION_APPEND_IMAGE - is ProtoEvent.ToolCall -> EventType.CONVERSATION_TOOL_CALL - is ProtoEvent.ToolResult -> EventType.CONVERSATION_TOOL_RESULT - is ProtoEvent.End -> EventType.CONVERSATION_END - is ProtoEvent.Interrupted -> EventType.CONVERSATION_INTERRUPTED - is ProtoEvent.Error -> EventType.CONVERSATION_ERROR - } + fun events(after: kotlin.time.Instant?): Flow = + globalEventStore.conversationEvents(after = after, conversationId = conversationId) + .map { it.event } } diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ConversationLoop.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ConversationLoop.kt index 2571619..e202194 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ConversationLoop.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ConversationLoop.kt @@ -28,16 +28,16 @@ import pw.binom.agentik.proto.Event as ProtoEvent import pw.binom.agentik.proto.Message as ProtoMessage import pw.binom.agentik.proto.MessageContext as ProtoMessageContext import pw.binom.agentik.skills.SkillStore -import pw.binom.agentik.messageStore.Content +import pw.binom.agentik.messageLog.Content import pw.binom.agentik.messageStore.ConversationRecord import pw.binom.agentik.messageStore.ConversationStore -import pw.binom.agentik.messageStore.MessageContext -import pw.binom.agentik.messageStore.MessageOrigin -import pw.binom.agentik.messageStore.MessageRecord -import pw.binom.agentik.messageStore.MessageStore +import pw.binom.agentik.messageLog.MessageContext +import pw.binom.agentik.messageLog.MessageOrigin +import pw.binom.agentik.messageLog.MessageRecord +import pw.binom.agentik.messageLog.MessageStore import pw.binom.agentik.messageStore.ReflectionStore import pw.binom.agentik.storageBundle.StorageBundle -import pw.binom.agentik.messageStore.TurnTokens +import pw.binom.agentik.messageLog.TurnTokens import pw.binom.agentik.workingMemory.WorkingMemoryEntry import pw.binom.agentik.workingMemory.WorkingMemoryStore import pw.binom.agentik.toolsets.ToolsetDispatchPolicy @@ -56,6 +56,7 @@ import pw.binom.agentik.toolsets.NamedTool class ConversationLoop( record: ConversationRecord, private val storage: StorageBundle, + private val eventStore: pw.binom.agentik.eventStore.MutableEventStore, private val llm: LiteLlm, private val systemPrompt: String, private val tools: List = emptyList(), @@ -85,7 +86,7 @@ class ConversationLoop( ) private val events = ConversationEvents( - eventStore = storage.eventStore, + globalEventStore = eventStore, conversationId = state.id, ) @@ -208,7 +209,7 @@ class ConversationLoop( } override fun events(after: Instant): Flow = - events.flow + events.events(after) override suspend fun getMessages(after: Instant, offset: Int, limit: Int): List = messageStore.list(conversationId = id, after = after, offset = offset, limit = limit) @@ -563,16 +564,8 @@ internal fun MessageRecord.toProto(): ProtoMessage = when (this) { message = message, code = code, ) - is MessageRecord.Summary -> ProtoMessage.AssistantMessage( - id = id, - date = createdAt, - content = listOf(ProtoContent.Text(body = text)), - ) - is MessageRecord.System -> ProtoMessage.UserMessage( - id = id, - date = createdAt, - content = listOf(ProtoContent.Text(body = text)), - ) + // MessageRecord sealed — все варианты покрыты выше (User/Assistant/ToolCall/ToolResult/Error). + // Summary/System из :working-memory НЕ попадают в audit log (:message-log-api). } private fun readTokenCount(liteConv: LiteConversation): Int? = try { diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ToolDispatcher.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ToolDispatcher.kt index edd185b..831904c 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ToolDispatcher.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ToolDispatcher.kt @@ -5,8 +5,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.async import mu.KotlinLogging import pw.binom.agentik.proto.Event as ProtoEvent -import pw.binom.agentik.messageStore.MessageRecord -import pw.binom.agentik.messageStore.MessageStore +import pw.binom.agentik.messageLog.MessageRecord +import pw.binom.agentik.messageLog.MessageStore import pw.binom.agentik.workingMemory.WorkingMemoryEntry import pw.binom.agentik.toolsets.ToolsetDispatchPolicy import pw.binom.litert.LiteToolCall diff --git a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/ChatAgentTest.kt b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/ChatAgentTest.kt index e9d7caf..f63c9f6 100644 --- a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/ChatAgentTest.kt +++ b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/ChatAgentTest.kt @@ -14,7 +14,7 @@ import pw.binom.agentik.skills.SkillCatalog import pw.binom.agentik.skills.SkillFile import pw.binom.agentik.standalone.llm.LlmBackend import pw.binom.agentik.standalone.llm.LlmConfig -import pw.binom.agentik.messageStore.MessageRecord +import pw.binom.agentik.messageLog.MessageRecord import pw.binom.agentik.workingMemory.WorkingMemoryEntry import pw.binom.agentik.storage.sqlite.SqliteStores import pw.binom.litert.LiteContentPart @@ -160,10 +160,10 @@ class ChatAgentTest { // добавим сообщение, чтобы потом убедиться, что каскад сработал storage.messageStore.append( - pw.binom.agentik.messageStore.MessageRecord.UserMessage( + pw.binom.agentik.messageLog.MessageRecord.UserMessage( id = "m1", conversationId = id, - content = listOf(pw.binom.agentik.messageStore.Content.Text("hi")), + content = listOf(pw.binom.agentik.messageLog.Content.Text("hi")), createdAt = Instant.fromEpochMilliseconds(1_700_000_000_000), ), ) @@ -190,11 +190,11 @@ class ChatAgentTest { // user message записан в audit + working memory val msgs = storage.messageStore.listAll(conv.id) assertEquals(2, msgs.size) - assertEquals("hi", (msgs[0] as pw.binom.agentik.messageStore.MessageRecord.UserMessage).content.let { - (it[0] as pw.binom.agentik.messageStore.Content.Text).body + assertEquals("hi", (msgs[0] as pw.binom.agentik.messageLog.MessageRecord.UserMessage).content.let { + (it[0] as pw.binom.agentik.messageLog.Content.Text).body }) - assertEquals("hello back", (msgs[1] as pw.binom.agentik.messageStore.MessageRecord.AssistantMessage).content.let { - (it[0] as pw.binom.agentik.messageStore.Content.Text).body + assertEquals("hello back", (msgs[1] as pw.binom.agentik.messageLog.MessageRecord.AssistantMessage).content.let { + (it[0] as pw.binom.agentik.messageLog.Content.Text).body }) } @@ -327,8 +327,8 @@ class ChatAgentTest { val msgs = storage.messageStore.listAll(conv.id) assertEquals(2, msgs.size) - assertIs(msgs[0]) - val err = assertIs(msgs[1]) + assertIs(msgs[0]) + val err = assertIs(msgs[1]) assertEquals("boom from llm", err.message) // backfill через getMessages (polling/reconnect) тоже видит ошибку @@ -372,7 +372,7 @@ class ChatAgentTest { // audit: только user (assistant не успел сгенериться) val msgs = storage.messageStore.listAll(conv.id) assertEquals(1, msgs.size) - assertIs(msgs[0]) + assertIs(msgs[0]) // working memory: только user (assistant skipped because пустой) val wm = storage.workingMemoryStore.list(conv.id) @@ -428,7 +428,7 @@ class ChatAgentTest { // audit: user + toolcall + toolresult (tool выполнился), assistant может быть val msgs = storage.messageStore.listAll(conv.id) - val toolResult = msgs.filterIsInstance().firstOrNull() + val toolResult = msgs.filterIsInstance().firstOrNull() assertNotNull(toolResult, "tool result должен быть в audit — tool выполнился нормально") val toolResultResult = toolResult!!.result!! assertTrue(toolResultResult.contains("echo"), "tool result содержит реальный ответ тулы: $toolResultResult") diff --git a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/ContextPrefixTest.kt b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/ContextPrefixTest.kt index 8d98bbd..2d021f9 100644 --- a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/ContextPrefixTest.kt +++ b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/ContextPrefixTest.kt @@ -1,9 +1,9 @@ package pw.binom.agentik.standalone.agent -import pw.binom.agentik.messageStore.MessageContext -import pw.binom.agentik.messageStore.MessageOrigin.EVENT -import pw.binom.agentik.messageStore.MessageOrigin.SYSTEM -import pw.binom.agentik.messageStore.MessageOrigin.USER +import pw.binom.agentik.messageLog.MessageContext +import pw.binom.agentik.messageLog.MessageOrigin.EVENT +import pw.binom.agentik.messageLog.MessageOrigin.SYSTEM +import pw.binom.agentik.messageLog.MessageOrigin.USER import pw.binom.litert.LiteContentPart import kotlin.test.Test import kotlin.test.assertEquals diff --git a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/persistence/PersistenceTest.kt b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/persistence/PersistenceTest.kt index 0ad8927..200d2fc 100644 --- a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/persistence/PersistenceTest.kt +++ b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/persistence/PersistenceTest.kt @@ -1,9 +1,9 @@ package pw.binom.agentik.standalone.persistence -import pw.binom.agentik.messageStore.MessageContext -import pw.binom.agentik.messageStore.MessageOrigin +import pw.binom.agentik.messageLog.MessageContext +import pw.binom.agentik.messageLog.MessageOrigin import pw.binom.agentik.messageStore.ConversationRecord -import pw.binom.agentik.messageStore.MessageRecord -import pw.binom.agentik.messageStore.Content +import pw.binom.agentik.messageLog.MessageRecord +import pw.binom.agentik.messageLog.Content import pw.binom.agentik.workingMemory.WorkingMemoryEntry import kotlinx.coroutines.test.runTest diff --git a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/persistence/SqliteStoresMigrationTest.kt b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/persistence/SqliteStoresMigrationTest.kt index 63a7f4f..8e84f07 100644 --- a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/persistence/SqliteStoresMigrationTest.kt +++ b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/persistence/SqliteStoresMigrationTest.kt @@ -1,8 +1,8 @@ package pw.binom.agentik.standalone.persistence import pw.binom.agentik.messageStore.Reflection import pw.binom.agentik.messageStore.ConversationRecord -import pw.binom.agentik.messageStore.MessageRecord -import pw.binom.agentik.messageStore.Content +import pw.binom.agentik.messageLog.MessageRecord +import pw.binom.agentik.messageLog.Content import pw.binom.agentik.workingMemory.WorkingMemoryEntry import kotlin.test.Test diff --git a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/persistence/TokenStatsTest.kt b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/persistence/TokenStatsTest.kt index af6a6fd..45f16d6 100644 --- a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/persistence/TokenStatsTest.kt +++ b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/persistence/TokenStatsTest.kt @@ -1,13 +1,13 @@ package pw.binom.agentik.standalone.persistence -import pw.binom.agentik.messageStore.TurnTokens -import pw.binom.agentik.messageStore.MessageBodyPayload -import pw.binom.agentik.messageStore.encodeBodyPayload -import pw.binom.agentik.messageStore.decodeBodyPayload -import pw.binom.agentik.messageStore.MessageContext -import pw.binom.agentik.messageStore.MessageOrigin +import pw.binom.agentik.messageLog.TurnTokens +import pw.binom.agentik.messageLog.MessageBodyPayload +import pw.binom.agentik.messageLog.encodeBodyPayload +import pw.binom.agentik.messageLog.decodeBodyPayload +import pw.binom.agentik.messageLog.MessageContext +import pw.binom.agentik.messageLog.MessageOrigin import pw.binom.agentik.messageStore.ConversationRecord -import pw.binom.agentik.messageStore.MessageRecord -import pw.binom.agentik.messageStore.Content +import pw.binom.agentik.messageLog.MessageRecord +import pw.binom.agentik.messageLog.Content import pw.binom.agentik.workingMemory.WorkingMemoryEntry import kotlin.test.Test diff --git a/storage-bundle/src/commonMain/kotlin/pw/binom/agentik/storageBundle/StorageBundle.kt b/storage-bundle/src/commonMain/kotlin/pw/binom/agentik/storageBundle/StorageBundle.kt index 69b114f..570d7bd 100644 --- a/storage-bundle/src/commonMain/kotlin/pw/binom/agentik/storageBundle/StorageBundle.kt +++ b/storage-bundle/src/commonMain/kotlin/pw/binom/agentik/storageBundle/StorageBundle.kt @@ -1,7 +1,7 @@ package pw.binom.agentik.storageBundle import pw.binom.agentik.messageStore.ConversationStore -import pw.binom.agentik.messageStore.MessageStore +import pw.binom.agentik.messageLog.MessageStore import pw.binom.agentik.messageStore.ReflectionStore import pw.binom.agentik.messageStore.events.EventStore import pw.binom.agentik.workingMemory.WorkingMemoryStore diff --git a/storage-inmemory/build.gradle.kts b/storage-inmemory/build.gradle.kts index d19f33d..e3605f0 100644 --- a/storage-inmemory/build.gradle.kts +++ b/storage-inmemory/build.gradle.kts @@ -21,6 +21,7 @@ kotlin { sourceSets { commonMain.dependencies { api(project(":message-store-api")) + api(project(":message-log-api")) api(project(":storage-bundle")) api(project(":working-memory-api")) } diff --git a/storage-inmemory/src/commonMain/kotlin/pw/binom/agentik/storage/inmemory/InMemoryMessageStore.kt b/storage-inmemory/src/commonMain/kotlin/pw/binom/agentik/storage/inmemory/InMemoryMessageStore.kt index 444e476..f37b8ff 100644 --- a/storage-inmemory/src/commonMain/kotlin/pw/binom/agentik/storage/inmemory/InMemoryMessageStore.kt +++ b/storage-inmemory/src/commonMain/kotlin/pw/binom/agentik/storage/inmemory/InMemoryMessageStore.kt @@ -5,10 +5,10 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import pw.binom.agentik.messageStore.MessageEvent -import pw.binom.agentik.messageStore.MessageRecord -import pw.binom.agentik.messageStore.MessageStore -import pw.binom.agentik.messageStore.TokenStats +import pw.binom.agentik.messageLog.MessageEvent +import pw.binom.agentik.messageLog.MessageRecord +import pw.binom.agentik.messageLog.MessageStore +import pw.binom.agentik.messageLog.TokenStats import kotlin.time.Instant /** diff --git a/storage-inmemory/src/commonTest/kotlin/pw/binom/agentik/storage/inmemory/InMemoryMessageStoreTest.kt b/storage-inmemory/src/commonTest/kotlin/pw/binom/agentik/storage/inmemory/InMemoryMessageStoreTest.kt index a001994..be22339 100644 --- a/storage-inmemory/src/commonTest/kotlin/pw/binom/agentik/storage/inmemory/InMemoryMessageStoreTest.kt +++ b/storage-inmemory/src/commonTest/kotlin/pw/binom/agentik/storage/inmemory/InMemoryMessageStoreTest.kt @@ -1,8 +1,8 @@ package pw.binom.agentik.storage.inmemory -import pw.binom.agentik.messageStore.Content -import pw.binom.agentik.messageStore.MessageRecord -import pw.binom.agentik.messageStore.TurnTokens +import pw.binom.agentik.messageLog.Content +import pw.binom.agentik.messageLog.MessageRecord +import pw.binom.agentik.messageLog.TurnTokens import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull @@ -113,8 +113,8 @@ class InMemoryMessageStoreTest { yield() // даём коллектору подписаться ДО append — иначе SharedFlow без replay потеряет эвент store.append(MessageRecord.UserMessage("u1", "c1", listOf(Content.Text("hi")), t0)) val ev = deferred.await() - assertTrue(ev is pw.binom.agentik.messageStore.MessageEvent.Appended) - val appended = ev as pw.binom.agentik.messageStore.MessageEvent.Appended + assertTrue(ev is pw.binom.agentik.messageLog.MessageEvent.Appended) + val appended = ev as pw.binom.agentik.messageLog.MessageEvent.Appended assertEquals("c1", appended.conversationId) assertEquals("u1", appended.record.id) } diff --git a/storage-inmemory/src/commonTest/kotlin/pw/binom/agentik/storage/inmemory/InMemoryWorkingMemoryStoreTest.kt b/storage-inmemory/src/commonTest/kotlin/pw/binom/agentik/storage/inmemory/InMemoryWorkingMemoryStoreTest.kt index 763c28f..172212a 100644 --- a/storage-inmemory/src/commonTest/kotlin/pw/binom/agentik/storage/inmemory/InMemoryWorkingMemoryStoreTest.kt +++ b/storage-inmemory/src/commonTest/kotlin/pw/binom/agentik/storage/inmemory/InMemoryWorkingMemoryStoreTest.kt @@ -1,6 +1,6 @@ package pw.binom.agentik.storage.inmemory -import pw.binom.agentik.messageStore.Content +import pw.binom.agentik.messageLog.Content import pw.binom.agentik.workingMemory.WorkingMemoryEntry import kotlin.test.Test import kotlin.test.assertEquals diff --git a/storage-ksqlite/build.gradle.kts b/storage-ksqlite/build.gradle.kts index d605e91..38c6db9 100644 --- a/storage-ksqlite/build.gradle.kts +++ b/storage-ksqlite/build.gradle.kts @@ -24,6 +24,7 @@ kotlin { sourceSets { commonMain.dependencies { api(project(":message-store-api")) + api(project(":message-log-api")) api(project(":storage-bundle")) api(project(":working-memory-api")) diff --git a/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteMessageStore.kt b/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteMessageStore.kt index 7cdc1d5..24f505a 100644 --- a/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteMessageStore.kt +++ b/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteMessageStore.kt @@ -1,11 +1,11 @@ package pw.binom.agentik.storage.ksqlite import kotlinx.serialization.json.Json -import pw.binom.agentik.messageStore.MessageRecord -import pw.binom.agentik.messageStore.MessageStore -import pw.binom.agentik.messageStore.TokenStats -import pw.binom.agentik.messageStore.decodeBodyPayload -import pw.binom.agentik.messageStore.encodeBodyPayload +import pw.binom.agentik.messageLog.MessageRecord +import pw.binom.agentik.messageLog.MessageStore +import pw.binom.agentik.messageLog.TokenStats +import pw.binom.agentik.messageLog.decodeBodyPayload +import pw.binom.agentik.messageLog.encodeBodyPayload import pw.binom.db.ksqlite.SQLiteConnection import pw.binom.db.ksqlite.SQLiteResultSet import kotlin.time.Instant diff --git a/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteStores.kt b/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteStores.kt index 0cdfd9a..799970d 100644 --- a/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteStores.kt +++ b/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteStores.kt @@ -1,7 +1,7 @@ package pw.binom.agentik.storage.ksqlite import pw.binom.agentik.messageStore.ConversationStore -import pw.binom.agentik.messageStore.MessageStore +import pw.binom.agentik.messageLog.MessageStore import pw.binom.agentik.messageStore.ReflectionStore import pw.binom.agentik.storageBundle.StorageBundle import pw.binom.agentik.workingMemory.WorkingMemoryStore diff --git a/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/MessageCodecs.kt b/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/MessageCodecs.kt index fc3f54b..35c0f6b 100644 --- a/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/MessageCodecs.kt +++ b/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/MessageCodecs.kt @@ -1,9 +1,9 @@ package pw.binom.agentik.storage.ksqlite import kotlinx.serialization.json.Json -import pw.binom.agentik.messageStore.MessageRecord -import pw.binom.agentik.messageStore.decodeBodyPayload -import pw.binom.agentik.messageStore.encodeBodyPayload +import pw.binom.agentik.messageLog.MessageRecord +import pw.binom.agentik.messageLog.decodeBodyPayload +import pw.binom.agentik.messageLog.encodeBodyPayload import pw.binom.db.ksqlite.SQLiteResultSet import kotlin.time.Instant @@ -36,8 +36,6 @@ internal fun encodeRecord(record: MessageRecord): Pair = when (r ErrorPayload.serializer(), ErrorPayload(message = record.message, code = record.code), ) - is MessageRecord.Summary, - is MessageRecord.System -> error("Summary/System — synthetic, cannot append to audit log") } internal fun SQLiteResultSet.toMessageRecord(json: Json): MessageRecord { diff --git a/storage-ksqlite/src/commonTest/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteMessageStoreTest.kt b/storage-ksqlite/src/commonTest/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteMessageStoreTest.kt index d4967ac..8f53c3b 100644 --- a/storage-ksqlite/src/commonTest/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteMessageStoreTest.kt +++ b/storage-ksqlite/src/commonTest/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteMessageStoreTest.kt @@ -1,9 +1,9 @@ package pw.binom.agentik.storage.ksqlite import kotlinx.coroutines.test.runTest -import pw.binom.agentik.messageStore.Content -import pw.binom.agentik.messageStore.MessageRecord -import pw.binom.agentik.messageStore.TurnTokens +import pw.binom.agentik.messageLog.Content +import pw.binom.agentik.messageLog.MessageRecord +import pw.binom.agentik.messageLog.TurnTokens import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test diff --git a/storage-ksqlite/src/commonTest/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteWorkingMemoryStoreTest.kt b/storage-ksqlite/src/commonTest/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteWorkingMemoryStoreTest.kt index 5397edb..551d980 100644 --- a/storage-ksqlite/src/commonTest/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteWorkingMemoryStoreTest.kt +++ b/storage-ksqlite/src/commonTest/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteWorkingMemoryStoreTest.kt @@ -1,7 +1,7 @@ package pw.binom.agentik.storage.ksqlite import kotlinx.coroutines.test.runTest -import pw.binom.agentik.messageStore.Content +import pw.binom.agentik.messageLog.Content import pw.binom.agentik.workingMemory.WorkingMemoryEntry import kotlin.test.AfterTest import kotlin.test.BeforeTest diff --git a/storage-sqlite/build.gradle.kts b/storage-sqlite/build.gradle.kts index 1a8ad5e..7fe0c1e 100644 --- a/storage-sqlite/build.gradle.kts +++ b/storage-sqlite/build.gradle.kts @@ -16,6 +16,7 @@ kotlin { sourceSets { commonMain.dependencies { api(project(":message-store-api")) + api(project(":message-log-api")) api(project(":storage-bundle")) api(project(":working-memory-api")) api(libs.sqldelight.runtime) diff --git a/storage-sqlite/src/jvmMain/kotlin/pw/binom/agentik/storage/sqlite/SqliteMessageStore.kt b/storage-sqlite/src/jvmMain/kotlin/pw/binom/agentik/storage/sqlite/SqliteMessageStore.kt index 5a78e38..f521a11 100644 --- a/storage-sqlite/src/jvmMain/kotlin/pw/binom/agentik/storage/sqlite/SqliteMessageStore.kt +++ b/storage-sqlite/src/jvmMain/kotlin/pw/binom/agentik/storage/sqlite/SqliteMessageStore.kt @@ -1,11 +1,11 @@ package pw.binom.agentik.storage.sqlite import kotlinx.serialization.json.Json -import pw.binom.agentik.messageStore.MessageRecord -import pw.binom.agentik.messageStore.MessageStore -import pw.binom.agentik.messageStore.TokenStats -import pw.binom.agentik.messageStore.decodeBodyPayload -import pw.binom.agentik.messageStore.encodeBodyPayload +import pw.binom.agentik.messageLog.MessageRecord +import pw.binom.agentik.messageLog.MessageStore +import pw.binom.agentik.messageLog.TokenStats +import pw.binom.agentik.messageLog.decodeBodyPayload +import pw.binom.agentik.messageLog.encodeBodyPayload import kotlin.time.Instant /** @@ -97,8 +97,6 @@ private fun encodeRecord(record: MessageRecord): Pair = when (re ErrorPayload.serializer(), ErrorPayload(message = record.message, code = record.code), ) - is MessageRecord.Summary, - is MessageRecord.System -> error("Summary/System — synthetic, cannot append to audit log") } @kotlinx.serialization.Serializable diff --git a/storage-sqlite/src/jvmMain/kotlin/pw/binom/agentik/storage/sqlite/SqliteStores.kt b/storage-sqlite/src/jvmMain/kotlin/pw/binom/agentik/storage/sqlite/SqliteStores.kt index a9cdc9e..f39b54a 100644 --- a/storage-sqlite/src/jvmMain/kotlin/pw/binom/agentik/storage/sqlite/SqliteStores.kt +++ b/storage-sqlite/src/jvmMain/kotlin/pw/binom/agentik/storage/sqlite/SqliteStores.kt @@ -4,7 +4,7 @@ import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver import pw.binom.agentik.messageStore.ConversationStore -import pw.binom.agentik.messageStore.MessageStore +import pw.binom.agentik.messageLog.MessageStore import pw.binom.agentik.messageStore.ReflectionStore import pw.binom.agentik.storageBundle.StorageBundle import pw.binom.agentik.messageStore.events.EventStore diff --git a/working-memory-api/build.gradle.kts b/working-memory-api/build.gradle.kts index 6c4ce1d..2c180a1 100644 --- a/working-memory-api/build.gradle.kts +++ b/working-memory-api/build.gradle.kts @@ -19,6 +19,7 @@ kotlin { sourceSets { commonMain.dependencies { api(project(":message-store-api")) + api(project(":message-log-api")) api(libs.kotlinx.coroutines.core) api(libs.kotlinx.serialization.core) api(libs.kotlinx.serialization.json) diff --git a/working-memory-api/src/commonMain/kotlin/pw/binom/agentik/workingMemory/WorkingMemoryEntry.kt b/working-memory-api/src/commonMain/kotlin/pw/binom/agentik/workingMemory/WorkingMemoryEntry.kt index 88bff54..b0d7d4a 100644 --- a/working-memory-api/src/commonMain/kotlin/pw/binom/agentik/workingMemory/WorkingMemoryEntry.kt +++ b/working-memory-api/src/commonMain/kotlin/pw/binom/agentik/workingMemory/WorkingMemoryEntry.kt @@ -2,8 +2,8 @@ package pw.binom.agentik.workingMemory import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import pw.binom.agentik.messageStore.Content -import pw.binom.agentik.messageStore.MessageContext +import pw.binom.agentik.messageLog.Content +import pw.binom.agentik.messageLog.MessageContext /** * Запись в working memory диалога: ровно то, что агент сейчас видит в