From 9ee942428d949c2e59ca779a0462607581979798 Mon Sep 17 00:00:00 2001 From: subochev Date: Tue, 15 Sep 2026 15:25:36 +0300 Subject: [PATCH] =?UTF-8?q?standalone:=20ChatAgent=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B8=D0=BC=D0=B0=D0=B5=D1=82=20StorageBundle=20=D0=B2?= =?UTF-8?q?=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20SqliteStores?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Финальный swap — ChatAgent/ChatConversation теперь работают через абстрактный StorageBundle (pw.binom.agentik.storage), а не через конкретный SqliteStores. Подготовка к Android-портированию (там будет :storage-android вместо :storage-sqlite). Изменения: - SqliteStores.asBundle() — convenience для превращения конкретного SQLite-импла в StorageBundle - ChatAgent(private val storage: StorageBundle) — было stores: SqliteStores - ChatConversation(private val storage: StorageBundle) — то же - Main.kt, DebugRoutes.kt — вызовы обновлены, используется .asBundle() - Все 5 тестовых файлов с ChatAgent(... stores = ...) — обновлены на ChatAgent(... storage = ...asBundle()) - StorageBundle : AutoCloseable — закрывает все 4 store'а; в тестах tearDown { storage.close() } Конфиг не менялся: toolsets остаётся emptyList() по умолчанию (полная невидимость механики тулсетов для модели). Подключение тулсетов — opt-in через параметр ChatAgent(toolsets = ...) для будущего e2e-теста в post-implementation. Tests: 340/340 green. Fatjar 240 MB. Без регрессий. --- .../binom/agentik/standalone/DebugRoutes.kt | 16 ++--- .../pw/binom/agentik/standalone/Main.kt | 14 ++--- .../agentik/standalone/agent/ChatAgent.kt | 22 +++---- .../standalone/agent/ChatConversation.kt | 9 ++- .../agentik/standalone/agent/ChatAgentTest.kt | 58 +++++++++---------- .../standalone/agent/ChatAgentToolsetsTest.kt | 28 ++++----- .../standalone/agent/CompactionTest.kt | 16 ++--- .../standalone/agent/MemoryWiringTest.kt | 18 +++--- .../agentik/storage/sqlite/SqliteStores.kt | 13 +++++ 9 files changed, 103 insertions(+), 91 deletions(-) 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 723c2a7..aa380cb 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/DebugRoutes.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/DebugRoutes.kt @@ -14,7 +14,7 @@ import pw.binom.agentik.standalone.agent.ChatConversation import pw.binom.agentik.standalone.agent.LlmReflector import pw.binom.agentik.standalone.agent.SkillMiner import pw.binom.agentik.standalone.agent.memory.Curator -import pw.binom.agentik.storage.sqlite.SqliteStores +import pw.binom.agentik.storage.StorageBundle import pw.binom.agentik.skills.SkillStore /** @@ -33,7 +33,7 @@ import pw.binom.agentik.skills.SkillStore */ internal fun Route.debugRoutes( agent: Agent, - stores: SqliteStores, + storage: pw.binom.agentik.storage.StorageBundle, reflector: LlmReflector?, skillMiner: SkillMiner?, skillStore: SkillStore?, @@ -44,14 +44,14 @@ internal fun Route.debugRoutes( ?: return@post call.respondText("conversationId required", status = HttpStatusCode.BadRequest) val minerReflector = reflector if (minerReflector == null) return@post call.respondText("reflection disabled", status = HttpStatusCode.NotFound) - val turns = recentTurns(stores, convId, maxTurns = 6) + val turns = recentTurns(storage, convId, maxTurns = 6) if (turns.isEmpty()) return@post call.respondText("no turns in conversation", status = HttpStatusCode.NotFound) val reflection = minerReflector.reflect(turns) if (reflection == null) { call.respondText("""{"reflected":false,"reason":"unparseable LLM reply"}""", contentType = ContentType.Application.Json) } else { val stamped = reflection.copy(conversationId = convId) - stores.reflections.insert(stamped) + storage.reflectionStore.insert(stamped) call.respondText( buildJsonObject { put("reflected", true) @@ -71,7 +71,7 @@ internal fun Route.debugRoutes( val miner = skillMiner val store = skillStore if (miner == null || store == null) return@post call.respondText("skill mining disabled", status = HttpStatusCode.NotFound) - val turns = recentTurns(stores, convId, maxTurns = miner.maxTurns) + val turns = recentTurns(storage, convId, maxTurns = miner.maxTurns) if (turns.isEmpty()) return@post call.respondText("no turns in conversation", status = HttpStatusCode.NotFound) val mined = miner.mine(turns, store.catalog.skills) for (s in mined) store.upsert(s) @@ -110,7 +110,7 @@ internal fun Route.debugRoutes( get("/debug/tokens") { val convId = call.parameters["conversationId"] ?: return@get call.respondText("conversationId required", status = HttpStatusCode.BadRequest) - val stats = stores.messages.tokenStats(convId) + val stats = storage.messageStore.tokenStats(convId) val json = buildJsonObject { put("conversationId", convId) put("turns", stats.turns.toString()) @@ -127,8 +127,8 @@ internal fun Route.debugRoutes( * (для debug-триггеров reflector/miner; та же логика, что у хуков * [ChatConversation]). */ -internal suspend fun recentTurns(stores: SqliteStores, conversationId: String, maxTurns: Int): List { - val rows = stores.workingMemory.list(conversationId) +internal suspend fun recentTurns(storage: pw.binom.agentik.storage.StorageBundle, conversationId: String, maxTurns: Int): List { + val rows = storage.workingMemoryStore.list(conversationId) val pairs = mutableListOf() var pendingUser: String? = null for (row in rows) { diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/Main.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/Main.kt index 319c5a2..c0c9eeb 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/Main.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/Main.kt @@ -58,7 +58,7 @@ fun main() { val config = AgentikConfig.fromEnv() val llm = config.llm.createLlm() - val stores = SqliteStores.open(dbPath = config.dbPath) + val storage = SqliteStores.open(dbPath = config.dbPath).asBundle() val mcpRegistry = McpRegistry.fromConfig(config.mcp) // Хранилище скилов: если skillsDir задан, читаем каталог + создаём @@ -179,12 +179,12 @@ fun main() { if (config.reflectionInterval > 0) LlmReflector(llm = llm) else null val recentReflections: List = if (config.reflectionTopK > 0) kotlinx.coroutines.runBlocking { - stores.reflections.listRecent(config.reflectionTopK) + storage.reflectionStore.listRecent(config.reflectionTopK) } else emptyList() val agent = ChatAgent( id = "agentik", - stores = stores, + storage = storage, llm = llm, llmConfig = config.llm, tools = mcpRegistry.namedTools, @@ -222,7 +222,7 @@ fun main() { if (config.debugEndpoints) { debugRoutes( agent = agent, - stores = stores, + storage = storage, reflector = reflector, skillMiner = skillMiner, skillStore = skillStore, @@ -260,14 +260,14 @@ fun main() { // Token stats по существующим диалогам (агрегат на старте — каждая запись // парсится из payload_json, ну >100 turns и БД приличная — но в рамках // стартапа это терпимо). - val existingConvs = kotlinx.coroutines.runBlocking { stores.conversations.list(offset = 0, limit = 1000) } + val existingConvs = kotlinx.coroutines.runBlocking { storage.conversationStore.list(offset = 0, limit = 1000) } if (existingConvs.isNotEmpty()) { var totalTurns = 0 var totalIn = 0L var totalOut = 0L for (c in existingConvs) { if (c.isTemporal) continue - val s = kotlinx.coroutines.runBlocking { stores.messages.tokenStats(c.id) } + val s = kotlinx.coroutines.runBlocking { storage.messageStore.tokenStats(c.id) } totalTurns += s.turns totalIn += s.inputTokens totalOut += s.outputTokens @@ -279,7 +279,7 @@ fun main() { Runtime.getRuntime().addShutdownHook(Thread { agent.close() mcpRegistry.close() - stores.close() + storage.close() llm.close() memorySystem?.close() }) 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 6d405ef..1f5f51d 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 @@ -19,7 +19,7 @@ import pw.binom.agentik.standalone.llm.LlmConfig import pw.binom.agentik.storage.ConversationRecord import pw.binom.agentik.storage.Reflection import pw.binom.agentik.storage.WorkingMemoryEntry -import pw.binom.agentik.storage.sqlite.SqliteStores +import pw.binom.agentik.storage.StorageBundle import pw.binom.agentik.toolsets.EnableToolsetTool import pw.binom.agentik.toolsets.DisableToolsetTool import pw.binom.agentik.toolsets.SystemPromptToolsetSection @@ -51,7 +51,7 @@ import kotlin.time.Instant */ class ChatAgent( override val id: String, - private val stores: SqliteStores, + private val storage: pw.binom.agentik.storage.StorageBundle, private val llm: LiteLlm, private val llmConfig: LlmConfig, private val tools: List = emptyList(), @@ -211,8 +211,8 @@ class ChatAgent( // и не переживают рестарт агента (см. Memory #3709). if (!temp) { runBlocking { - stores.conversations.upsert(rec) - stores.workingMemory.append( + storage.conversationStore.upsert(rec) + storage.workingMemoryStore.append( conversationId = id, entry = WorkingMemoryEntry.System(text = systemPrompt), now = now, @@ -221,7 +221,7 @@ class ChatAgent( } val conv = ChatConversation( record = rec, - stores = stores, + storage = storage, llm = llm, systemPrompt = systemPrompt, tools = allTools, @@ -232,7 +232,7 @@ class ChatAgent( contextWindow = contextWindow, compressionThreshold = compressionThreshold, contextCompactor = contextCompactor, - reflectionStore = stores.reflections, + reflectionStore = storage.reflectionStore, reflector = reflector, reflectionInterval = reflectionInterval, skillMiner = skillMiner, @@ -248,7 +248,7 @@ class ChatAgent( override suspend fun getConversation(id: String): ProtoConversation? { liveLock.withLock { live[id] }?.let { if (!it.isClosed) return it } - val rec = stores.conversations.get(id) ?: return null + val rec = storage.conversationStore.get(id) ?: return null return newConversation(rec).also { liveLock.withLock { live[id] = it } } @@ -257,13 +257,13 @@ class ChatAgent( override suspend fun deleteConversation(id: String): Boolean { val conv = liveLock.withLock { live.remove(id) } conv?.close() - val ok = stores.conversations.delete(id) + val ok = storage.conversationStore.delete(id) if (ok) agentEvents.tryEmit(AgentEvent.Deleted(date = now(), id = id)) return ok } override suspend fun getConversations(offset: Int, limit: Int): List = - stores.conversations.list(offset = offset, limit = limit).map { rec -> + storage.conversationStore.list(offset = offset, limit = limit).map { rec -> liveLock.withLock { live[rec.id] } ?: newConversation(rec).also { liveLock.withLock { live[rec.id] = it } @@ -272,7 +272,7 @@ class ChatAgent( private fun newConversation(rec: ConversationRecord): ChatConversation = ChatConversation( record = rec, - stores = stores, + storage = storage, llm = llm, systemPrompt = systemPrompt, tools = allTools, @@ -283,7 +283,7 @@ class ChatAgent( contextWindow = contextWindow, compressionThreshold = compressionThreshold, contextCompactor = contextCompactor, - reflectionStore = stores.reflections, + reflectionStore = storage.reflectionStore, reflector = reflector, reflectionInterval = reflectionInterval, skillMiner = skillMiner, diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ChatConversation.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ChatConversation.kt index 58dbfc1..816c2cb 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ChatConversation.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ChatConversation.kt @@ -38,7 +38,6 @@ import pw.binom.agentik.storage.MessageStore import pw.binom.agentik.storage.WorkingMemoryEntry import pw.binom.agentik.storage.WorkingMemoryRow import pw.binom.agentik.storage.WorkingMemoryStore -import pw.binom.agentik.storage.sqlite.SqliteStores import pw.binom.agentik.toolsets.ToolsetDispatchPolicy import pw.binom.litert.LiteContentPart import pw.binom.litert.LiteConversation @@ -71,7 +70,7 @@ import kotlin.time.Instant */ class ChatConversation( record: ConversationRecord, - private val stores: SqliteStores, + private val storage: pw.binom.agentik.storage.StorageBundle, private val llm: LiteLlm, private val systemPrompt: String, private val tools: List = emptyList(), @@ -152,9 +151,9 @@ class ChatConversation( override val title: String? get() = record.title override val updatedAt: Instant get() = record.updatedAt - private val conversationStore: ConversationStore get() = stores.conversations - private val messageStore: MessageStore get() = stores.messages - private val workingMemory: WorkingMemoryStore get() = stores.workingMemory + private val conversationStore: ConversationStore get() = storage.conversationStore + private val messageStore: MessageStore get() = storage.messageStore + private val workingMemory: WorkingMemoryStore get() = storage.workingMemoryStore private val toolsByName: Map = tools.associateBy { it.name } 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 0f16ec7..cd31502 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 @@ -38,28 +38,28 @@ import kotlin.time.Instant class ChatAgentTest { - private lateinit var stores: SqliteStores + private lateinit var storage: pw.binom.agentik.storage.StorageBundle private lateinit var fakeLlm: FakeLiteLlm @BeforeTest fun setup() { - stores = SqliteStores.inMemory() + storage = SqliteStores.inMemory().asBundle() fakeLlm = FakeLiteLlm() } @AfterTest fun tearDown() { - stores.close() + storage.close() } private fun newAgent( - stores: SqliteStores = this.stores, + storage: pw.binom.agentik.storage.StorageBundle = this.storage, llm: LiteLlm = this.fakeLlm, tools: List = emptyList(), skills: SkillCatalog = SkillCatalog.EMPTY, ): ChatAgent = ChatAgent( id = "agentik", - stores = stores, + storage = storage, llm = llm, llmConfig = LlmConfig( backend = LlmBackend.OPENAI, @@ -75,7 +75,7 @@ class ChatAgentTest { val agent = newAgent() val conv = agent.createConversation(temp = false) as ChatConversation - val wm = stores.workingMemory.list(conv.id) + val wm = storage.workingMemoryStore.list(conv.id) assertEquals(1, wm.size) val first = wm[0] val sysEntry = first.entry as pw.binom.agentik.storage.WorkingMemoryEntry.System @@ -90,7 +90,7 @@ class ChatAgentTest { val agent = newAgent(skills = skills) val conv = agent.createConversation(temp = false) as ChatConversation - val system = stores.workingMemory.list(conv.id).first().entry + val system = storage.workingMemoryStore.list(conv.id).first().entry as pw.binom.agentik.storage.WorkingMemoryEntry.System assertTrue("be brief" in system.text) assertTrue("## Навыки" in system.text) @@ -147,7 +147,7 @@ class ChatAgentTest { val id = conv.id // добавим сообщение, чтобы потом убедиться, что каскад сработал - stores.messages.append( + storage.messageStore.append( pw.binom.agentik.storage.MessageRecord.UserMessage( id = "m1", conversationId = id, @@ -157,8 +157,8 @@ class ChatAgentTest { ) assertTrue(agent.deleteConversation(id)) assertNull(agent.getConversation(id)) - assertNull(stores.conversations.get(id)) - assertEquals(emptyList(), stores.messages.listAll(id)) + assertNull(storage.conversationStore.get(id)) + assertEquals(emptyList(), storage.messageStore.listAll(id)) } @Test @@ -176,7 +176,7 @@ class ChatAgentTest { conv.send(listOf(Content.Text("hi"))) // user message записан в audit + working memory - val msgs = stores.messages.listAll(conv.id) + 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 @@ -200,10 +200,10 @@ class ChatAgentTest { conv2.send(listOf(Content.Text("second user"))) // Первая беседа должна иметь только свою систему + 1 user + 1 assistant - val wm1 = stores.workingMemory.list(conv1.id) + val wm1 = storage.workingMemoryStore.list(conv1.id) assertEquals(3, wm1.size) // Вторая беседа — только своё - val wm2 = stores.workingMemory.list(conv2.id) + val wm2 = storage.workingMemoryStore.list(conv2.id) assertEquals(3, wm2.size) } @@ -237,12 +237,12 @@ class ChatAgentTest { fakeLlm.reply = "first reply" conv.send(listOf(Content.Text("first user"))) // первый turn: WM = [system, user, assistant] - assertEquals(3, stores.workingMemory.list(conv.id).size) + assertEquals(3, storage.workingMemoryStore.list(conv.id).size) fakeLlm.reply = "second reply" conv.send(listOf(Content.Text("second user"))) // второй turn: WM должен вырасти до [system, user, assistant, user, assistant] - val wm = stores.workingMemory.list(conv.id) + val wm = storage.workingMemoryStore.list(conv.id) assertEquals(5, wm.size) // Длинно-живущий LiteConversation: один на ChatConversation, история // накапливается через sendStreamContents, без пересоздания. @@ -303,7 +303,7 @@ class ChatAgentTest { assertTrue(events.any { it is ProtoEvent.Error && it.message == "boom from llm" }, "events=$events") assertTrue(events.any { it is ProtoEvent.End }, "events=$events") - val msgs = stores.messages.listAll(conv.id) + val msgs = storage.messageStore.listAll(conv.id) assertEquals(2, msgs.size) assertIs(msgs[0]) val err = assertIs(msgs[1]) @@ -336,7 +336,7 @@ class ChatAgentTest { sendJob.join() // user сообщение в audit должно быть, assistant — нет (был отменён) - val msgs = stores.messages.listAll(conv.id) + val msgs = storage.messageStore.listAll(conv.id) assertEquals(1, msgs.size) assertIs(msgs[0]) } @@ -344,12 +344,12 @@ class ChatAgentTest { @Test fun `temp conversation is not persisted across agent instances`() = runTest { // Поднимаем file-backed БД, создаём temp-беседу - stores.close() + storage.close() val dbPath = (System.getProperty("java.io.tmpdir") + "/agentik-test-${System.nanoTime()}.db") - stores = SqliteStores.open(dbPath) + storage = SqliteStores.open(dbPath).asBundle() val agent1 = ChatAgent( id = "agentik", - stores = stores, + storage = storage, llm = FakeLiteLlm().also { fakeLlm = it }, llmConfig = LlmConfig( backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI, @@ -362,11 +362,11 @@ class ChatAgentTest { assertNotNull(agent1.getConversation(tempId)) // Переоткрываем БД — temp-беседа не должна пережить рестарт - stores.close() - stores = SqliteStores.open(dbPath) + storage.close() + storage = SqliteStores.open(dbPath).asBundle() val agent2 = ChatAgent( id = "agentik", - stores = stores, + storage = storage, llm = fakeLlm, llmConfig = LlmConfig( backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI, @@ -380,12 +380,12 @@ class ChatAgentTest { @Test fun `non-temp conversation persists across agent instances`() = runTest { - stores.close() + storage.close() val dbPath = (System.getProperty("java.io.tmpdir") + "/agentik-test-${System.nanoTime()}.db") - stores = SqliteStores.open(dbPath) + storage = SqliteStores.open(dbPath).asBundle() val agent1 = ChatAgent( id = "agentik", - stores = stores, + storage = storage, llm = FakeLiteLlm().also { fakeLlm = it }, llmConfig = LlmConfig( backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI, @@ -396,11 +396,11 @@ class ChatAgentTest { val conv = agent1.createConversation(temp = false) val id = conv.id - stores.close() - stores = SqliteStores.open(dbPath) + storage.close() + storage = SqliteStores.open(dbPath).asBundle() val agent2 = ChatAgent( id = "agentik", - stores = stores, + storage = storage, llm = fakeLlm, llmConfig = LlmConfig( backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI, diff --git a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/ChatAgentToolsetsTest.kt b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/ChatAgentToolsetsTest.kt index 3e9f4b9..4ca8c3c 100644 --- a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/ChatAgentToolsetsTest.kt +++ b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/ChatAgentToolsetsTest.kt @@ -39,11 +39,11 @@ class ChatAgentToolsetsTest { private fun newAgent( toolsets: List = emptyList(), - ): Pair { - val stores = SqliteStores.inMemory() + ): Pair { + val storage = SqliteStores.inMemory().asBundle() val agent = ChatAgent( id = "test-agent", - stores = stores, + storage = storage, llm = stubLlm(), llmConfig = LlmConfig( backend = pw.binom.agentik.standalone.llm.LlmBackend.GOOGLE, @@ -52,12 +52,12 @@ class ChatAgentToolsetsTest { ), toolsets = toolsets, ) - return agent to stores + return agent to storage } @Test fun `default (empty toolsets) does not register enable or disable tools`() { - val (agent, stores) = newAgent() + val (agent, storage) = newAgent() try { // Проверяем через allTools-эквивалент: вызываем enable_toolset // и ожидаем что он не найден — это значит тул не зарегистрирован. @@ -66,7 +66,7 @@ class ChatAgentToolsetsTest { val prompt = (conv as ChatConversation).let { it.systemPromptForTest() } assertFalse(prompt.contains("## Toolsets"), "toolsets section must NOT appear when toolsets empty") assertFalse(prompt.contains("enable_toolset"), "enable_toolset must NOT be mentioned when toolsets empty") - } finally { stores.close() } + } finally { storage.close() } } @Test @@ -83,7 +83,7 @@ class ChatAgentToolsetsTest { tools = listOf(ToolsetContribution.ToolEntry("fetch_url", stubTool("fetch_url"))), ), ) - val (agent, stores) = newAgent(toolsets = toolsets) + val (agent, storage) = newAgent(toolsets = toolsets) try { val conv = agent.createConversation(temp = true) as ChatConversation val prompt = conv.systemPromptForTest() @@ -92,7 +92,7 @@ class ChatAgentToolsetsTest { assertTrue(prompt.contains("- web — fetch and parse web pages")) // Оба тула — в списке allTools (через system prompt проверяем только prompt; // наличие тулов проверим отдельно — см. ниже). - } finally { stores.close() } + } finally { storage.close() } } @Test @@ -105,7 +105,7 @@ class ChatAgentToolsetsTest { tools = listOf(ToolsetContribution.ToolEntry("resize_image", mediaTool)), ), ) - val (agent, stores) = newAgent(toolsets = toolsets) + val (agent, storage) = newAgent(toolsets = toolsets) try { val conv = agent.createConversation(temp = true) as ChatConversation @@ -120,7 +120,7 @@ class ChatAgentToolsetsTest { conv.dispatchToolForTest("resize_image", "{}") } assertEquals("image-resized-100x100", resized) - } finally { stores.close() } + } finally { storage.close() } } @Test @@ -133,7 +133,7 @@ class ChatAgentToolsetsTest { tools = listOf(ToolsetContribution.ToolEntry("resize_image", mediaTool)), ), ) - val (agent, stores) = newAgent(toolsets = toolsets) + val (agent, storage) = newAgent(toolsets = toolsets) try { val conv = agent.createConversation(temp = true) as ChatConversation @@ -142,7 +142,7 @@ class ChatAgentToolsetsTest { conv.dispatchToolForTest("resize_image", "{}") } assertEquals("auto-activated-and-ran", result) - } finally { stores.close() } + } finally { storage.close() } } @Test @@ -158,7 +158,7 @@ class ChatAgentToolsetsTest { tools = listOf(ToolsetContribution.ToolEntry("resize_image", stubTool("resize_image"))), ), ) - val (agent, stores) = newAgent(toolsets = toolsets) + val (agent, storage) = newAgent(toolsets = toolsets) try { val conv = agent.createConversation(temp = true) as ChatConversation @@ -174,7 +174,7 @@ class ChatAgentToolsetsTest { conv.dispatchToolForTest("disable_toolset", """{"name":"media"}""") } assertEquals("Toolset 'media' deactivated.", disabledAgain) - } finally { stores.close() } + } finally { storage.close() } } } diff --git a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/CompactionTest.kt b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/CompactionTest.kt index efd3338..d6d9020 100644 --- a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/CompactionTest.kt +++ b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/CompactionTest.kt @@ -39,18 +39,18 @@ import kotlinx.coroutines.flow.asSharedFlow */ class CompactionTest { - private lateinit var stores: SqliteStores + private lateinit var storage: pw.binom.agentik.storage.StorageBundle private lateinit var fakeLlm: FakeLiteLlm @BeforeTest fun setup() { - stores = SqliteStores.inMemory() + storage = SqliteStores.inMemory().asBundle() fakeLlm = FakeLiteLlm() } @AfterTest fun tearDown() { - stores.close() + storage.close() } private fun newAgent( @@ -62,7 +62,7 @@ class CompactionTest { val reviewer = if (memoryStore != null) KeywordMdReviewer() else null return ChatAgent( id = "test", - stores = stores, + storage = storage, llm = fakeLlm, llmConfig = LlmConfig( backend = LlmBackend.OPENAI, @@ -86,7 +86,7 @@ class CompactionTest { repeat(10) { conv.send(listOf(ProtoContent.Text("turn $it: ${"x".repeat(200)}"))) } - val wm = stores.workingMemory.list(conv.id) + 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 } assertEquals(0, summaries.size, "compaction must not run without contextWindow") @@ -98,7 +98,7 @@ class CompactionTest { val agent = newAgent(contextWindow = 10, compactor = null) val conv = agent.createConversation(temp = false) as ChatConversation conv.send(listOf(ProtoContent.Text("first"))) - val wm = stores.workingMemory.list(conv.id) + 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 } assertEquals(0, summaries.size, "no compaction runs without compactor") @@ -117,7 +117,7 @@ class CompactionTest { // Compactor должен был быть вызван хотя бы раз. assertTrue(compactor.calls > 0, "compactor must be called at least once when above threshold") // В working memory должна появиться Summary. - val wm = stores.workingMemory.list(conv.id) + val wm = storage.workingMemoryStore.list(conv.id) val summaries = wm.filter { it.entry is pw.binom.agentik.storage.WorkingMemoryEntry.Summary } assertTrue(summaries.isNotEmpty(), "at least one Summary entry should be present after compaction") // Summary-текст — то, что вернул наш compactor. @@ -157,7 +157,7 @@ class CompactionTest { conv.send(listOf(ProtoContent.Text("second turn"))) conv.send(listOf(ProtoContent.Text("third turn — long content ${"y".repeat(150)}"))) - val wm = stores.workingMemory.list(conv.id) + val wm = storage.workingMemoryStore.list(conv.id) // Должны быть: System + хотя бы один Summary + последние KEEP_RECENT_TURNS ходов. // KEEP_RECENT_TURNS = 4 → user/assistant последних двух ходов (third + second) могут быть не тронуты. val userAssistantCount = wm.count { diff --git a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/MemoryWiringTest.kt b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/MemoryWiringTest.kt index 44fe56a..e19988c 100644 --- a/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/MemoryWiringTest.kt +++ b/standalone/src/jvmTest/kotlin/pw/binom/agentik/standalone/agent/MemoryWiringTest.kt @@ -47,13 +47,13 @@ import kotlin.time.Instant */ class MemoryWiringTest { - private lateinit var stores: SqliteStores + private lateinit var storage: pw.binom.agentik.storage.StorageBundle private lateinit var fakeLlm: FakeLiteLlm private lateinit var root: Path @BeforeTest fun setup() { - stores = SqliteStores.inMemory() + storage = SqliteStores.inMemory().asBundle() fakeLlm = FakeLiteLlm() root = Path(SystemTemporaryDirectory.toString(), "agentik-mem-${java.util.UUID.randomUUID()}") SystemFileSystem.createDirectories(root, mustCreate = true) @@ -61,7 +61,7 @@ class MemoryWiringTest { @AfterTest fun tearDown() { - stores.close() + storage.close() runCatching { SystemFileSystem.delete(root, mustExist = false) } } @@ -71,7 +71,7 @@ class MemoryWiringTest { reviewer: MemoryReviewer, ): ChatAgent = ChatAgent( id = "agentik", - stores = stores, + storage = storage, llm = fakeLlm, llmConfig = LlmConfig( backend = LlmBackend.OPENAI, @@ -88,7 +88,7 @@ class MemoryWiringTest { val system = openMdMemorySystem(root) val agent = newAgent(system.store, system.prefetcher, system.reviewer) val conv = agent.createConversation(temp = false) - val wm = stores.workingMemory.list(conv.id) + val wm = storage.workingMemoryStore.list(conv.id) val sysRow = wm.first { it.entry is WorkingMemoryEntry.System } val text = (sysRow.entry as WorkingMemoryEntry.System).text assertTrue(text.contains(MemorySystemGuidance.MEMORY_GUIDANCE.take(80)), @@ -102,7 +102,7 @@ class MemoryWiringTest { val soulBody = "I am a helpful test persona. I always answer in one short line." val agent = ChatAgent( id = "agentik", - stores = stores, + storage = storage, llm = fakeLlm, llmConfig = LlmConfig( backend = LlmBackend.OPENAI, @@ -112,7 +112,7 @@ class MemoryWiringTest { soulBody = soulBody, ) val conv = agent.createConversation(temp = false) - val wm = stores.workingMemory.list(conv.id) + val wm = storage.workingMemoryStore.list(conv.id) val sysRow = wm.first { it.entry is WorkingMemoryEntry.System } val text = (sysRow.entry as WorkingMemoryEntry.System).text assertTrue(text.startsWith(soulBody), @@ -126,7 +126,7 @@ class MemoryWiringTest { fun `soul body not added when null`() = runBlocking { val agent = ChatAgent( id = "agentik", - stores = stores, + storage = storage, llm = fakeLlm, llmConfig = LlmConfig( backend = LlmBackend.OPENAI, @@ -135,7 +135,7 @@ class MemoryWiringTest { ), ) val conv = agent.createConversation(temp = false) - val wm = stores.workingMemory.list(conv.id) + val wm = storage.workingMemoryStore.list(conv.id) val sysRow = wm.first { it.entry is WorkingMemoryEntry.System } val text = (sysRow.entry as WorkingMemoryEntry.System).text assertTrue(text.startsWith("be brief"), 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 0b6e36c..a25805f 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 @@ -6,6 +6,7 @@ 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.sqlite.SqliteReflectionStore import pw.binom.agentik.storage.WorkingMemoryStore @@ -21,6 +22,18 @@ class SqliteStores private constructor( val reflections: ReflectionStore, ) : AutoCloseable { + /** + * Удобный агрегатор — превращает SqliteStores в [StorageBundle] для передачи + * в агенты, которые работают через общий контракт хранения (ChatAgent после + * commit 6 в :standalone принимает [StorageBundle], не [SqliteStores]). + */ + fun asBundle(): StorageBundle = StorageBundle( + conversationStore = conversations, + messageStore = messages, + workingMemoryStore = workingMemory, + reflectionStore = reflections, + ) + override fun close() { conversations.close() messages.close()