refactor(storage): split :storage-core into message-store-api + working-memory-api
ci / JVM build + tests (push) Failing after 2m5s

Разделяет монолитный :storage-core на 3 модуля с чёткими границами:

  :message-store-api   — MessageStore, ReflectionStore, EventStore, ConversationStore +
                          Content, Payload, MessageContext, Ids, MessageEvent
                          (audit log + event stream)
  :working-memory-api  — WorkingMemoryStore + WorkingMemoryEntry
                          (runtime context с compaction)
  :storage-bundle      — StorageBundle агрегатор, зависит от обоих
                          (только для server-side runtime)

Пакеты:
  pw.binom.agentik.storage.*  → УДАЛЕНО
  pw.binom.agentik.messageStore.*        — append-only API
  pw.binom.agentik.messageStore.events.* — EventStore + EventRecord
  pw.binom.agentik.workingMemory.*      — WM API
  pw.binom.agentik.storageBundle.*       — aggregator

Зачем:
  - Тонкий клиент может подтянуть ТОЛЬКО :message-store-api (~15KB, нет
    compaction-логики, нет MessageStore+WorkingMemoryStore cross-deps).
  - Android-agent в будущем подключит :message-store-api для audit log,
    серверный runtime — :storage-bundle со всем.
  - Компиляционные границы защищают от случайной зависимости от WM
    в read-only клиентах (раньше один :storage-core не давал такой
    гарантии).

Миграция:
  - Имплементации (:storage-inmemory, :storage-sqlite, :storage-ksqlite)
    обновили package + добавили deps на оба API модуля + :storage-bundle.
  - Тесты из :storage-core (PersistenceTest, SqliteStoresMigrationTest,
    TokenStatsTest) переехали в :standalone, получили testImplementation
    на оба API модуля и импорты новых типов.
  - 52 файла в :standalone, :agent-toolsets, :llm-tools, :server, :client,
    :agentik-cli обновили FQN.
  - :storage-core удалён.

Совместимость схем не меняется — все 5 impl'ов (3 backend × 5 store) хранят
данные в тех же таблицах, миграция между Sqlite и Ksqlite возможна через SQL dump.

