standalone: ChatAgent принимает StorageBundle вместо SqliteStores
Финальный 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. Без регрессий.
This commit is contained in:
@@ -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<ConversationTurn> {
|
||||
val rows = stores.workingMemory.list(conversationId)
|
||||
internal suspend fun recentTurns(storage: pw.binom.agentik.storage.StorageBundle, conversationId: String, maxTurns: Int): List<ConversationTurn> {
|
||||
val rows = storage.workingMemoryStore.list(conversationId)
|
||||
val pairs = mutableListOf<ConversationTurn>()
|
||||
var pendingUser: String? = null
|
||||
for (row in rows) {
|
||||
|
||||
@@ -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<pw.binom.agentik.storage.Reflection> =
|
||||
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()
|
||||
})
|
||||
|
||||
@@ -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<NamedTool> = 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<ProtoConversation> =
|
||||
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,
|
||||
|
||||
@@ -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<NamedTool> = 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<String, NamedTool> = tools.associateBy { it.name }
|
||||
|
||||
|
||||
@@ -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<NamedTool> = 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<pw.binom.agentik.storage.MessageRecord.UserMessage>(msgs[0])
|
||||
val err = assertIs<pw.binom.agentik.storage.MessageRecord.Error>(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<pw.binom.agentik.storage.MessageRecord.UserMessage>(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,
|
||||
|
||||
+14
-14
@@ -39,11 +39,11 @@ class ChatAgentToolsetsTest {
|
||||
|
||||
private fun newAgent(
|
||||
toolsets: List<ToolsetContribution> = emptyList(),
|
||||
): Pair<ChatAgent, SqliteStores> {
|
||||
val stores = SqliteStores.inMemory()
|
||||
): Pair<ChatAgent, pw.binom.agentik.storage.StorageBundle> {
|
||||
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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user