refactor(storage): drop StorageBundle + legacy EventStore, add HttpEventStore
ci / JVM build + tests (push) Successful in 6m13s
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:
@@ -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 {
|
||||
|
||||
+9
-12
@@ -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,
|
||||
|
||||
+7
-4
@@ -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 {
|
||||
|
||||
+15
-6
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user