Тесты:
  standalone         178 ✅
  agent-toolsets      36 ✅
  storage-inmemory    47 ✅
  storage-sqlite      17 ✅  (включая переехавшие persistence/* + tokenStats)
  storage-ksqlite     36 ✅
  ---
  Total: 314 tests, 0 failures
This commit is contained in:
2026-09-20 15:02:54 +03:00
parent dd7aec8df1
commit 2d9ad526bb
82 changed files with 341 additions and 403 deletions
+2 -2
View File
@@ -21,8 +21,8 @@ kotlin {
sourceSets {
commonMain.dependencies {
// :storage-core — для StorageBundle в ToolsetContext (commit 5+)
api(project(":storage-core"))
api(project(":storage-bundle"))
// :storage-bundle transitively подтягивает :message-store-api и :working-memory-api.
// litert-kmp: LiteTool интерфейс (sync describe/invoke)
api(libs.litert.api)
@@ -136,7 +136,7 @@ internal class AgentClient(
/**
* Запись event'а, которую возвращает /events/replay endpoint.
*
* Это **мини-зеркало** `pw.binom.agentik.storage.events.EventRecord` — клиент
* Это **мини-зеркало** `pw.binom.agentik.messageStore.events.EventRecord` — клиент
* не зависит от `:storage-core`, поэтому определяет свою модель (wire-only).
*
* Формат полностью совместим с сервером — `agentikJson.encodeToString(...)` там
+2 -1
View File
@@ -19,7 +19,8 @@ kotlin {
sourceSets {
commonMain.dependencies {
api(project(":memory-api"))
api(project(":storage-core"))
api(project(":message-store-api"))
api(project(":working-memory-api"))
api(project(":skills"))
api(libs.litert.api)
implementation(libs.kotlinx.coroutines.core)
@@ -10,7 +10,7 @@ import pw.binom.agentik.memory.MemoryStore
import pw.binom.agentik.memory.MemoryStoreEvent
import pw.binom.agentik.memory.NewMemoryNote
import pw.binom.agentik.memory.ReviewedTurn
import pw.binom.agentik.storage.Ids
import pw.binom.agentik.messageStore.Ids
import pw.binom.litert.LiteLlm
import kotlin.time.Clock
import kotlin.time.Instant
@@ -5,8 +5,8 @@ import kotlinx.coroutines.withContext
import pw.binom.agentik.memory.ConversationTurn
import pw.binom.litert.LiteConversationConfig
import pw.binom.litert.LiteLlm
import pw.binom.agentik.storage.Ids
import pw.binom.agentik.storage.Reflection
import pw.binom.agentik.messageStore.Ids
import pw.binom.agentik.messageStore.Reflection
import kotlin.time.Clock
/**
+33
View File
@@ -0,0 +1,33 @@
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.serialization)
}
// Public API для append-only storage агента (audit log, replay-after-disconnect,
// conversation metadata). Этот модуль НЕ содержит working memory — он для
// тонких клиентов, которые хотят только события/сообщения без тяжёлой
// runtime-логики.
//
// Зависимости:
// - kotlinx.coroutines / serialization (KMP).
// - БЕЗ :storage-core — мы его полностью выпиливаем, интерфейсы переезжают сюда.
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)
}
}
}
@@ -1,4 +1,4 @@
package pw.binom.agentik.storage
package pw.binom.agentik.messageStore
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -1,4 +1,4 @@
package pw.binom.agentik.storage
package pw.binom.agentik.messageStore
import kotlin.time.Instant
@@ -1,4 +1,4 @@
package pw.binom.agentik.storage
package pw.binom.agentik.messageStore
import kotlin.time.Instant
@@ -1,4 +1,4 @@
package pw.binom.agentik.storage
package pw.binom.agentik.messageStore
import kotlin.uuid.Uuid
@@ -1,4 +1,4 @@
package pw.binom.agentik.storage
package pw.binom.agentik.messageStore
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -1,4 +1,4 @@
package pw.binom.agentik.storage
package pw.binom.agentik.messageStore
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -1,4 +1,4 @@
package pw.binom.agentik.storage
package pw.binom.agentik.messageStore
import kotlinx.coroutines.flow.Flow
import kotlin.time.Instant
@@ -1,4 +1,4 @@
package pw.binom.agentik.storage
package pw.binom.agentik.messageStore
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -1,4 +1,4 @@
package pw.binom.agentik.storage
package pw.binom.agentik.messageStore
import kotlinx.coroutines.flow.Flow
import kotlin.time.Instant
@@ -1,4 +1,4 @@
package pw.binom.agentik.storage.events
package pw.binom.agentik.messageStore.events
import kotlinx.serialization.Serializable
import kotlin.time.Instant
+2 -1
View File
@@ -23,7 +23,8 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(project(":proto"))
implementation(project(":storage-core"))
implementation(project(":message-store-api"))
implementation(project(":working-memory-api"))
// Ktor (без engine — engine подключает потребитель, см. :standalone).
implementation(libs.ktor.server.core)
@@ -6,7 +6,7 @@ import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.routing.Route
import io.ktor.server.routing.route
import pw.binom.agentik.proto.Agent
import pw.binom.agentik.storage.events.EventStore
import pw.binom.agentik.messageStore.events.EventStore
/**
* Встраивает HTTP/SSE-фасад протокола agentik в твой Ktor-роутинг.
@@ -25,7 +25,7 @@ import pw.binom.agentik.proto.AllEvent
import pw.binom.agentik.proto.Conversation
import pw.binom.agentik.proto.Content
import pw.binom.agentik.proto.Event
import pw.binom.agentik.storage.events.EventStore
import pw.binom.agentik.messageStore.events.EventStore
import kotlin.time.Instant
internal fun Route.agentikRoutes(agent: Agent, eventStore: EventStore? = null) {
+5 -3
View File
@@ -63,12 +63,14 @@ include(":memory-vector")
// Интерфейсы хранилища и разговорной истории: MessageStore / WorkingMemoryStore /
// ConversationStore / ReflectionStore + StorageBundle агрегатор. Реализации —
// в :storage-inmemory / :storage-sqlite / :storage-android (последний deferred).
include(":storage-core")
// In-memory реализация всех 4 store'ов из :storage-core. KMP, без платформенных
// In-memory реализация store'ов из :message-store-api и :working-memory-api. KMP,
// IO-зависимостей. Используется в тестах (быстрый setup, без JDBC) и будет
// использоваться в Android-сборке (JVector/SQLite не подходят для ART out-of-box).
include(":message-store-api")
include(":working-memory-api")
include(":storage-bundle")
include(":storage-inmemory")
// SQLDelight-реализация всех 4 store'ов из :storage-core. JVM-only (SQLDelight
// SQLDelight-реализация store'ов из :message-store-api и :working-memory-api. JVM-only
// native драйверов для KMP вне JVM пока не публикует). Содержит 4 .sq-файла +
// 5 классов: SqliteStores, SqliteConversationStore, SqliteMessageStore,
// SqliteWorkingMemoryStore, SqliteReflectionStore. Бэкенд для прод-запуска
+3 -1
View File
@@ -32,6 +32,7 @@ kotlin {
sourceSets {
commonMain.dependencies {
api(project(":storage-bundle"))
implementation(project(":proto"))
implementation(project(":server"))
@@ -56,7 +57,8 @@ kotlin {
if (!skipVectorMemory) {
implementation(project(":memory-vector"))
}
implementation(project(":storage-core"))
implementation(project(":message-store-api"))
implementation(project(":working-memory-api"))
implementation(project(":storage-sqlite"))
implementation(project(":agent-toolsets"))
// Generic LLM-side tools (LlmReflector, SkillMiner, LlmMemoryReviewer,
@@ -14,7 +14,7 @@ import pw.binom.agentik.standalone.agent.ChatConversation
import pw.binom.agentik.llm.tools.LlmReflector
import pw.binom.agentik.llm.tools.SkillMiner
import pw.binom.agentik.standalone.agent.memory.Curator
import pw.binom.agentik.storage.StorageBundle
import pw.binom.agentik.storageBundle.StorageBundle
import pw.binom.agentik.skills.SkillStore
/**
@@ -33,7 +33,7 @@ import pw.binom.agentik.skills.SkillStore
*/
internal fun Route.debugRoutes(
agent: Agent,
storage: pw.binom.agentik.storage.StorageBundle,
storage: pw.binom.agentik.storageBundle.StorageBundle,
reflector: LlmReflector?,
skillMiner: SkillMiner?,
skillStore: SkillStore?,
@@ -127,14 +127,14 @@ internal fun Route.debugRoutes(
* (для debug-триггеров reflector/miner; та же логика, что у хуков
* [ChatConversation]).
*/
internal suspend fun recentTurns(storage: pw.binom.agentik.storage.StorageBundle, conversationId: String, maxTurns: Int): List<ConversationTurn> {
internal suspend fun recentTurns(storage: pw.binom.agentik.storageBundle.StorageBundle, conversationId: String, maxTurns: Int): List<ConversationTurn> {
val rows = storage.workingMemoryStore.list(conversationId)
val pairs = mutableListOf<ConversationTurn>()
var pendingUser: String? = null
for (row in rows) {
when (val e = row.entry) {
is pw.binom.agentik.storage.WorkingMemoryEntry.User -> pendingUser = e.content.text()
is pw.binom.agentik.storage.WorkingMemoryEntry.Assistant -> {
is pw.binom.agentik.workingMemory.WorkingMemoryEntry.User -> pendingUser = e.content.text()
is pw.binom.agentik.workingMemory.WorkingMemoryEntry.Assistant -> {
val user = pendingUser ?: ""
pendingUser = null
pairs += ConversationTurn(userMessage = user, assistantMessage = e.content.text())
@@ -146,5 +146,5 @@ internal suspend fun recentTurns(storage: pw.binom.agentik.storage.StorageBundle
}
/** Текстовое содержимое записей working memory (Text-контент, без картинок). */
internal fun List<pw.binom.agentik.storage.Content>.text(): String =
filterIsInstance<pw.binom.agentik.storage.Content.Text>().joinToString("\n") { it.body }
internal fun List<pw.binom.agentik.messageStore.Content>.text(): String =
filterIsInstance<pw.binom.agentik.messageStore.Content.Text>().joinToString("\n") { it.body }
@@ -317,7 +317,7 @@ private fun runServer() {
// и interval > 0. Загружаем top-K последних рефлексий из SQLite в system prompt.
val reflector: pw.binom.agentik.llm.tools.LlmReflector? =
if (config.reflection.interval > 0) LlmReflector(llm = llm) else null
val recentReflections: List<pw.binom.agentik.storage.Reflection> =
val recentReflections: List<pw.binom.agentik.messageStore.Reflection> =
if (config.reflection.topK > 0) kotlinx.coroutines.runBlocking {
storage.reflectionStore.listRecent(config.reflection.topK)
} else emptyList()
@@ -12,10 +12,10 @@ 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.storage.Content
import pw.binom.agentik.storage.ReflectionStore
import pw.binom.agentik.storage.WorkingMemoryEntry
import pw.binom.agentik.storage.WorkingMemoryStore
import pw.binom.agentik.messageStore.Content
import pw.binom.agentik.messageStore.ReflectionStore
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
import pw.binom.agentik.workingMemory.WorkingMemoryStore
import pw.binom.agentik.llm.tools.LlmReflector
import pw.binom.agentik.llm.tools.SkillMiner
import java.util.concurrent.atomic.AtomicLong
@@ -30,13 +30,13 @@ import pw.binom.agentik.skills.SkillCatalog
import pw.binom.agentik.skills.renderSystemPromptSection
import pw.binom.agentik.standalone.agent.memory.MemoryToolsFactory
import pw.binom.agentik.standalone.llm.LlmConfig
import pw.binom.agentik.storage.ConversationRecord
import pw.binom.agentik.storage.Ids
import pw.binom.agentik.storage.Reflection
import pw.binom.agentik.storage.StorageBundle
import pw.binom.agentik.storage.WorkingMemoryEntry
import pw.binom.agentik.storage.events.EventRecord
import pw.binom.agentik.storage.events.EventType
import pw.binom.agentik.messageStore.ConversationRecord
import pw.binom.agentik.messageStore.Ids
import pw.binom.agentik.messageStore.Reflection
import pw.binom.agentik.storageBundle.StorageBundle
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
import pw.binom.agentik.messageStore.events.EventRecord
import pw.binom.agentik.messageStore.events.EventType
import pw.binom.agentik.toolsets.DisableToolsetTool
import pw.binom.agentik.toolsets.EnableToolsetTool
import pw.binom.agentik.toolsets.NamedTool
@@ -68,7 +68,7 @@ import pw.binom.litert.LiteLlm
*/
class ChatAgent(
override val id: String,
private val storage: pw.binom.agentik.storage.StorageBundle,
private val storage: pw.binom.agentik.storageBundle.StorageBundle,
private val llm: LiteLlm,
private val llmConfig: LlmConfig,
private val tools: List<NamedTool> = emptyList(),
@@ -296,7 +296,7 @@ class ChatAgent(
override fun createConversation(temp: Boolean): ProtoConversation {
val now = now()
val id = pw.binom.agentik.storage.Ids.new("conv")
val id = pw.binom.agentik.messageStore.Ids.new("conv")
val rec = ConversationRecord(
id = id,
title = null,
@@ -6,10 +6,10 @@ 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.storage.Content
import pw.binom.agentik.storage.WorkingMemoryEntry
import pw.binom.agentik.storage.WorkingMemoryRow
import pw.binom.agentik.storage.WorkingMemoryStore
import pw.binom.agentik.messageStore.Content
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
import pw.binom.agentik.workingMemory.WorkingMemoryRow
import pw.binom.agentik.workingMemory.WorkingMemoryStore
import pw.binom.litert.LiteContentPart
import pw.binom.litert.LiteConversation
import pw.binom.litert.LiteConversationConfig
@@ -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.storage.MessageContext
import pw.binom.agentik.storage.MessageOrigin
import pw.binom.agentik.messageStore.MessageContext
import pw.binom.agentik.messageStore.MessageOrigin
internal class ContextBuilder(
private val memoryPrefetcher: MemoryPrefetcher?,
@@ -8,10 +8,10 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import mu.KotlinLogging
import pw.binom.agentik.proto.Event as ProtoEvent
import pw.binom.agentik.storage.Ids
import pw.binom.agentik.storage.events.EventRecord
import pw.binom.agentik.storage.events.EventStore
import pw.binom.agentik.storage.events.EventType
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 {}
@@ -28,18 +28,18 @@ 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.storage.Content
import pw.binom.agentik.storage.ConversationRecord
import pw.binom.agentik.storage.ConversationStore
import pw.binom.agentik.storage.MessageContext
import pw.binom.agentik.storage.MessageOrigin
import pw.binom.agentik.storage.MessageRecord
import pw.binom.agentik.storage.MessageStore
import pw.binom.agentik.storage.ReflectionStore
import pw.binom.agentik.storage.StorageBundle
import pw.binom.agentik.storage.TurnTokens
import pw.binom.agentik.storage.WorkingMemoryEntry
import pw.binom.agentik.storage.WorkingMemoryStore
import pw.binom.agentik.messageStore.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.messageStore.ReflectionStore
import pw.binom.agentik.storageBundle.StorageBundle
import pw.binom.agentik.messageStore.TurnTokens
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
import pw.binom.agentik.workingMemory.WorkingMemoryStore
import pw.binom.agentik.toolsets.ToolsetDispatchPolicy
import pw.binom.litert.LiteContentPart
import pw.binom.litert.LiteConversation
@@ -469,7 +469,7 @@ class ConversationLoop(
private fun now(): Instant =
Instant.fromEpochMilliseconds(System.currentTimeMillis())
private fun newId(prefix: String): String = pw.binom.agentik.storage.Ids.new(prefix)
private fun newId(prefix: String): String = pw.binom.agentik.messageStore.Ids.new(prefix)
private fun encodeArgsJson(arguments: Map<String, Any?>): String {
val el = JsonElement.serializer()
@@ -1,7 +1,7 @@
package pw.binom.agentik.standalone.agent
import kotlinx.coroutines.CoroutineScope
import pw.binom.agentik.storage.ConversationRecord
import pw.binom.agentik.messageStore.ConversationRecord
import pw.binom.litert.LiteConversation
import java.util.concurrent.atomic.AtomicReference
import pw.binom.agentik.toolsets.NamedTool
@@ -5,9 +5,9 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import mu.KotlinLogging
import pw.binom.agentik.proto.Event as ProtoEvent
import pw.binom.agentik.storage.MessageRecord
import pw.binom.agentik.storage.MessageStore
import pw.binom.agentik.storage.WorkingMemoryEntry
import pw.binom.agentik.messageStore.MessageRecord
import pw.binom.agentik.messageStore.MessageStore
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
import pw.binom.agentik.toolsets.ToolsetDispatchPolicy
import pw.binom.litert.LiteToolCall
import pw.binom.litert.LiteTool
@@ -5,7 +5,7 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.time.Instant
import pw.binom.agentik.skills.SkillCatalog
import pw.binom.agentik.storage.Reflection
import pw.binom.agentik.messageStore.Reflection
class ChatAgentReflectionTest {
@@ -14,8 +14,8 @@ 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.storage.MessageRecord
import pw.binom.agentik.storage.WorkingMemoryEntry
import pw.binom.agentik.messageStore.MessageRecord
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
import pw.binom.agentik.storage.sqlite.SqliteStores
import pw.binom.litert.LiteContentPart
import pw.binom.litert.LiteConversation
@@ -41,7 +41,7 @@ import pw.binom.agentik.toolsets.NamedTool
class ChatAgentTest {
private lateinit var storage: pw.binom.agentik.storage.StorageBundle
private lateinit var storage: pw.binom.agentik.storageBundle.StorageBundle
private lateinit var fakeLlm: FakeLiteLlm
@BeforeTest
@@ -56,7 +56,7 @@ class ChatAgentTest {
}
private fun newAgent(
storage: pw.binom.agentik.storage.StorageBundle = this.storage,
storage: pw.binom.agentik.storageBundle.StorageBundle = this.storage,
llm: LiteLlm = this.fakeLlm,
tools: List<NamedTool> = emptyList(),
skills: SkillCatalog = SkillCatalog.EMPTY,
@@ -160,10 +160,10 @@ class ChatAgentTest {
// добавим сообщение, чтобы потом убедиться, что каскад сработал
storage.messageStore.append(
pw.binom.agentik.storage.MessageRecord.UserMessage(
pw.binom.agentik.messageStore.MessageRecord.UserMessage(
id = "m1",
conversationId = id,
content = listOf(pw.binom.agentik.storage.Content.Text("hi")),
content = listOf(pw.binom.agentik.messageStore.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.storage.MessageRecord.UserMessage).content.let {
(it[0] as pw.binom.agentik.storage.Content.Text).body
assertEquals("hi", (msgs[0] as pw.binom.agentik.messageStore.MessageRecord.UserMessage).content.let {
(it[0] as pw.binom.agentik.messageStore.Content.Text).body
})
assertEquals("hello back", (msgs[1] as pw.binom.agentik.storage.MessageRecord.AssistantMessage).content.let {
(it[0] as pw.binom.agentik.storage.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
})
}
@@ -327,8 +327,8 @@ class ChatAgentTest {
val msgs = storage.messageStore.listAll(conv.id)
assertEquals(2, msgs.size)
assertIs<pw.binom.agentik.storage.MessageRecord.UserMessage>(msgs[0])
val err = assertIs<pw.binom.agentik.storage.MessageRecord.Error>(msgs[1])
assertIs<pw.binom.agentik.messageStore.MessageRecord.UserMessage>(msgs[0])
val err = assertIs<pw.binom.agentik.messageStore.MessageRecord.Error>(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<pw.binom.agentik.storage.MessageRecord.UserMessage>(msgs[0])
assertIs<pw.binom.agentik.messageStore.MessageRecord.UserMessage>(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<pw.binom.agentik.storage.MessageRecord.ToolResult>().firstOrNull()
val toolResult = msgs.filterIsInstance<pw.binom.agentik.messageStore.MessageRecord.ToolResult>().firstOrNull()
assertNotNull(toolResult, "tool result должен быть в audit — tool выполнился нормально")
val toolResultResult = toolResult!!.result!!
assertTrue(toolResultResult.contains("echo"), "tool result содержит реальный ответ тулы: $toolResultResult")
@@ -39,7 +39,7 @@ class ChatAgentToolsetsTest {
private fun newAgent(
toolsets: List<ToolsetContribution> = emptyList(),
): Pair<ChatAgent, pw.binom.agentik.storage.StorageBundle> {
): Pair<ChatAgent, pw.binom.agentik.storageBundle.StorageBundle> {
val storage = SqliteStores.inMemory().asBundle()
val agent = ChatAgent(
id = "test-agent",
@@ -40,7 +40,7 @@ import pw.binom.agentik.llm.tools.SummaryTurn
*/
class CompactionTest {
private lateinit var storage: pw.binom.agentik.storage.StorageBundle
private lateinit var storage: pw.binom.agentik.storageBundle.StorageBundle
private lateinit var fakeLlm: FakeLiteLlm
@BeforeTest
@@ -89,7 +89,7 @@ class CompactionTest {
}
val wm = storage.workingMemoryStore.list(conv.id)
// Без compaction все ходы остаются в памяти (System + 10 user/assistant = 21 строк).
val summaries = wm.filter { it.entry is pw.binom.agentik.storage.WorkingMemoryEntry.Summary }
val summaries = wm.filter { it.entry is pw.binom.agentik.workingMemory.WorkingMemoryEntry.Summary }
assertEquals(0, summaries.size, "compaction must not run without contextWindow")
}
@@ -101,7 +101,7 @@ class CompactionTest {
conv.send(listOf(ProtoContent.Text("first")))
val wm = storage.workingMemoryStore.list(conv.id)
// System + User + Assistant = 3. Без compactor — никаких Summary.
val summaries = wm.filter { it.entry is pw.binom.agentik.storage.WorkingMemoryEntry.Summary }
val summaries = wm.filter { it.entry is pw.binom.agentik.workingMemory.WorkingMemoryEntry.Summary }
assertEquals(0, summaries.size, "no compaction runs without compactor")
}
@@ -119,10 +119,10 @@ class CompactionTest {
assertTrue(compactor.calls > 0, "compactor must be called at least once when above threshold")
// В working memory должна появиться Summary.
val wm = storage.workingMemoryStore.list(conv.id)
val summaries = wm.filter { it.entry is pw.binom.agentik.storage.WorkingMemoryEntry.Summary }
val summaries = wm.filter { it.entry is pw.binom.agentik.workingMemory.WorkingMemoryEntry.Summary }
assertTrue(summaries.isNotEmpty(), "at least one Summary entry should be present after compaction")
// Summary-текст — то, что вернул наш compactor.
val summaryText = (summaries.first().entry as pw.binom.agentik.storage.WorkingMemoryEntry.Summary).text
val summaryText = (summaries.first().entry as pw.binom.agentik.workingMemory.WorkingMemoryEntry.Summary).text
assertTrue(summaryText.startsWith("**Goal**"), "summary text should come from compactor: $summaryText")
}
@@ -162,8 +162,8 @@ class CompactionTest {
// Должны быть: System + хотя бы один Summary + последние KEEP_RECENT_TURNS ходов.
// KEEP_RECENT_TURNS = 4 → user/assistant последних двух ходов (third + second) могут быть не тронуты.
val userAssistantCount = wm.count {
it.entry is pw.binom.agentik.storage.WorkingMemoryEntry.User ||
it.entry is pw.binom.agentik.storage.WorkingMemoryEntry.Assistant
it.entry is pw.binom.agentik.workingMemory.WorkingMemoryEntry.User ||
it.entry is pw.binom.agentik.workingMemory.WorkingMemoryEntry.Assistant
}
// Минимум 1 ход остаётся (KEEP_RECENT_TURNS).
assertTrue(userAssistantCount >= 1, "at least one recent turn must be preserved")
@@ -1,9 +1,9 @@
package pw.binom.agentik.standalone.agent
import pw.binom.agentik.storage.MessageContext
import pw.binom.agentik.storage.MessageOrigin.EVENT
import pw.binom.agentik.storage.MessageOrigin.SYSTEM
import pw.binom.agentik.storage.MessageOrigin.USER
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.litert.LiteContentPart
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -49,7 +49,7 @@ import kotlin.time.Instant
*/
class MemoryWiringTest {
private lateinit var storage: pw.binom.agentik.storage.StorageBundle
private lateinit var storage: pw.binom.agentik.storageBundle.StorageBundle
private lateinit var fakeLlm: FakeLiteLlm
private lateinit var root: Path
@@ -1,4 +1,10 @@
package pw.binom.agentik.storage
package pw.binom.agentik.standalone.persistence
import pw.binom.agentik.messageStore.MessageContext
import pw.binom.agentik.messageStore.MessageOrigin
import pw.binom.agentik.messageStore.ConversationRecord
import pw.binom.agentik.messageStore.MessageRecord
import pw.binom.agentik.messageStore.Content
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
import kotlinx.coroutines.test.runTest
import pw.binom.agentik.storage.sqlite.SqliteStores
@@ -1,4 +1,9 @@
package pw.binom.agentik.storage
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.workingMemory.WorkingMemoryEntry
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -1,4 +1,14 @@
package pw.binom.agentik.storage
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.messageStore.ConversationRecord
import pw.binom.agentik.messageStore.MessageRecord
import pw.binom.agentik.messageStore.Content
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
import kotlin.test.Test
import kotlin.test.assertEquals
+24
View File
@@ -0,0 +1,24 @@
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
// Агрегатор всех 5 storage-интерфейсов (MessageStore + ReflectionStore +
// EventStore + ConversationStore + WorkingMemoryStore). Зависит от обоих API
// модулей. Нужен только server-side рантайму (`:standalone`, `:agentik-cli`,
// `:agent-toolsets`, будущий `:android-agent` core). Тонкие клиенты этот модуль
// НЕ подтягивают — они работают с одним только `:message-store-api`.
kotlin {
jvmToolchain(21)
jvm()
linuxX64()
mingwX64()
sourceSets {
commonMain.dependencies {
api(project(":message-store-api"))
api(project(":working-memory-api"))
}
}
}
@@ -0,0 +1,42 @@
package pw.binom.agentik.storageBundle
import pw.binom.agentik.messageStore.ConversationStore
import pw.binom.agentik.messageStore.MessageStore
import pw.binom.agentik.messageStore.ReflectionStore
import pw.binom.agentik.messageStore.events.EventStore
import pw.binom.agentik.workingMemory.WorkingMemoryStore
/**
* Агрегатор всех storage-интерфейсов для полного агент-рантайма.
*
* Зависит от **обоих** API модулей — `:message-store-api` и `:working-memory-api`.
* Тонкие клиенты, которым нужен только audit log, могут не подтягивать этот модуль
* и работать с [MessageStore] / [ReflectionStore] / [EventStore] / [ConversationStore]
* напрямую.
*
* **Реализации агрегата**:
* - [SqliteStorageBundle] — JVM-only, SQLDelight + SQLite. В `:storage-sqlite`.
* - [InMemoryStorageBundle] — KMP, Map-based. В `:storage-inmemory`.
*
* `SkillStore` НЕ входит — он в `:skills` (контент-файлы навыков), принимается
* отдельно в `ChatAgent`.
*
* AutoCloseable: один `close()` закрывает все store'ы; в реализациях без
* ресурсов (in-memory) — no-op.
*/
data class StorageBundle(
val conversationStore: ConversationStore,
val messageStore: MessageStore,
val workingMemoryStore: WorkingMemoryStore,
val reflectionStore: ReflectionStore,
val eventStore: EventStore? = null,
) : AutoCloseable {
override fun close() {
conversationStore.close()
messageStore.close()
workingMemoryStore.close()
reflectionStore.close()
eventStore?.close()
}
}
-77
View File
@@ -1,77 +0,0 @@
# `:storage-core` — контракт хранилища (KMP, jvm + native)
## Что это
Интерфейсы persistence-уровня для `:standalone`:
- `MessageStore` — append-only история сообщений по диалогу.
- `WorkingMemoryStore` — rolling buffer текущего хода (`AssistantMessage`,
`ToolExchange`, `UserMessage`, system-prompt) для fast-recovery
при reconnect/relance.
- `ConversationStore` — метаданные диалогов (id, title, model,
timestamps).
- `ReflectionStore` — LLM-reflections (свободная форма заметок
хранителя).
Решает: позволяет запустить агента на Android-in-memory, на
desktop-SQLite, или на production-SQLite, не переписывая логику.
Контракт минимален и async-friendly.
## Где используется
- `:storage-inmemory` — для тестов и Android.
- `:storage-sqlite` — прод (Desktop / server / однодесктопный
Android-development).
## Как подключить
```kotlin
kotlin {
sourceSets.commonMain.dependencies {
api("pw.binom.agentik:storage-core:0.1.0")
}
}
```
## Версии
`gradle/libs.versions.toml` → `[versions] agentik-storage-core`.
## Что в API
```kotlin
interface MessageStore {
suspend fun append(conversationId: String, message: Message): Unit
suspend fun after(conversationId: String, instant: Instant, limit: Int = 100): List<Message>
}
interface WorkingMemoryStore {
suspend fun save(conv: String, entry: WorkingMemoryEntry): Unit
fun load(conv: String): Flow<WorkingMemoryEntry> // cold flow
suspend fun clear(conv: String): Unit
}
sealed interface WorkingMemoryEntry {
val id: String
val date: Instant
class UserMessage(...) : WorkingMemoryEntry
class AssistantMessage(...) : WorkingMemoryEntry
class ToolExchange(val toolName: String, val toolArgsJson: String, val resultText: String, val wasCancelled: Boolean) : WorkingMemoryEntry
class SystemPrompt(...) : WorkingMemoryEntry
}
```
## Тесты
Контрактные тесты (общие для всех имплементаций) — в `:storage-sqlite`
и `:storage-inmemory`.
## Чего здесь НЕТ
- Никакого HTTP / SSE.
- Никакой конкретной БД. Backend'ы в `:storage-*`.
## Текущий статус
Используется продакшеном. Контракт зафиксирован после
interrupt-имплементации (см. [INTERRUPT-DESIGN.md](../../docs/INTERRUPT-DESIGN.md)).
@@ -1,36 +0,0 @@
package pw.binom.agentik.storage
import pw.binom.agentik.storage.events.EventStore
/**
* Агрегатор всех storage-интерфейсов, нужных агенту для работы с историей диалога.
*
* Реализации:
* - [SqliteStorageBundle] — JVM-only, основная. SQLDelight + SQLite.
* - [InMemoryStorageBundle] — Map-based, для тестов.
* - `:storage-android` — Android-специфичная (deferred, Android-сборка в планах).
*
* `SkillStore` НЕ входит сюда — он живёт в модуле `:skills` (другая ответственность:
* не сообщения/рефлексии, а контент-файлы навыков) и принимается отдельно в `ChatAgent`.
*
* [EventStore] входит начиная с commit "event-store" — для replay после
* disconnect (см. `/events/replay` endpoint в `:server`).
*
* AutoCloseable: один `close()` закрывает все store'ы. В реализациях,
* которые не владеют ресурсами (in-memory), close — no-op.
*/
data class StorageBundle(
val conversationStore: ConversationStore,
val messageStore: MessageStore,
val workingMemoryStore: WorkingMemoryStore,
val reflectionStore: ReflectionStore,
val eventStore: EventStore? = null,
) : AutoCloseable {
override fun close() {
conversationStore.close()
messageStore.close()
workingMemoryStore.close()
reflectionStore.close()
eventStore?.close()
}
}
@@ -1,81 +0,0 @@
package pw.binom.agentik.storage
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
/**
* Тесты payload-формата: контекст инициации хода персистится через
* `payload_json` SQLite, старый plain-array формат читается без потерь.
*/
class PayloadTest {
@Test
fun `user context roundtrips through MessageBodyPayload`() {
val ctx = MessageContext(
origin = MessageOrigin.USER,
description = "irc PRIVMSG",
sourceId = "irc:agentik",
)
val encoded = encodeBodyPayload(listOf(Content.Text("hi")), ctx)
// новый формат: wrapper-объект с полем context
assertTrue(encoded.startsWith("{"), "expected wrapped object, got: $encoded")
assertTrue(encoded.contains("\"context\""), "expected context field, got: $encoded")
val decoded = decodeBodyPayload(encoded)
assertEquals(listOf(Content.Text("hi")), decoded.content)
assertEquals(ctx, decoded.context)
}
@Test
fun `event context roundtrips with metadata`() {
val ctx = MessageContext(
origin = MessageOrigin.EVENT,
description = "scheduled cron morning-briefing",
sourceId = "cron-42",
metadata = buildJsonObject {
put("scheduledAt", JsonPrimitive("2026-09-14T08:00:00Z"))
put("rule", JsonPrimitive("0 8 * * *"))
},
)
val encoded = encodeBodyPayload(listOf(Content.Text("wake up")), ctx)
val decoded = decodeBodyPayload(encoded)
assertEquals(MessageOrigin.EVENT, decoded.context?.origin)
assertEquals("scheduled cron morning-briefing", decoded.context?.description)
assertEquals("cron-42", decoded.context?.sourceId)
assertEquals("0 8 * * *", (decoded.context?.metadata?.let { (it as kotlinx.serialization.json.JsonObject)["rule"] } as? JsonPrimitive)?.content)
}
@Test
fun `null context produces wrapper without context field`() {
val encoded = encodeBodyPayload(listOf(Content.Text("hello")))
assertTrue(encoded.contains("\"content\""), "expected content field, got: $encoded")
val decoded = decodeBodyPayload(encoded)
assertEquals(listOf(Content.Text("hello")), decoded.content)
assertNull(decoded.context)
}
@Test
fun `legacy plain-array payload still decodes backward compat`() {
val legacy = "[" +
"""{"type":"text","body":"old message"}""" +
"]"
val decoded = decodeBodyPayload(legacy)
assertEquals(listOf(Content.Text("old message")), decoded.content)
assertNull(decoded.context)
}
@Test
fun `system context with description only roundtrips`() {
val ctx = MessageContext(origin = MessageOrigin.SYSTEM, description = "agent startup greeting")
val encoded = encodeBodyPayload(listOf(Content.Text("boot")), ctx)
val decoded = decodeBodyPayload(encoded)
assertEquals(MessageOrigin.SYSTEM, decoded.context?.origin)
assertEquals("agent startup greeting", decoded.context?.description)
assertNull(decoded.context?.sourceId)
assertNull(decoded.context?.metadata)
}
}
+3 -1
View File
@@ -20,7 +20,9 @@ kotlin {
sourceSets {
commonMain.dependencies {
api(project(":storage-core"))
api(project(":message-store-api"))
api(project(":storage-bundle"))
api(project(":working-memory-api"))
}
commonTest.dependencies {
implementation(kotlin("test"))
@@ -3,8 +3,8 @@ package pw.binom.agentik.storage.inmemory
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.time.Clock
import pw.binom.agentik.storage.ConversationRecord
import pw.binom.agentik.storage.ConversationStore
import pw.binom.agentik.messageStore.ConversationRecord
import pw.binom.agentik.messageStore.ConversationStore
import kotlin.time.Instant
/**
@@ -1,8 +1,8 @@
package pw.binom.agentik.storage.inmemory
import pw.binom.agentik.storage.events.EventRecord
import pw.binom.agentik.storage.events.EventStore
import pw.binom.agentik.storage.events.EventType
import pw.binom.agentik.messageStore.events.EventRecord
import pw.binom.agentik.messageStore.events.EventStore
import pw.binom.agentik.messageStore.events.EventType
import kotlin.time.Instant
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -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.storage.MessageEvent
import pw.binom.agentik.storage.MessageRecord
import pw.binom.agentik.storage.MessageStore
import pw.binom.agentik.storage.TokenStats
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 kotlin.time.Instant
/**
@@ -5,9 +5,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import pw.binom.agentik.storage.Reflection
import pw.binom.agentik.storage.ReflectionEvent
import pw.binom.agentik.storage.ReflectionStore
import pw.binom.agentik.messageStore.Reflection
import pw.binom.agentik.messageStore.ReflectionEvent
import pw.binom.agentik.messageStore.ReflectionStore
import kotlin.time.Instant
/**
@@ -1,6 +1,6 @@
package pw.binom.agentik.storage.inmemory
import pw.binom.agentik.storage.StorageBundle
import pw.binom.agentik.storageBundle.StorageBundle
import kotlin.time.Clock
/**
@@ -2,10 +2,10 @@ package pw.binom.agentik.storage.inmemory
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import pw.binom.agentik.storage.Ids
import pw.binom.agentik.storage.WorkingMemoryEntry
import pw.binom.agentik.storage.WorkingMemoryRow
import pw.binom.agentik.storage.WorkingMemoryStore
import pw.binom.agentik.messageStore.Ids
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
import pw.binom.agentik.workingMemory.WorkingMemoryRow
import pw.binom.agentik.workingMemory.WorkingMemoryStore
import kotlin.time.Instant
/**
@@ -1,6 +1,6 @@
package pw.binom.agentik.storage.inmemory
import pw.binom.agentik.storage.ConversationRecord
import pw.binom.agentik.messageStore.ConversationRecord
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@@ -2,8 +2,8 @@ package pw.binom.agentik.storage.inmemory
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.test.runTest
import pw.binom.agentik.storage.events.EventRecord
import pw.binom.agentik.storage.events.EventType
import pw.binom.agentik.messageStore.events.EventRecord
import pw.binom.agentik.messageStore.events.EventType
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
@@ -1,8 +1,8 @@
package pw.binom.agentik.storage.inmemory
import pw.binom.agentik.storage.Content
import pw.binom.agentik.storage.MessageRecord
import pw.binom.agentik.storage.TurnTokens
import pw.binom.agentik.messageStore.Content
import pw.binom.agentik.messageStore.MessageRecord
import pw.binom.agentik.messageStore.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.storage.MessageEvent.Appended)
val appended = ev as pw.binom.agentik.storage.MessageEvent.Appended
assertTrue(ev is pw.binom.agentik.messageStore.MessageEvent.Appended)
val appended = ev as pw.binom.agentik.messageStore.MessageEvent.Appended
assertEquals("c1", appended.conversationId)
assertEquals("u1", appended.record.id)
}
@@ -1,7 +1,7 @@
package pw.binom.agentik.storage.inmemory
import pw.binom.agentik.storage.Reflection
import pw.binom.agentik.storage.ReflectionEvent
import pw.binom.agentik.messageStore.Reflection
import pw.binom.agentik.messageStore.ReflectionEvent
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@@ -1,7 +1,7 @@
package pw.binom.agentik.storage.inmemory
import pw.binom.agentik.storage.Content
import pw.binom.agentik.storage.WorkingMemoryEntry
import pw.binom.agentik.messageStore.Content
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
+3 -1
View File
@@ -23,7 +23,9 @@ kotlin {
sourceSets {
commonMain.dependencies {
api(project(":storage-core"))
api(project(":message-store-api"))
api(project(":storage-bundle"))
api(project(":working-memory-api"))
// ksqlite ещё не опубликован в Maven Central — только в локальном
// caffeine-репо. Версия пока 0.1.0-SNAPSHOT (CI fallback из README).
@@ -1,8 +1,8 @@
package pw.binom.agentik.storage.ksqlite
import kotlin.time.Instant
import pw.binom.agentik.storage.ConversationRecord
import pw.binom.agentik.storage.ConversationStore
import pw.binom.agentik.messageStore.ConversationRecord
import pw.binom.agentik.messageStore.ConversationStore
import pw.binom.db.ksqlite.SQLiteConnection
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
@@ -1,8 +1,8 @@
package pw.binom.agentik.storage.ksqlite
import pw.binom.agentik.storage.events.EventRecord
import pw.binom.agentik.storage.events.EventStore
import pw.binom.agentik.storage.events.EventType
import pw.binom.agentik.messageStore.events.EventRecord
import pw.binom.agentik.messageStore.events.EventStore
import pw.binom.agentik.messageStore.events.EventType
import pw.binom.db.ksqlite.SQLiteConnection
import pw.binom.db.ksqlite.SQLitePreparedStatement
import pw.binom.db.ksqlite.SQLiteResultSet
@@ -1,11 +1,11 @@
package pw.binom.agentik.storage.ksqlite
import kotlinx.serialization.json.Json
import pw.binom.agentik.storage.MessageRecord
import pw.binom.agentik.storage.MessageStore
import pw.binom.agentik.storage.TokenStats
import pw.binom.agentik.storage.decodeBodyPayload
import pw.binom.agentik.storage.encodeBodyPayload
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.db.ksqlite.SQLiteConnection
import pw.binom.db.ksqlite.SQLiteResultSet
import kotlin.time.Instant
@@ -1,8 +1,8 @@
package pw.binom.agentik.storage.ksqlite
import pw.binom.agentik.storage.Reflection
import pw.binom.agentik.storage.ReflectionEvent
import pw.binom.agentik.storage.ReflectionStore
import pw.binom.agentik.messageStore.Reflection
import pw.binom.agentik.messageStore.ReflectionEvent
import pw.binom.agentik.messageStore.ReflectionStore
import pw.binom.db.ksqlite.SQLiteConnection
import pw.binom.db.ksqlite.SQLiteResultSet
import kotlin.time.Clock
@@ -1,11 +1,11 @@
package pw.binom.agentik.storage.ksqlite
import pw.binom.agentik.storage.ConversationStore
import pw.binom.agentik.storage.MessageStore
import pw.binom.agentik.storage.ReflectionStore
import pw.binom.agentik.storage.StorageBundle
import pw.binom.agentik.storage.WorkingMemoryStore
import pw.binom.agentik.storage.events.EventStore
import pw.binom.agentik.messageStore.ConversationStore
import pw.binom.agentik.messageStore.MessageStore
import pw.binom.agentik.messageStore.ReflectionStore
import pw.binom.agentik.storageBundle.StorageBundle
import pw.binom.agentik.workingMemory.WorkingMemoryStore
import pw.binom.agentik.messageStore.events.EventStore
import pw.binom.db.ksqlite.SQLiteConnection
/**
@@ -1,10 +1,10 @@
package pw.binom.agentik.storage.ksqlite
import kotlinx.serialization.json.Json
import pw.binom.agentik.storage.WorkingMemoryEntry
import pw.binom.agentik.storage.WorkingMemoryRow
import pw.binom.agentik.storage.WorkingMemoryStore
import pw.binom.agentik.storage.Ids
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
import pw.binom.agentik.workingMemory.WorkingMemoryRow
import pw.binom.agentik.workingMemory.WorkingMemoryStore
import pw.binom.agentik.messageStore.Ids
import pw.binom.db.ksqlite.SQLiteConnection
import kotlin.time.Instant
import kotlinx.coroutines.Dispatchers
@@ -1,9 +1,9 @@
package pw.binom.agentik.storage.ksqlite
import kotlinx.serialization.json.Json
import pw.binom.agentik.storage.MessageRecord
import pw.binom.agentik.storage.decodeBodyPayload
import pw.binom.agentik.storage.encodeBodyPayload
import pw.binom.agentik.messageStore.MessageRecord
import pw.binom.agentik.messageStore.decodeBodyPayload
import pw.binom.agentik.messageStore.encodeBodyPayload
import pw.binom.db.ksqlite.SQLiteResultSet
import kotlin.time.Instant
@@ -1,7 +1,7 @@
package pw.binom.agentik.storage.ksqlite
import kotlinx.coroutines.test.runTest
import pw.binom.agentik.storage.ConversationRecord
import pw.binom.agentik.messageStore.ConversationRecord
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
@@ -1,8 +1,8 @@
package pw.binom.agentik.storage.ksqlite
import kotlinx.coroutines.test.runTest
import pw.binom.agentik.storage.events.EventRecord
import pw.binom.agentik.storage.events.EventType
import pw.binom.agentik.messageStore.events.EventRecord
import pw.binom.agentik.messageStore.events.EventType
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
@@ -1,9 +1,9 @@
package pw.binom.agentik.storage.ksqlite
import kotlinx.coroutines.test.runTest
import pw.binom.agentik.storage.Content
import pw.binom.agentik.storage.MessageRecord
import pw.binom.agentik.storage.TurnTokens
import pw.binom.agentik.messageStore.Content
import pw.binom.agentik.messageStore.MessageRecord
import pw.binom.agentik.messageStore.TurnTokens
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
@@ -20,7 +20,7 @@ class KsqliteMessageStoreTest {
stores = KsqliteStores.inMemory("msg-${kotlin.random.Random.nextLong()}")
kotlinx.coroutines.runBlocking {
stores.conversations.upsert(
pw.binom.agentik.storage.ConversationRecord(
pw.binom.agentik.messageStore.ConversationRecord(
"conv1", null, false,
Instant.parse("2026-09-15T10:00:00Z"),
Instant.parse("2026-09-15T10:00:00Z"),
@@ -1,7 +1,7 @@
package pw.binom.agentik.storage.ksqlite
import kotlinx.coroutines.test.runTest
import pw.binom.agentik.storage.Reflection
import pw.binom.agentik.messageStore.Reflection
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
@@ -1,8 +1,8 @@
package pw.binom.agentik.storage.ksqlite
import kotlinx.coroutines.test.runTest
import pw.binom.agentik.storage.Content
import pw.binom.agentik.storage.WorkingMemoryEntry
import pw.binom.agentik.messageStore.Content
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
+3 -1
View File
@@ -15,7 +15,9 @@ kotlin {
sourceSets {
commonMain.dependencies {
api(project(":storage-core"))
api(project(":message-store-api"))
api(project(":storage-bundle"))
api(project(":working-memory-api"))
api(libs.sqldelight.runtime)
api(libs.sqldelight.coroutines)
}
@@ -1,8 +1,8 @@
package pw.binom.agentik.storage.sqlite
import kotlin.time.Instant
import pw.binom.agentik.storage.ConversationRecord
import pw.binom.agentik.storage.ConversationStore
import pw.binom.agentik.messageStore.ConversationRecord
import pw.binom.agentik.messageStore.ConversationStore
/**
* SQLite-реализация [ConversationStore].
@@ -2,9 +2,9 @@ package pw.binom.agentik.storage.sqlite
import kotlin.time.Instant
import mu.KotlinLogging
import pw.binom.agentik.storage.events.EventRecord
import pw.binom.agentik.storage.events.EventStore
import pw.binom.agentik.storage.events.EventType
import pw.binom.agentik.messageStore.events.EventRecord
import pw.binom.agentik.messageStore.events.EventStore
import pw.binom.agentik.messageStore.events.EventType
import pw.binom.agentik.storage.sqlite.Agent_event as DbAgentEvent
private val log = KotlinLogging.logger {}
@@ -1,11 +1,11 @@
package pw.binom.agentik.storage.sqlite
import kotlinx.serialization.json.Json
import pw.binom.agentik.storage.MessageRecord
import pw.binom.agentik.storage.MessageStore
import pw.binom.agentik.storage.TokenStats
import pw.binom.agentik.storage.decodeBodyPayload
import pw.binom.agentik.storage.encodeBodyPayload
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 kotlin.time.Instant
/**
@@ -6,9 +6,9 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import mu.KotlinLogging
import pw.binom.agentik.storage.Reflection
import pw.binom.agentik.storage.ReflectionEvent
import pw.binom.agentik.storage.ReflectionStore
import pw.binom.agentik.messageStore.Reflection
import pw.binom.agentik.messageStore.ReflectionEvent
import pw.binom.agentik.messageStore.ReflectionStore
import pw.binom.agentik.storage.sqlite.AgentikDatabase
import pw.binom.agentik.storage.sqlite.Reflection as DbReflection
import pw.binom.agentik.storage.sqlite.ReflectionQueries
@@ -3,13 +3,13 @@ package pw.binom.agentik.storage.sqlite
import app.cash.sqldelight.db.QueryResult
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import pw.binom.agentik.storage.ConversationStore
import pw.binom.agentik.storage.MessageStore
import pw.binom.agentik.storage.ReflectionStore
import pw.binom.agentik.storage.StorageBundle
import pw.binom.agentik.storage.events.EventStore
import pw.binom.agentik.messageStore.ConversationStore
import pw.binom.agentik.messageStore.MessageStore
import pw.binom.agentik.messageStore.ReflectionStore
import pw.binom.agentik.storageBundle.StorageBundle
import pw.binom.agentik.messageStore.events.EventStore
import pw.binom.agentik.storage.sqlite.SqliteReflectionStore
import pw.binom.agentik.storage.WorkingMemoryStore
import pw.binom.agentik.workingMemory.WorkingMemoryStore
/**
* Корневой объект SQLite-слоя: держит [SqlDriver] и пять store'ов (включая
@@ -1,9 +1,9 @@
package pw.binom.agentik.storage.sqlite
import kotlinx.serialization.json.Json
import pw.binom.agentik.storage.WorkingMemoryEntry
import pw.binom.agentik.storage.WorkingMemoryRow
import pw.binom.agentik.storage.WorkingMemoryStore
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
import pw.binom.agentik.workingMemory.WorkingMemoryRow
import pw.binom.agentik.workingMemory.WorkingMemoryStore
import kotlin.time.Instant
/**
@@ -90,4 +90,4 @@ private fun Working_memory.toRow(): WorkingMemoryRow {
)
}
private fun newId(): String = pw.binom.agentik.storage.Ids.new("wm")
private fun newId(): String = pw.binom.agentik.messageStore.Ids.new("wm")
@@ -5,8 +5,8 @@ import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertNotNull
import kotlin.time.Instant
import pw.binom.agentik.storage.Reflection
import pw.binom.agentik.storage.ReflectionEvent
import pw.binom.agentik.messageStore.Reflection
import pw.binom.agentik.messageStore.ReflectionEvent
class ReflectionStoreTest {
@@ -1,8 +1,8 @@
package pw.binom.agentik.storage.sqlite
import kotlinx.coroutines.test.runTest
import pw.binom.agentik.storage.events.EventRecord
import pw.binom.agentik.storage.events.EventType
import pw.binom.agentik.messageStore.events.EventRecord
import pw.binom.agentik.messageStore.events.EventType
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
@@ -3,24 +3,22 @@ plugins {
alias(libs.plugins.kotlin.serialization)
}
// Public API для mutable working-memory — runtime context агента (compaction,
// order_idx, summary entries). Зависит от :message-store-api для Ids (wm- префикс).
//
// НЕ нужен тонким клиентам — только серверному рантайму (`:standalone`, `:agentik-cli`,
// будущий `:android-agent` core).
kotlin {
jvmToolchain(21)
// Зеркалит набор :proto / :server / :memory-api — KMP-модуль с интерфейсами
// хранилища и разговорной истории, без платформенного IO. Конкретные
// реализации (sqlite, in-memory, android) живут в отдельных модулях.
jvm()
macosX64()
macosArm64()
iosX64()
iosArm64()
iosSimulatorArm64()
linuxX64()
linuxArm64()
mingwX64()
sourceSets {
commonMain.dependencies {
api(project(":message-store-api"))
api(libs.kotlinx.coroutines.core)
api(libs.kotlinx.serialization.core)
api(libs.kotlinx.serialization.json)
@@ -1,7 +1,9 @@
package pw.binom.agentik.storage
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
/**
* Запись в working memory диалога: ровно то, что агент сейчас видит в
@@ -1,4 +1,4 @@
package pw.binom.agentik.storage
package pw.binom.agentik.workingMemory
import kotlin.time.Instant