From bd65c29b48b5c9e8e6f95e627b10610f19805a0a Mon Sep 17 00:00:00 2001 From: subochev Date: Sun, 20 Sep 2026 21:08:55 +0300 Subject: [PATCH] refactor(message-log-api): split `MessageStore` into read-only and mutable interfaces - Introduced `MutableMessageStore` for producers with an `append` operation, separate from read-only `MessageStore`. - Updated all consumers and implementations to use the appropriate interface (`read-only` for observers, `mutable` for producers). - Improves modularity and ensures compile-time guarantees against unintended write operations in the audit log. --- .../binom/agentik/messageLog/MessageStore.kt | 20 ++++++++++++------- .../agentik/messageLog/MutableMessageStore.kt | 17 ++++++++++++++++ .../agentik/standalone/agent/ChatAgent.kt | 4 ++-- .../standalone/agent/ConversationLoop.kt | 4 ++-- .../standalone/agent/ToolDispatcher.kt | 4 ++-- .../storage/inmemory/InMemoryMessageStore.kt | 4 ++-- .../storage/inmemory/InMemoryStorage.kt | 4 ++-- .../storage/ksqlite/KsqliteMessageStore.kt | 4 ++-- .../agentik/storage/ksqlite/KsqliteStores.kt | 4 ++-- .../storage/sqlite/SqliteMessageStore.kt | 4 ++-- .../agentik/storage/sqlite/SqliteStores.kt | 4 ++-- 11 files changed, 48 insertions(+), 25 deletions(-) create mode 100644 message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/MutableMessageStore.kt 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 index e03661a..597ad74 100644 --- 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 @@ -4,21 +4,27 @@ import kotlinx.coroutines.flow.Flow import kotlin.time.Instant /** - * Append-only audit log сообщений. + * Append-only audit log сообщений — read-only представление. * - * Только `append` и чтение. Никаких обновлений, никакого удаления - * (кроме каскадного вместе с ConversationStore.delete). + * Producer-операция [MutableMessageStore.append] находится на + * [MutableMessageStore] — этот интерфейс только для чтения, чтобы + * consumer'ы физически не могли писать в audit log. + * + * Никаких обновлений, никакого удаления (кроме каскадного вместе + * с 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 + /** + * Subscribe на события append'ов. Default — `emptyFlow()` для store'ов + * без live-уведомлений (например, SQLite без триггеров); impl'ы с + * in-memory notification (`:storage-inmemory`) override'ят. + */ fun events(): Flow = kotlinx.coroutines.flow.emptyFlow() suspend fun tokenStats(conversationId: String): TokenStats -} - +} \ No newline at end of file diff --git a/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/MutableMessageStore.kt b/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/MutableMessageStore.kt new file mode 100644 index 0000000..443dd6d --- /dev/null +++ b/message-log-api/src/commonMain/kotlin/pw/binom/agentik/messageLog/MutableMessageStore.kt @@ -0,0 +1,17 @@ +package pw.binom.agentik.messageLog + +/** + * Mutable вариант [MessageStore] — добавляет producer-операцию [append]. + * + * Этот интерфейс предназначен **только для producer'ов** (ChatAgent, + * ConversationLoop, ToolDispatcher, sub-agents, A2A-bridge). + * Consumer'ы (DebugRoutes, admin dashboards, parent agents) должны + * принимать **read-only** [MessageStore] — тогда невозможно случайно + * записать в audit log из observer'а. + * + * **Append семантика**: см. KDoc [MessageStore.append][MessageStore] — + * на этом интерфейсе (не дублируем). + */ +interface MutableMessageStore : MessageStore { + suspend fun append(record: MessageRecord) +} \ No newline at end of file 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 aeb0fbb..e582854 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 @@ -30,7 +30,7 @@ import pw.binom.agentik.messageStore.ConversationStore import pw.binom.agentik.messageStore.Ids import pw.binom.agentik.messageStore.Reflection import pw.binom.agentik.messageStore.ReflectionStore -import pw.binom.agentik.messageLog.MessageStore +import pw.binom.agentik.messageLog.MutableMessageStore import pw.binom.agentik.workingMemory.WorkingMemoryStore import pw.binom.agentik.toolsets.DisableToolsetTool import pw.binom.agentik.toolsets.EnableToolsetTool @@ -64,7 +64,7 @@ import pw.binom.litert.LiteLlm class ChatAgent( override val id: String, private val conversationStore: ConversationStore, - private val messageStore: MessageStore, + private val messageStore: MutableMessageStore, private val workingMemoryStore: WorkingMemoryStore, private val reflectionStore: ReflectionStore, private val llm: LiteLlm, 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 47d5eb5..c6df4eb 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 @@ -34,7 +34,7 @@ import pw.binom.agentik.messageStore.ConversationStore 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.messageLog.MutableMessageStore import pw.binom.agentik.messageStore.ReflectionStore import pw.binom.agentik.messageLog.TurnTokens import pw.binom.agentik.workingMemory.WorkingMemoryEntry @@ -55,7 +55,7 @@ import pw.binom.agentik.toolsets.NamedTool class ConversationLoop( record: ConversationRecord, private val conversationStore: ConversationStore, - private val messageStore: MessageStore, + private val messageStore: MutableMessageStore, private val workingMemoryStore: WorkingMemoryStore, private val reflectionStore: ReflectionStore?, private val eventStore: pw.binom.agentik.eventStore.MutableEventStore, 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 831904c..2f7ec40 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 @@ -6,7 +6,7 @@ import kotlinx.coroutines.async import mu.KotlinLogging import pw.binom.agentik.proto.Event as ProtoEvent import pw.binom.agentik.messageLog.MessageRecord -import pw.binom.agentik.messageLog.MessageStore +import pw.binom.agentik.messageLog.MutableMessageStore import pw.binom.agentik.workingMemory.WorkingMemoryEntry import pw.binom.agentik.toolsets.ToolsetDispatchPolicy import pw.binom.litert.LiteToolCall @@ -16,7 +16,7 @@ import pw.binom.agentik.toolsets.NamedTool internal class ToolDispatcher( private val state: ConversationState, - private val messageStore: MessageStore, + private val messageStore: MutableMessageStore, private val events: ConversationEvents, private val backgroundEvents: BackgroundEventBus, private val toolsByName: MutableMap, 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 f37b8ff..d89dd19 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 @@ -7,7 +7,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import pw.binom.agentik.messageLog.MessageEvent import pw.binom.agentik.messageLog.MessageRecord -import pw.binom.agentik.messageLog.MessageStore +import pw.binom.agentik.messageLog.MutableMessageStore import pw.binom.agentik.messageLog.TokenStats import kotlin.time.Instant @@ -22,7 +22,7 @@ import kotlin.time.Instant * любом KMP-таргете (включая iOS/native, где SQLite через Android driver * недоступен). */ -class InMemoryMessageStore : MessageStore { +class InMemoryMessageStore : MutableMessageStore { private val byConv: MutableMap> = mutableMapOf() private val events = MutableSharedFlow(extraBufferCapacity = 64) diff --git a/storage-inmemory/src/commonMain/kotlin/pw/binom/agentik/storage/inmemory/InMemoryStorage.kt b/storage-inmemory/src/commonMain/kotlin/pw/binom/agentik/storage/inmemory/InMemoryStorage.kt index 8fbd162..f750237 100644 --- a/storage-inmemory/src/commonMain/kotlin/pw/binom/agentik/storage/inmemory/InMemoryStorage.kt +++ b/storage-inmemory/src/commonMain/kotlin/pw/binom/agentik/storage/inmemory/InMemoryStorage.kt @@ -1,6 +1,6 @@ package pw.binom.agentik.storage.inmemory -import pw.binom.agentik.messageLog.MessageStore +import pw.binom.agentik.messageLog.MutableMessageStore import pw.binom.agentik.messageStore.ConversationStore import pw.binom.agentik.messageStore.ReflectionStore import pw.binom.agentik.workingMemory.WorkingMemoryStore @@ -22,7 +22,7 @@ import kotlin.time.Clock object InMemoryStorage { data class Bundle( val conversationStore: ConversationStore, - val messageStore: MessageStore, + val messageStore: MutableMessageStore, val workingMemoryStore: WorkingMemoryStore, val reflectionStore: ReflectionStore, ) 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 24f505a..f3a6af4 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 @@ -2,7 +2,7 @@ package pw.binom.agentik.storage.ksqlite import kotlinx.serialization.json.Json import pw.binom.agentik.messageLog.MessageRecord -import pw.binom.agentik.messageLog.MessageStore +import pw.binom.agentik.messageLog.MutableMessageStore import pw.binom.agentik.messageLog.TokenStats import pw.binom.agentik.messageLog.decodeBodyPayload import pw.binom.agentik.messageLog.encodeBodyPayload @@ -24,7 +24,7 @@ import kotlinx.coroutines.withContext */ class KsqliteMessageStore( private val connection: SQLiteConnection, -) : MessageStore { +) : MutableMessageStore { private val mutex = Mutex() private val json = Json { ignoreUnknownKeys = true } 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 2903a7f..7c131db 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.messageLog.MessageStore +import pw.binom.agentik.messageLog.MutableMessageStore import pw.binom.agentik.messageStore.ReflectionStore import pw.binom.agentik.workingMemory.WorkingMemoryStore import pw.binom.db.ksqlite.SQLiteConnection @@ -18,7 +18,7 @@ import pw.binom.db.ksqlite.SQLiteConnection class KsqliteStores private constructor( val connection: SQLiteConnection, val conversations: ConversationStore, - val messages: MessageStore, + val messages: MutableMessageStore, val workingMemory: WorkingMemoryStore, val reflections: ReflectionStore, ) : AutoCloseable { 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 f521a11..47d800d 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 @@ -2,7 +2,7 @@ package pw.binom.agentik.storage.sqlite import kotlinx.serialization.json.Json import pw.binom.agentik.messageLog.MessageRecord -import pw.binom.agentik.messageLog.MessageStore +import pw.binom.agentik.messageLog.MutableMessageStore import pw.binom.agentik.messageLog.TokenStats import pw.binom.agentik.messageLog.decodeBodyPayload import pw.binom.agentik.messageLog.encodeBodyPayload @@ -19,7 +19,7 @@ import kotlin.time.Instant * SQLDelight сохраняет snake_case в сгенерированной data class (`Message`), * поэтому обращаемся через `conversation_id`, `payload_json`, `created_at`. */ -class SqliteMessageStore(private val db: AgentikDatabase) : MessageStore { +class SqliteMessageStore(private val db: AgentikDatabase) : MutableMessageStore { private val q get() = db.messageQueries 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 5fe4b2b..acdd036 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.messageLog.MessageStore +import pw.binom.agentik.messageLog.MutableMessageStore import pw.binom.agentik.messageStore.ReflectionStore import pw.binom.agentik.storage.sqlite.SqliteReflectionStore import pw.binom.agentik.workingMemory.WorkingMemoryStore @@ -15,7 +15,7 @@ import pw.binom.agentik.workingMemory.WorkingMemoryStore class SqliteStores private constructor( val driver: SqlDriver, val conversations: ConversationStore, - val messages: MessageStore, + val messages: MutableMessageStore, val workingMemory: WorkingMemoryStore, val reflections: ReflectionStore, ) : AutoCloseable {