refactor(storage): drop StorageBundle + legacy EventStore, add HttpEventStore
ci / JVM build + tests (push) Successful in 6m13s

- Remove legacy pw.binom.agentik.messageStore.events.EventStore (EventRecord,
  EventType) and all three impls (in-memory, sqlite, ksqlite) + tests + .sq
- Drop :storage-bundle module entirely; ChatAgent / ChatConversation /
  ConversationLoop / DebugRoutes now take stores individually
  (conversationStore, messageStore, workingMemoryStore, reflectionStore,
  eventStore) instead of StorageBundle
- Delete server endpoints /events/replay and /conversations/{id}/events/replay;
  Route.agentikAgent no longer takes eventStore param
- Add :client/HttpEventStore implementing :event-store/EventStore over HTTP:
  events() -> GET /events/all, agentEvents() -> GET /events,
  conversationEvents(convId) -> GET /conversations/{id}/events;
  exposed via AgentClient.eventStore
- :event-store: add macosX64/macosArm64/linuxArm64 targets to match :client KMP
- :working-memory-api: drop api dep on :message-store-api (no longer needed)
- :storage-{inmemory,sqlite,ksqlite}: drop deps on :storage-bundle
This commit is contained in:
2026-09-20 18:06:46 +03:00
parent 15f3952eba
commit d74af621d8
36 changed files with 313 additions and 1259 deletions
+3 -1
View File
@@ -32,9 +32,11 @@ kotlin {
sourceSets {
commonMain.dependencies {
api(project(":storage-bundle"))
implementation(project(":proto"))
implementation(project(":server"))
implementation(project(":message-store-api"))
implementation(project(":message-log-api"))
implementation(project(":working-memory-api"))
// Commons
implementation(libs.kotlinx.coroutines.core)
@@ -9,13 +9,15 @@ import io.ktor.server.routing.post
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import pw.binom.agentik.memory.ConversationTurn
import pw.binom.agentik.messageStore.ReflectionStore
import pw.binom.agentik.messageLog.MessageStore
import pw.binom.agentik.proto.Agent
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.storageBundle.StorageBundle
import pw.binom.agentik.skills.SkillStore
import pw.binom.agentik.workingMemory.WorkingMemoryStore
/**
* Debug-эндпоинты для ручного триггерирования фоновых фич (без ожидания
@@ -33,7 +35,9 @@ import pw.binom.agentik.skills.SkillStore
*/
internal fun Route.debugRoutes(
agent: Agent,
storage: pw.binom.agentik.storageBundle.StorageBundle,
messageStore: MessageStore,
workingMemoryStore: WorkingMemoryStore,
reflectionStore: ReflectionStore,
reflector: LlmReflector?,
skillMiner: SkillMiner?,
skillStore: SkillStore?,
@@ -44,14 +48,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(storage, convId, maxTurns = 6)
val turns = recentTurns(workingMemoryStore, 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)
storage.reflectionStore.insert(stamped)
reflectionStore.insert(stamped)
call.respondText(
buildJsonObject {
put("reflected", true)
@@ -71,7 +75,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(storage, convId, maxTurns = miner.maxTurns)
val turns = recentTurns(workingMemoryStore, 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 +114,7 @@ internal fun Route.debugRoutes(
get("/debug/tokens") {
val convId = call.parameters["conversationId"]
?: return@get call.respondText("conversationId required", status = HttpStatusCode.BadRequest)
val stats = storage.messageStore.tokenStats(convId)
val stats = messageStore.tokenStats(convId)
val json = buildJsonObject {
put("conversationId", convId)
put("turns", stats.turns.toString())
@@ -127,8 +131,8 @@ internal fun Route.debugRoutes(
* (для debug-триггеров reflector/miner; та же логика, что у хуков
* [ChatConversation]).
*/
internal suspend fun recentTurns(storage: pw.binom.agentik.storageBundle.StorageBundle, conversationId: String, maxTurns: Int): List<ConversationTurn> {
val rows = storage.workingMemoryStore.list(conversationId)
internal suspend fun recentTurns(workingMemoryStore: WorkingMemoryStore, conversationId: String, maxTurns: Int): List<ConversationTurn> {
val rows = workingMemoryStore.list(conversationId)
val pairs = mutableListOf<ConversationTurn>()
var pendingUser: String? = null
for (row in rows) {
@@ -198,7 +198,7 @@ private fun runServer() {
}
val llm = config.llm.createLlm()
val storage = SqliteStores.open(dbPath = config.agent.dbPath).asBundle()
val sqliteStores = SqliteStores.open(dbPath = config.agent.dbPath)
val mcpRegistry = McpRegistry.fromConfig(config.mcp)
// Хранилище скилов: если skillsDir задан, читаем каталог + создаём
@@ -319,12 +319,15 @@ private fun runServer() {
if (config.reflection.interval > 0) LlmReflector(llm = llm) else null
val recentReflections: List<pw.binom.agentik.messageStore.Reflection> =
if (config.reflection.topK > 0) kotlinx.coroutines.runBlocking {
storage.reflectionStore.listRecent(config.reflection.topK)
sqliteStores.reflections.listRecent(config.reflection.topK)
} else emptyList()
val agent = ChatAgent(
id = "agentik",
storage = storage,
conversationStore = sqliteStores.conversations,
messageStore = sqliteStores.messages,
workingMemoryStore = sqliteStores.workingMemory,
reflectionStore = sqliteStores.reflections,
llm = llm,
llmConfig = config.llm,
tools = mcpRegistry.namedTools,
@@ -365,7 +368,9 @@ private fun runServer() {
if (config.debug.endpoints) {
debugRoutes(
agent = agent,
storage = storage,
messageStore = sqliteStores.messages,
workingMemoryStore = sqliteStores.workingMemory,
reflectionStore = sqliteStores.reflections,
reflector = reflector,
skillMiner = skillMiner,
skillStore = skillStore,
@@ -403,14 +408,14 @@ private fun runServer() {
// Token stats по существующим диалогам (агрегат на старте — каждая запись
// парсится из payload_json, ну >100 turns и БД приличная — но в рамках
// стартапа это терпимо).
val existingConvs = kotlinx.coroutines.runBlocking { storage.conversationStore.list(offset = 0, limit = 1000) }
val existingConvs = kotlinx.coroutines.runBlocking { sqliteStores.conversations.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 { storage.messageStore.tokenStats(c.id) }
val s = kotlinx.coroutines.runBlocking { sqliteStores.messages.tokenStats(c.id) }
totalTurns += s.turns
totalIn += s.inputTokens
totalOut += s.outputTokens
@@ -422,7 +427,9 @@ private fun runServer() {
Runtime.getRuntime().addShutdownHook(Thread {
agent.close()
mcpRegistry.close()
storage.close()
})
Runtime.getRuntime().addShutdownHook(Thread {
sqliteStores.close()
llm.close()
memorySystem?.close()
})
@@ -26,10 +26,12 @@ 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.messageStore.ConversationRecord
import pw.binom.agentik.messageStore.ConversationStore
import pw.binom.agentik.messageStore.Ids
import pw.binom.agentik.messageStore.Reflection
import pw.binom.agentik.messageStore.events.EventRecord
import pw.binom.agentik.messageStore.events.EventType
import pw.binom.agentik.messageStore.ReflectionStore
import pw.binom.agentik.messageLog.MessageStore
import pw.binom.agentik.workingMemory.WorkingMemoryStore
import pw.binom.agentik.toolsets.DisableToolsetTool
import pw.binom.agentik.toolsets.EnableToolsetTool
import pw.binom.agentik.toolsets.NamedTool
@@ -61,7 +63,10 @@ import pw.binom.litert.LiteLlm
*/
class ChatAgent(
override val id: String,
private val storage: pw.binom.agentik.storageBundle.StorageBundle,
private val conversationStore: ConversationStore,
private val messageStore: MessageStore,
private val workingMemoryStore: WorkingMemoryStore,
private val reflectionStore: ReflectionStore,
private val llm: LiteLlm,
private val llmConfig: LlmConfig,
private val tools: List<NamedTool> = emptyList(),
@@ -251,12 +256,15 @@ class ChatAgent(
// и не переживают рестарт агента (см. Memory #3709).
if (!temp) {
runBlocking {
storage.conversationStore.upsert(rec)
conversationStore.upsert(rec)
}
}
val conv = ChatConversation(
record = rec,
storage = storage,
conversationStore = conversationStore,
messageStore = messageStore,
workingMemoryStore = workingMemoryStore,
reflectionStore = reflectionStore,
eventStore = eventStore,
llm = llm,
systemPrompt = systemPrompt,
@@ -268,7 +276,6 @@ class ChatAgent(
contextWindow = contextWindow,
compressionThreshold = compressionThreshold,
contextCompactor = contextCompactor,
reflectionStore = storage.reflectionStore,
reflector = reflector,
skillMiner = skillMiner,
skillMiningStore = skillStore,
@@ -289,7 +296,7 @@ class ChatAgent(
override suspend fun getConversation(id: String): ProtoConversation? {
liveLock.withLock { live[id] }?.let { if (!it.isClosed) return it }
val rec = storage.conversationStore.get(id) ?: return null
val rec = conversationStore.get(id) ?: return null
return newConversation(rec).also {
liveLock.withLock { live[id] = it }
}
@@ -298,7 +305,7 @@ class ChatAgent(
override suspend fun deleteConversation(id: String): Boolean {
val conv = liveLock.withLock { live.remove(id) }
conv?.close()
val ok = storage.conversationStore.delete(id)
val ok = conversationStore.delete(id)
if (ok) {
val event = AgentEvent.Deleted(date = now(), id = id)
eventStore.append(CommonEvent.Agent(date = now(), event = event))
@@ -307,7 +314,7 @@ class ChatAgent(
}
override suspend fun getConversations(offset: Int, limit: Int): List<ProtoConversation> =
storage.conversationStore.list(offset = offset, limit = limit).map { rec ->
conversationStore.list(offset = offset, limit = limit).map { rec ->
liveLock.withLock { live[rec.id] }
?: newConversation(rec).also {
liveLock.withLock { live[rec.id] = it }
@@ -316,7 +323,10 @@ class ChatAgent(
private fun newConversation(rec: ConversationRecord): ChatConversation = ChatConversation(
record = rec,
storage = storage,
conversationStore = conversationStore,
messageStore = messageStore,
workingMemoryStore = workingMemoryStore,
reflectionStore = reflectionStore,
eventStore = eventStore,
llm = llm,
systemPrompt = systemPrompt,
@@ -328,11 +338,10 @@ class ChatAgent(
contextWindow = contextWindow,
compressionThreshold = compressionThreshold,
contextCompactor = contextCompactor,
reflectionStore = storage.reflectionStore,
reflector = reflector,
skillMiner = skillMiner,
skillMiningStore = skillStore,
)
reflector = reflector,
skillMiner = skillMiner,
skillMiningStore = skillStore,
)
override fun close() {
runBlocking {
@@ -36,7 +36,6 @@ import pw.binom.agentik.messageLog.MessageOrigin
import pw.binom.agentik.messageLog.MessageRecord
import pw.binom.agentik.messageLog.MessageStore
import pw.binom.agentik.messageStore.ReflectionStore
import pw.binom.agentik.storageBundle.StorageBundle
import pw.binom.agentik.messageLog.TurnTokens
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
import pw.binom.agentik.workingMemory.WorkingMemoryStore
@@ -55,7 +54,10 @@ import pw.binom.agentik.toolsets.NamedTool
class ConversationLoop(
record: ConversationRecord,
private val storage: StorageBundle,
private val conversationStore: ConversationStore,
private val messageStore: MessageStore,
private val workingMemoryStore: WorkingMemoryStore,
private val reflectionStore: ReflectionStore?,
private val eventStore: pw.binom.agentik.eventStore.MutableEventStore,
private val llm: LiteLlm,
private val systemPrompt: String,
@@ -67,7 +69,6 @@ class ConversationLoop(
private val contextWindow: Int? = null,
private val compressionThreshold: Double = 0.8,
private val contextCompactor: ContextCompactor? = null,
private val reflectionStore: ReflectionStore? = null,
private val reflector: LlmReflector? = null,
private val skillMiner: SkillMiner? = null,
private val skillMiningStore: SkillStore? = null,
@@ -93,10 +94,6 @@ class ConversationLoop(
/** Per-conversation background event bus. Lifecycle scoped к этому ConversationLoop. */
private val backgroundEvents = BackgroundEventBus()
private val conversationStore: ConversationStore get() = storage.conversationStore
private val messageStore: MessageStore get() = storage.messageStore
private val workingMemory: WorkingMemoryStore get() = storage.workingMemoryStore
private val toolsByName: MutableMap<String, NamedTool> = tools.associateBy { it.name }.toMutableMap()
private val contextBuilder = ContextBuilder(memoryPrefetcher = memoryPrefetcher)
@@ -108,7 +105,7 @@ class ConversationLoop(
contextCompactor = contextCompactor,
memoryReviewer = memoryReviewer,
memoryStoreForReview = memoryStoreForReview,
workingMemory = workingMemory,
workingMemory = workingMemoryStore,
liteLlm = llm,
systemPrompt = systemPrompt,
backgroundEvents = backgroundEvents,
@@ -128,7 +125,7 @@ class ConversationLoop(
private val backgroundScheduler = BackgroundScheduler(
state = state,
workingMemory = workingMemory,
workingMemory = workingMemoryStore,
config = BackgroundConfig(
memoryReviewer = memoryReviewer,
memoryStore = memoryStoreForReview,
@@ -179,7 +176,7 @@ class ConversationLoop(
if (!state.isTemporal) {
messageStore.append(userRecord)
workingMemory.append(
workingMemoryStore.append(
conversationId = id,
entry = WorkingMemoryEntry.User(
sourceMessageId = userMessageId,
@@ -412,7 +409,7 @@ class ConversationLoop(
)
messageStore.append(assistantRecord)
workingMemory.append(
workingMemoryStore.append(
conversationId = id,
entry = WorkingMemoryEntry.Assistant(
sourceMessageId = assistantId,
@@ -422,7 +419,7 @@ class ConversationLoop(
)
for (ex in toolExchanges) {
workingMemory.append(
workingMemoryStore.append(
conversationId = id,
entry = ex,
now = assistantAt,
@@ -41,28 +41,31 @@ import pw.binom.agentik.toolsets.NamedTool
class ChatAgentTest {
private lateinit var storage: pw.binom.agentik.storageBundle.StorageBundle
private lateinit var sqliteStores: pw.binom.agentik.storage.sqlite.SqliteStores
private lateinit var fakeLlm: FakeLiteLlm
@BeforeTest
fun setup() {
storage = SqliteStores.inMemory().asBundle()
sqliteStores = SqliteStores.inMemory()
fakeLlm = FakeLiteLlm()
}
@AfterTest
fun tearDown() {
storage.close()
sqliteStores.close()
}
private fun newAgent(
storage: pw.binom.agentik.storageBundle.StorageBundle = this.storage,
sqliteStores: pw.binom.agentik.storage.sqlite.SqliteStores = this.sqliteStores,
llm: LiteLlm = this.fakeLlm,
tools: List<NamedTool> = emptyList(),
skills: SkillCatalog = SkillCatalog.EMPTY,
): ChatAgent = ChatAgent(
id = "agentik",
storage = storage,
conversationStore = sqliteStores.conversations,
messageStore = sqliteStores.messages,
workingMemoryStore = sqliteStores.workingMemory,
reflectionStore = sqliteStores.reflections,
llm = llm,
llmConfig = LlmConfig(
backend = LlmBackend.OPENAI,
@@ -82,7 +85,7 @@ class ChatAgentTest {
val agent = newAgent()
val conv = agent.createConversation(temp = false) as ChatConversation
val wm = storage.workingMemoryStore.list(conv.id)
val wm = sqliteStores.workingMemory.list(conv.id)
assertEquals(0, wm.size)
// System prompt виден через LiteConversationConfig, который LLM получит
// при первом send (см. `send passes system prompt and past history to LLM on first send`).
@@ -159,7 +162,7 @@ class ChatAgentTest {
val id = conv.id
// добавим сообщение, чтобы потом убедиться, что каскад сработал
storage.messageStore.append(
sqliteStores.messages.append(
pw.binom.agentik.messageLog.MessageRecord.UserMessage(
id = "m1",
conversationId = id,
@@ -169,8 +172,8 @@ class ChatAgentTest {
)
assertTrue(agent.deleteConversation(id))
assertNull(agent.getConversation(id))
assertNull(storage.conversationStore.get(id))
assertEquals(emptyList(), storage.messageStore.listAll(id))
assertNull(sqliteStores.conversations.get(id))
assertEquals(emptyList(), sqliteStores.messages.listAll(id))
}
@Test
@@ -188,7 +191,7 @@ class ChatAgentTest {
conv.send(listOf(Content.Text("hi")))
// user message записан в audit + working memory
val msgs = storage.messageStore.listAll(conv.id)
val msgs = sqliteStores.messages.listAll(conv.id)
assertEquals(2, msgs.size)
assertEquals("hi", (msgs[0] as pw.binom.agentik.messageLog.MessageRecord.UserMessage).content.let {
(it[0] as pw.binom.agentik.messageLog.Content.Text).body
@@ -214,9 +217,9 @@ class ChatAgentTest {
// В working_memory теперь НЕТ System-entries — только user + assistant.
// Системный промт живёт в ChatConversation.systemPrompt и едет в LLM
// через LiteConversationConfig.systemInstruction.
val wm1 = storage.workingMemoryStore.list(conv1.id)
val wm1 = sqliteStores.workingMemory.list(conv1.id)
assertEquals(2, wm1.size)
val wm2 = storage.workingMemoryStore.list(conv2.id)
val wm2 = sqliteStores.workingMemory.list(conv2.id)
assertEquals(2, wm2.size)
}
@@ -256,12 +259,12 @@ class ChatAgentTest {
fakeLlm.reply = "first reply"
conv.send(listOf(Content.Text("first user")))
// первый turn: WM = [user, assistant]
assertEquals(2, storage.workingMemoryStore.list(conv.id).size)
assertEquals(2, sqliteStores.workingMemory.list(conv.id).size)
fakeLlm.reply = "second reply"
conv.send(listOf(Content.Text("second user")))
// второй turn: WM должен вырасти до [user, assistant, user, assistant]
val wm = storage.workingMemoryStore.list(conv.id)
val wm = sqliteStores.workingMemory.list(conv.id)
System.err.println("[TEST] wm.size=${wm.size}")
wm.forEachIndexed { i, row -> System.err.println("[TEST] $i: ${row.entry::class.simpleName} id=${row.id}") }
assertEquals(4, wm.size)
@@ -325,7 +328,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 = storage.messageStore.listAll(conv.id)
val msgs = sqliteStores.messages.listAll(conv.id)
assertEquals(2, msgs.size)
assertIs<pw.binom.agentik.messageLog.MessageRecord.UserMessage>(msgs[0])
val err = assertIs<pw.binom.agentik.messageLog.MessageRecord.Error>(msgs[1])
@@ -370,12 +373,12 @@ class ChatAgentTest {
eventsJob.cancel()
// audit: только user (assistant не успел сгенериться)
val msgs = storage.messageStore.listAll(conv.id)
val msgs = sqliteStores.messages.listAll(conv.id)
assertEquals(1, msgs.size)
assertIs<pw.binom.agentik.messageLog.MessageRecord.UserMessage>(msgs[0])
// working memory: только user (assistant skipped because пустой)
val wm = storage.workingMemoryStore.list(conv.id)
val wm = sqliteStores.workingMemory.list(conv.id)
assertEquals(1, wm.size)
assertTrue(wm[0].entry is WorkingMemoryEntry.User)
@@ -427,14 +430,14 @@ class ChatAgentTest {
eventsJob.cancel()
// audit: user + toolcall + toolresult (tool выполнился), assistant может быть
val msgs = storage.messageStore.listAll(conv.id)
val msgs = sqliteStores.messages.listAll(conv.id)
val toolResult = msgs.filterIsInstance<pw.binom.agentik.messageLog.MessageRecord.ToolResult>().firstOrNull()
assertNotNull(toolResult, "tool result должен быть в audit — tool выполнился нормально")
val toolResultResult = toolResult!!.result!!
assertTrue(toolResultResult.contains("echo"), "tool result содержит реальный ответ тулы: $toolResultResult")
// working memory: user + tool_exchange
val wm = storage.workingMemoryStore.list(conv.id)
val wm = sqliteStores.workingMemory.list(conv.id)
val exchanges = wm.mapNotNull { (it.entry as? WorkingMemoryEntry.ToolExchange) }
assertEquals(1, exchanges.size)
assertEquals("echo_tool", exchanges[0].toolName)
@@ -450,12 +453,15 @@ class ChatAgentTest {
@Test
fun `temp conversation is not persisted across agent instances`() = runTest {
// Поднимаем file-backed БД, создаём temp-беседу
storage.close()
sqliteStores.close()
val dbPath = (System.getProperty("java.io.tmpdir") + "/agentik-test-${System.nanoTime()}.db")
storage = SqliteStores.open(dbPath).asBundle()
sqliteStores = SqliteStores.open(dbPath)
val agent1 = ChatAgent(
id = "agentik",
storage = storage,
conversationStore = sqliteStores.conversations,
messageStore = sqliteStores.messages,
workingMemoryStore = sqliteStores.workingMemory,
reflectionStore = sqliteStores.reflections,
llm = FakeLiteLlm().also { fakeLlm = it },
llmConfig = LlmConfig(
backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI,
@@ -468,11 +474,14 @@ class ChatAgentTest {
assertNotNull(agent1.getConversation(tempId))
// Переоткрываем БД — temp-беседа не должна пережить рестарт
storage.close()
storage = SqliteStores.open(dbPath).asBundle()
sqliteStores.close()
sqliteStores = SqliteStores.open(dbPath)
val agent2 = ChatAgent(
id = "agentik",
storage = storage,
conversationStore = sqliteStores.conversations,
messageStore = sqliteStores.messages,
workingMemoryStore = sqliteStores.workingMemory,
reflectionStore = sqliteStores.reflections,
llm = fakeLlm,
llmConfig = LlmConfig(
backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI,
@@ -486,12 +495,15 @@ class ChatAgentTest {
@Test
fun `non-temp conversation persists across agent instances`() = runTest {
storage.close()
sqliteStores.close()
val dbPath = (System.getProperty("java.io.tmpdir") + "/agentik-test-${System.nanoTime()}.db")
storage = SqliteStores.open(dbPath).asBundle()
sqliteStores = SqliteStores.open(dbPath)
val agent1 = ChatAgent(
id = "agentik",
storage = storage,
conversationStore = sqliteStores.conversations,
messageStore = sqliteStores.messages,
workingMemoryStore = sqliteStores.workingMemory,
reflectionStore = sqliteStores.reflections,
llm = FakeLiteLlm().also { fakeLlm = it },
llmConfig = LlmConfig(
backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI,
@@ -502,11 +514,14 @@ class ChatAgentTest {
val conv = agent1.createConversation(temp = false)
val id = conv.id
storage.close()
storage = SqliteStores.open(dbPath).asBundle()
sqliteStores.close()
sqliteStores = SqliteStores.open(dbPath)
val agent2 = ChatAgent(
id = "agentik",
storage = storage,
conversationStore = sqliteStores.conversations,
messageStore = sqliteStores.messages,
workingMemoryStore = sqliteStores.workingMemory,
reflectionStore = sqliteStores.reflections,
llm = fakeLlm,
llmConfig = LlmConfig(
backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI,
@@ -39,11 +39,14 @@ class ChatAgentToolsetsTest {
private fun newAgent(
toolsets: List<ToolsetContribution> = emptyList(),
): Pair<ChatAgent, pw.binom.agentik.storageBundle.StorageBundle> {
val storage = SqliteStores.inMemory().asBundle()
): Pair<ChatAgent, pw.binom.agentik.storage.sqlite.SqliteStores> {
val sqliteStores = SqliteStores.inMemory()
val agent = ChatAgent(
id = "test-agent",
storage = storage,
conversationStore = sqliteStores.conversations,
messageStore = sqliteStores.messages,
workingMemoryStore = sqliteStores.workingMemory,
reflectionStore = sqliteStores.reflections,
llm = stubLlm(),
llmConfig = LlmConfig(
backend = pw.binom.agentik.standalone.llm.LlmBackend.GOOGLE,
@@ -52,7 +55,7 @@ class ChatAgentToolsetsTest {
),
toolsets = toolsets,
)
return agent to storage
return agent to sqliteStores
}
@Test
@@ -40,18 +40,18 @@ import pw.binom.agentik.llm.tools.SummaryTurn
*/
class CompactionTest {
private lateinit var storage: pw.binom.agentik.storageBundle.StorageBundle
private lateinit var sqliteStores: pw.binom.agentik.storage.sqlite.SqliteStores
private lateinit var fakeLlm: FakeLiteLlm
@BeforeTest
fun setup() {
storage = SqliteStores.inMemory().asBundle()
sqliteStores = SqliteStores.inMemory()
fakeLlm = FakeLiteLlm()
}
@AfterTest
fun tearDown() {
storage.close()
sqliteStores.close()
}
private fun newAgent(
@@ -63,7 +63,10 @@ class CompactionTest {
val reviewer = if (memoryStore != null) KeywordMdReviewer() else null
return ChatAgent(
id = "test",
storage = storage,
conversationStore = sqliteStores.conversations,
messageStore = sqliteStores.messages,
workingMemoryStore = sqliteStores.workingMemory,
reflectionStore = sqliteStores.reflections,
llm = fakeLlm,
llmConfig = LlmConfig(
backend = LlmBackend.OPENAI,
@@ -87,7 +90,7 @@ class CompactionTest {
repeat(10) {
conv.send(listOf(ProtoContent.Text("turn $it: ${"x".repeat(200)}")))
}
val wm = storage.workingMemoryStore.list(conv.id)
val wm = sqliteStores.workingMemory.list(conv.id)
// Без compaction все ходы остаются в памяти (System + 10 user/assistant = 21 строк).
val summaries = wm.filter { it.entry is pw.binom.agentik.workingMemory.WorkingMemoryEntry.Summary }
assertEquals(0, summaries.size, "compaction must not run without contextWindow")
@@ -99,7 +102,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 = storage.workingMemoryStore.list(conv.id)
val wm = sqliteStores.workingMemory.list(conv.id)
// System + User + Assistant = 3. Без compactor — никаких Summary.
val summaries = wm.filter { it.entry is pw.binom.agentik.workingMemory.WorkingMemoryEntry.Summary }
assertEquals(0, summaries.size, "no compaction runs without compactor")
@@ -118,7 +121,7 @@ class CompactionTest {
// Compactor должен был быть вызван хотя бы раз.
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 wm = sqliteStores.workingMemory.list(conv.id)
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.
@@ -158,7 +161,7 @@ class CompactionTest {
conv.send(listOf(ProtoContent.Text("second turn")))
conv.send(listOf(ProtoContent.Text("third turn — long content ${"y".repeat(150)}")))
val wm = storage.workingMemoryStore.list(conv.id)
val wm = sqliteStores.workingMemory.list(conv.id)
// Должны быть: System + хотя бы один Summary + последние KEEP_RECENT_TURNS ходов.
// KEEP_RECENT_TURNS = 4 → user/assistant последних двух ходов (third + second) могут быть не тронуты.
val userAssistantCount = wm.count {
@@ -49,13 +49,13 @@ import kotlin.time.Instant
*/
class MemoryWiringTest {
private lateinit var storage: pw.binom.agentik.storageBundle.StorageBundle
private lateinit var sqliteStores: pw.binom.agentik.storage.sqlite.SqliteStores
private lateinit var fakeLlm: FakeLiteLlm
private lateinit var root: Path
@BeforeTest
fun setup() {
storage = SqliteStores.inMemory().asBundle()
sqliteStores = SqliteStores.inMemory()
fakeLlm = FakeLiteLlm()
root = Path(SystemTemporaryDirectory.toString(), "agentik-mem-${java.util.UUID.randomUUID()}")
SystemFileSystem.createDirectories(root, mustCreate = true)
@@ -63,7 +63,7 @@ class MemoryWiringTest {
@AfterTest
fun tearDown() {
storage.close()
sqliteStores.close()
runCatching { SystemFileSystem.delete(root, mustExist = false) }
}
@@ -76,7 +76,10 @@ class MemoryWiringTest {
contextCompactor: ContextCompactor = EchoCompactor,
): ChatAgent = ChatAgent(
id = "agentik",
storage = storage,
conversationStore = sqliteStores.conversations,
messageStore = sqliteStores.messages,
workingMemoryStore = sqliteStores.workingMemory,
reflectionStore = sqliteStores.reflections,
llm = fakeLlm,
llmConfig = LlmConfig(
backend = LlmBackend.OPENAI,
@@ -112,7 +115,10 @@ class MemoryWiringTest {
val soulBody = "I am a helpful test persona. I always answer in one short line."
val agent = ChatAgent(
id = "agentik",
storage = storage,
conversationStore = sqliteStores.conversations,
messageStore = sqliteStores.messages,
workingMemoryStore = sqliteStores.workingMemory,
reflectionStore = sqliteStores.reflections,
llm = fakeLlm,
llmConfig = LlmConfig(
backend = LlmBackend.OPENAI,
@@ -137,7 +143,10 @@ class MemoryWiringTest {
fun `soul body not added when null`() = runBlocking {
val agent = ChatAgent(
id = "agentik",
storage = storage,
conversationStore = sqliteStores.conversations,
messageStore = sqliteStores.messages,
workingMemoryStore = sqliteStores.workingMemory,
reflectionStore = sqliteStores.reflections,
llm = fakeLlm,
llmConfig = LlmConfig(
backend = LlmBackend.OPENAI,