Коммит заменяет SQLDelight на ksqlite и добавляет journal/context/outbox
ci / JVM build + tests (push) Failing after 1m24s
ci / JVM build + tests (push) Failing after 1m24s
This commit is contained in:
@@ -22,6 +22,13 @@ val skipVectorMemory: Boolean =
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
|
||||
// Отключаем auto-propagation Apple-таргетов от KMP-зависимостей (`:proto`,
|
||||
// `:server`, `:agent-toolsets` имеют Apple-варианты). `:standalone` JVM-only,
|
||||
// Apple таргеты не нужны и ломают резолюцию `:message-store-api` в appleMain
|
||||
// (он KMP jvm+linuxX64+mingwX64 без Apple).
|
||||
// @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
|
||||
// applyDefaultHierarchyTemplate { }
|
||||
|
||||
jvm {
|
||||
binaries {
|
||||
executable {
|
||||
@@ -61,7 +68,7 @@ kotlin {
|
||||
}
|
||||
implementation(project(":message-store-api"))
|
||||
implementation(project(":working-memory-api"))
|
||||
implementation(project(":storage-sqlite"))
|
||||
implementation(project(":storage-ksqlite"))
|
||||
|
||||
// Новый единый канал событий агента — заменил старые
|
||||
// `agentEvents: MutableSharedFlow<AgentEvent>` и per-conv `ConversationEvents._flow`.
|
||||
|
||||
@@ -10,7 +10,6 @@ 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
|
||||
@@ -27,7 +26,10 @@ import pw.binom.agentik.workingMemory.WorkingMemoryStore
|
||||
* - `POST /debug/skill-mine?conversationId=...` — прогон [SkillMiner] прямо сейчас
|
||||
* - `POST /debug/curate` — прогон [Curator.runPass] прямо сейчас
|
||||
* - `POST /debug/compact?conversationId=...` — принудительный compaction
|
||||
* - `GET /debug/tokens?conversationId=...` — token-статистика диалога из БД
|
||||
*
|
||||
* Token-stats эндпоинт убран вместе с `MessageStore.tokenStats()` (см.
|
||||
* :message-log-api/MessageStore.kt). Сейчас token accounting доступен
|
||||
* только через assistant-сообщения с `TurnTokens` (см. MessageRecord).
|
||||
*
|
||||
* Каждый возвращает JSON с результатом (что сохранил / нашёл / сжал), чтобы в
|
||||
* тестах было видно не только "триггер сработал", а что именно LLM намайнила.
|
||||
@@ -35,7 +37,6 @@ import pw.binom.agentik.workingMemory.WorkingMemoryStore
|
||||
*/
|
||||
internal fun Route.debugRoutes(
|
||||
agent: Agent,
|
||||
messageStore: MessageStore,
|
||||
workingMemoryStore: WorkingMemoryStore,
|
||||
reflectionStore: ReflectionStore,
|
||||
reflector: LlmReflector?,
|
||||
@@ -110,20 +111,6 @@ internal fun Route.debugRoutes(
|
||||
val ok = chatConv.forceCompactNow()
|
||||
call.respondText("""{"compacted":$ok}""", contentType = ContentType.Application.Json)
|
||||
}
|
||||
|
||||
get("/debug/tokens") {
|
||||
val convId = call.parameters["conversationId"]
|
||||
?: return@get call.respondText("conversationId required", status = HttpStatusCode.BadRequest)
|
||||
val stats = messageStore.tokenStats(convId)
|
||||
val json = buildJsonObject {
|
||||
put("conversationId", convId)
|
||||
put("turns", stats.turns.toString())
|
||||
put("inputTokens", stats.inputTokens.toString())
|
||||
put("outputTokens", stats.outputTokens.toString())
|
||||
put("totalTokens", (stats.inputTokens + stats.outputTokens).toString())
|
||||
}.toString()
|
||||
call.respondText(json, contentType = ContentType.Application.Json)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,7 +29,7 @@ import pw.binom.agentik.standalone.config.AppConfig.MemoryBackend
|
||||
import pw.binom.agentik.standalone.llm.LlmBackend
|
||||
import pw.binom.agentik.standalone.llm.ModelDownloader
|
||||
import pw.binom.agentik.mcp.bridge.McpRegistry
|
||||
import pw.binom.agentik.storage.sqlite.SqliteStores
|
||||
import pw.binom.agentik.storage.ksqlite.KsqliteStores
|
||||
import java.io.File
|
||||
import pw.binom.agentik.llm.tools.SkillMiner
|
||||
/**
|
||||
@@ -198,7 +198,7 @@ private fun runServer() {
|
||||
}
|
||||
|
||||
val llm = config.llm.createLlm()
|
||||
val sqliteStores = SqliteStores.open(dbPath = config.agent.dbPath)
|
||||
val sqliteStores = KsqliteStores.open(path = config.agent.dbPath)
|
||||
val mcpRegistry = McpRegistry.fromConfig(config.mcp)
|
||||
|
||||
// Хранилище скилов: если skillsDir задан, читаем каталог + создаём
|
||||
@@ -368,7 +368,6 @@ private fun runServer() {
|
||||
if (config.debug.endpoints) {
|
||||
debugRoutes(
|
||||
agent = agent,
|
||||
messageStore = sqliteStores.messages,
|
||||
workingMemoryStore = sqliteStores.workingMemory,
|
||||
reflectionStore = sqliteStores.reflections,
|
||||
reflector = reflector,
|
||||
@@ -405,24 +404,13 @@ private fun runServer() {
|
||||
if (config.debug.endpoints) {
|
||||
println(" debug endpoints: enabled (/debug/reflect, /debug/skill-mine, /debug/curate, /debug/compact, /debug/tokens)")
|
||||
}
|
||||
// Token stats по существующим диалогам (агрегат на старте — каждая запись
|
||||
// парсится из payload_json, ну >100 turns и БД приличная — но в рамках
|
||||
// стартапа это терпимо).
|
||||
// Раньше здесь был агрегат tokenStats() по всем conv'ам при старте. Метод
|
||||
// убран из :message-log-api (MessageStore стал чисто read-only list+listFlow);
|
||||
// см. agentik :message-log-api/MessageStore.kt. Token accounting теперь
|
||||
// доступен через assistant-сообщения с TurnTokens (см. MessageRecord.AssistantMessage).
|
||||
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 { sqliteStores.messages.tokenStats(c.id) }
|
||||
totalTurns += s.turns
|
||||
totalIn += s.inputTokens
|
||||
totalOut += s.outputTokens
|
||||
}
|
||||
if (totalTurns > 0) {
|
||||
println(" tokens: ${existingConvs.size} convs, $totalTurns turns, in=${totalIn}, out=${totalOut}, total=${totalIn + totalOut}")
|
||||
}
|
||||
println(" conversations: ${existingConvs.size} (active)")
|
||||
}
|
||||
Runtime.getRuntime().addShutdownHook(Thread {
|
||||
agent.close()
|
||||
|
||||
+17
-2
@@ -196,8 +196,23 @@ class ConversationLoop(
|
||||
}
|
||||
|
||||
override suspend fun interrupt() {
|
||||
if (activeTurn?.isActive != true) {
|
||||
log.info { "interrupt() no-op: no active turn for $id" }
|
||||
// Всегда ставим флаг — даже если activeTurn ещё не стартовал.
|
||||
// runTurn проверяет interrupted.get() при входе (short-circuit) и в
|
||||
// каждой итерации цикла + finally. Если turn запустится ПОСЛЕ нашего
|
||||
// interrupt() — он увидит флаг на entry и сразу завершится без
|
||||
// реального LLM-вызова. Если turn уже идёт — Interrupted + End придут
|
||||
// в finally.
|
||||
//
|
||||
// Раньше здесь был early-return при `!activeTurn?.isActive`, но это
|
||||
// давало race с точки зрения тестов: send() может завершиться
|
||||
// быстрее (например, на ksqlite-бэкенде, где messageStore.append
|
||||
// практически мгновенный), и interrupt(), вызванный после
|
||||
// delay(200) от launch send(), видел completed Job → no-op →
|
||||
// Interrupted event не эмитится.
|
||||
val wasActive = activeTurn?.isActive
|
||||
if (wasActive != null && !wasActive) {
|
||||
// Предыдущий turn уже завершился — interrupt() действительно no-op.
|
||||
log.info { "interrupt() no-op: previous turn already completed for $id" }
|
||||
return
|
||||
}
|
||||
interrupted.set(true)
|
||||
|
||||
@@ -16,7 +16,7 @@ import pw.binom.agentik.standalone.llm.LlmBackend
|
||||
import pw.binom.agentik.standalone.llm.LlmConfig
|
||||
import pw.binom.agentik.messageLog.MessageRecord
|
||||
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
|
||||
import pw.binom.agentik.storage.sqlite.SqliteStores
|
||||
import pw.binom.agentik.storage.ksqlite.KsqliteStores
|
||||
import pw.binom.litert.LiteContentPart
|
||||
import pw.binom.litert.LiteConversation
|
||||
import pw.binom.litert.LiteConversationConfig
|
||||
@@ -41,12 +41,12 @@ import pw.binom.agentik.toolsets.NamedTool
|
||||
|
||||
class ChatAgentTest {
|
||||
|
||||
private lateinit var sqliteStores: pw.binom.agentik.storage.sqlite.SqliteStores
|
||||
private lateinit var sqliteStores: pw.binom.agentik.storage.ksqlite.KsqliteStores
|
||||
private lateinit var fakeLlm: FakeLiteLlm
|
||||
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
sqliteStores = SqliteStores.inMemory()
|
||||
sqliteStores = KsqliteStores.inMemory("chat-${kotlin.random.Random.nextLong()}")
|
||||
fakeLlm = FakeLiteLlm()
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class ChatAgentTest {
|
||||
}
|
||||
|
||||
private fun newAgent(
|
||||
sqliteStores: pw.binom.agentik.storage.sqlite.SqliteStores = this.sqliteStores,
|
||||
sqliteStores: pw.binom.agentik.storage.ksqlite.KsqliteStores = this.sqliteStores,
|
||||
llm: LiteLlm = this.fakeLlm,
|
||||
tools: List<NamedTool> = emptyList(),
|
||||
skills: SkillCatalog = SkillCatalog.EMPTY,
|
||||
@@ -173,7 +173,7 @@ class ChatAgentTest {
|
||||
assertTrue(agent.deleteConversation(id))
|
||||
assertNull(agent.getConversation(id))
|
||||
assertNull(sqliteStores.conversations.get(id))
|
||||
assertEquals(emptyList(), sqliteStores.messages.listAll(id))
|
||||
assertEquals(emptyList(), sqliteStores.messages.listFlow(id, Instant.DISTANT_PAST).toList())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -191,7 +191,7 @@ class ChatAgentTest {
|
||||
conv.send(listOf(Content.Text("hi")))
|
||||
|
||||
// user message записан в audit + working memory
|
||||
val msgs = sqliteStores.messages.listAll(conv.id)
|
||||
val msgs = sqliteStores.messages.listFlow(conv.id, Instant.DISTANT_PAST).toList()
|
||||
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
|
||||
@@ -328,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 = sqliteStores.messages.listAll(conv.id)
|
||||
val msgs = sqliteStores.messages.listFlow(conv.id, Instant.DISTANT_PAST).toList()
|
||||
assertEquals(2, msgs.size)
|
||||
assertIs<pw.binom.agentik.messageLog.MessageRecord.UserMessage>(msgs[0])
|
||||
val err = assertIs<pw.binom.agentik.messageLog.MessageRecord.Error>(msgs[1])
|
||||
@@ -373,7 +373,7 @@ class ChatAgentTest {
|
||||
eventsJob.cancel()
|
||||
|
||||
// audit: только user (assistant не успел сгенериться)
|
||||
val msgs = sqliteStores.messages.listAll(conv.id)
|
||||
val msgs = sqliteStores.messages.listFlow(conv.id, Instant.DISTANT_PAST).toList()
|
||||
assertEquals(1, msgs.size)
|
||||
assertIs<pw.binom.agentik.messageLog.MessageRecord.UserMessage>(msgs[0])
|
||||
|
||||
@@ -430,7 +430,7 @@ class ChatAgentTest {
|
||||
eventsJob.cancel()
|
||||
|
||||
// audit: user + toolcall + toolresult (tool выполнился), assistant может быть
|
||||
val msgs = sqliteStores.messages.listAll(conv.id)
|
||||
val msgs = sqliteStores.messages.listFlow(conv.id, Instant.DISTANT_PAST).toList()
|
||||
val toolResult = msgs.filterIsInstance<pw.binom.agentik.messageLog.MessageRecord.ToolResult>().firstOrNull()
|
||||
assertNotNull(toolResult, "tool result должен быть в audit — tool выполнился нормально")
|
||||
val toolResultResult = toolResult!!.result!!
|
||||
@@ -455,7 +455,7 @@ class ChatAgentTest {
|
||||
// Поднимаем file-backed БД, создаём temp-беседу
|
||||
sqliteStores.close()
|
||||
val dbPath = (System.getProperty("java.io.tmpdir") + "/agentik-test-${System.nanoTime()}.db")
|
||||
sqliteStores = SqliteStores.open(dbPath)
|
||||
sqliteStores = KsqliteStores.open(dbPath)
|
||||
val agent1 = ChatAgent(
|
||||
id = "agentik",
|
||||
conversationStore = sqliteStores.conversations,
|
||||
@@ -475,7 +475,7 @@ class ChatAgentTest {
|
||||
|
||||
// Переоткрываем БД — temp-беседа не должна пережить рестарт
|
||||
sqliteStores.close()
|
||||
sqliteStores = SqliteStores.open(dbPath)
|
||||
sqliteStores = KsqliteStores.open(dbPath)
|
||||
val agent2 = ChatAgent(
|
||||
id = "agentik",
|
||||
conversationStore = sqliteStores.conversations,
|
||||
@@ -497,7 +497,7 @@ class ChatAgentTest {
|
||||
fun `non-temp conversation persists across agent instances`() = runTest {
|
||||
sqliteStores.close()
|
||||
val dbPath = (System.getProperty("java.io.tmpdir") + "/agentik-test-${System.nanoTime()}.db")
|
||||
sqliteStores = SqliteStores.open(dbPath)
|
||||
sqliteStores = KsqliteStores.open(dbPath)
|
||||
val agent1 = ChatAgent(
|
||||
id = "agentik",
|
||||
conversationStore = sqliteStores.conversations,
|
||||
@@ -515,7 +515,7 @@ class ChatAgentTest {
|
||||
val id = conv.id
|
||||
|
||||
sqliteStores.close()
|
||||
sqliteStores = SqliteStores.open(dbPath)
|
||||
sqliteStores = KsqliteStores.open(dbPath)
|
||||
val agent2 = ChatAgent(
|
||||
id = "agentik",
|
||||
conversationStore = sqliteStores.conversations,
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ package pw.binom.agentik.standalone.agent
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import pw.binom.agentik.standalone.llm.LlmConfig
|
||||
import pw.binom.agentik.storage.sqlite.SqliteStores
|
||||
import pw.binom.agentik.storage.ksqlite.KsqliteStores
|
||||
import pw.binom.agentik.toolsets.ToolsetContribution
|
||||
import pw.binom.litert.LiteLlm
|
||||
import pw.binom.litert.LiteTool
|
||||
@@ -39,8 +39,8 @@ class ChatAgentToolsetsTest {
|
||||
|
||||
private fun newAgent(
|
||||
toolsets: List<ToolsetContribution> = emptyList(),
|
||||
): Pair<ChatAgent, pw.binom.agentik.storage.sqlite.SqliteStores> {
|
||||
val sqliteStores = SqliteStores.inMemory()
|
||||
): Pair<ChatAgent, pw.binom.agentik.storage.ksqlite.KsqliteStores> {
|
||||
val sqliteStores = KsqliteStores.inMemory("toolsets-${kotlin.random.Random.nextLong()}")
|
||||
val agent = ChatAgent(
|
||||
id = "test-agent",
|
||||
conversationStore = sqliteStores.conversations,
|
||||
|
||||
@@ -11,7 +11,7 @@ import pw.binom.agentik.memory.md.KeywordMdReviewer
|
||||
import pw.binom.agentik.standalone.llm.LlmBackend
|
||||
import pw.binom.agentik.standalone.llm.LlmConfig
|
||||
import pw.binom.agentik.standalone.llm.OpenAiConfig
|
||||
import pw.binom.agentik.storage.sqlite.SqliteStores
|
||||
import pw.binom.agentik.storage.ksqlite.KsqliteStores
|
||||
import pw.binom.agentik.proto.Content as ProtoContent
|
||||
import pw.binom.litert.LiteConversation
|
||||
import pw.binom.litert.LiteConversationConfig
|
||||
@@ -40,12 +40,12 @@ import pw.binom.agentik.llm.tools.SummaryTurn
|
||||
*/
|
||||
class CompactionTest {
|
||||
|
||||
private lateinit var sqliteStores: pw.binom.agentik.storage.sqlite.SqliteStores
|
||||
private lateinit var sqliteStores: pw.binom.agentik.storage.ksqlite.KsqliteStores
|
||||
private lateinit var fakeLlm: FakeLiteLlm
|
||||
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
sqliteStores = SqliteStores.inMemory()
|
||||
sqliteStores = KsqliteStores.inMemory("compact-${kotlin.random.Random.nextLong()}")
|
||||
fakeLlm = FakeLiteLlm()
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import pw.binom.agentik.standalone.agent.memory.MemoryToolsFactory
|
||||
import pw.binom.agentik.standalone.llm.LlmBackend
|
||||
import pw.binom.agentik.standalone.llm.LlmConfig
|
||||
import pw.binom.agentik.standalone.llm.OpenAiConfig
|
||||
import pw.binom.agentik.storage.sqlite.SqliteStores
|
||||
import pw.binom.agentik.storage.ksqlite.KsqliteStores
|
||||
import pw.binom.agentik.llm.tools.ContextCompactor
|
||||
import pw.binom.agentik.llm.tools.SummaryTurn
|
||||
import kotlin.test.AfterTest
|
||||
@@ -49,13 +49,13 @@ import kotlin.time.Instant
|
||||
*/
|
||||
class MemoryWiringTest {
|
||||
|
||||
private lateinit var sqliteStores: pw.binom.agentik.storage.sqlite.SqliteStores
|
||||
private lateinit var sqliteStores: pw.binom.agentik.storage.ksqlite.KsqliteStores
|
||||
private lateinit var fakeLlm: FakeLiteLlm
|
||||
private lateinit var root: Path
|
||||
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
sqliteStores = SqliteStores.inMemory()
|
||||
sqliteStores = KsqliteStores.inMemory("memwire-${kotlin.random.Random.nextLong()}")
|
||||
fakeLlm = FakeLiteLlm()
|
||||
root = Path(SystemTemporaryDirectory.toString(), "agentik-mem-${java.util.UUID.randomUUID()}")
|
||||
SystemFileSystem.createDirectories(root, mustCreate = true)
|
||||
|
||||
+11
-10
@@ -6,8 +6,9 @@ import pw.binom.agentik.messageLog.MessageRecord
|
||||
import pw.binom.agentik.messageLog.Content
|
||||
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
|
||||
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import pw.binom.agentik.storage.sqlite.SqliteStores
|
||||
import pw.binom.agentik.storage.ksqlite.KsqliteStores
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
@@ -21,11 +22,11 @@ import kotlin.time.Instant
|
||||
|
||||
class PersistenceTest {
|
||||
|
||||
private lateinit var stores: SqliteStores
|
||||
private lateinit var stores: KsqliteStores
|
||||
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
stores = SqliteStores.inMemory()
|
||||
stores = KsqliteStores.inMemory("persist-${kotlin.random.Random.nextLong()}")
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
@@ -109,13 +110,13 @@ class PersistenceTest {
|
||||
),
|
||||
now = t0,
|
||||
)
|
||||
assertEquals(1, stores.messages.listAll("c1").size)
|
||||
assertEquals(1, stores.messages.listFlow("c1", Instant.DISTANT_PAST).toList().size)
|
||||
assertEquals(1, stores.workingMemory.list("c1").size)
|
||||
|
||||
val removed = stores.conversations.delete("c1")
|
||||
assertTrue(removed)
|
||||
assertNull(stores.conversations.get("c1"))
|
||||
assertEquals(emptyList(), stores.messages.listAll("c1"))
|
||||
assertEquals(emptyList(), stores.messages.listFlow("c1", Instant.DISTANT_PAST).toList())
|
||||
assertEquals(emptyList(), stores.workingMemory.list("c1"))
|
||||
}
|
||||
|
||||
@@ -129,7 +130,7 @@ class PersistenceTest {
|
||||
MessageRecord.AssistantMessage("m2", "c1", listOf(Content.Text("yo")), t0),
|
||||
)
|
||||
|
||||
val all = stores.messages.listAll("c1")
|
||||
val all = stores.messages.listFlow("c1", Instant.DISTANT_PAST).toList()
|
||||
assertEquals(2, all.size)
|
||||
assertEquals("m1", all[0].id)
|
||||
assertEquals("m2", all[1].id)
|
||||
@@ -284,7 +285,7 @@ class PersistenceTest {
|
||||
createdAt = Instant.fromEpochMilliseconds(1_700_000_001_000),
|
||||
),
|
||||
)
|
||||
val all = stores.messages.listAll("c1")
|
||||
val all = stores.messages.listFlow("c1", Instant.DISTANT_PAST).toList()
|
||||
assertEquals(2, all.size)
|
||||
val first = assertIs<MessageRecord.Error>(all[0])
|
||||
assertEquals("e1", first.id)
|
||||
@@ -305,7 +306,7 @@ class PersistenceTest {
|
||||
createdAt = t0,
|
||||
),
|
||||
)
|
||||
val all = stores.messages.listAll("c1")
|
||||
val all = stores.messages.listFlow("c1", Instant.DISTANT_PAST).toList()
|
||||
val image = (all[0] as MessageRecord.UserMessage).content[0] as Content.Image
|
||||
assertEquals("image/png", image.mime)
|
||||
assertTrue(bytes.contentEquals(image.data))
|
||||
@@ -328,7 +329,7 @@ class PersistenceTest {
|
||||
context = ctx,
|
||||
),
|
||||
)
|
||||
val all = stores.messages.listAll("c1")
|
||||
val all = stores.messages.listFlow("c1", Instant.DISTANT_PAST).toList()
|
||||
assertEquals(1, all.size)
|
||||
val user = assertIs<MessageRecord.UserMessage>(all[0])
|
||||
assertEquals(ctx, user.context)
|
||||
@@ -345,7 +346,7 @@ class PersistenceTest {
|
||||
createdAt = t0,
|
||||
),
|
||||
)
|
||||
val all = stores.messages.listAll("c1")
|
||||
val all = stores.messages.listFlow("c1", Instant.DISTANT_PAST).toList()
|
||||
val user = assertIs<MessageRecord.UserMessage>(all[0])
|
||||
assertNull(user.context)
|
||||
}
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
package pw.binom.agentik.standalone.persistence
|
||||
import pw.binom.agentik.messageStore.Reflection
|
||||
import pw.binom.agentik.messageStore.ConversationRecord
|
||||
import pw.binom.agentik.messageLog.MessageRecord
|
||||
import pw.binom.agentik.messageLog.Content
|
||||
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
import pw.binom.agentik.storage.sqlite.SqliteStores
|
||||
|
||||
class SqliteStoresMigrationTest {
|
||||
|
||||
@Test
|
||||
fun `reflection table is created on fresh inMemory database`() {
|
||||
val stores = SqliteStores.inMemory()
|
||||
try {
|
||||
// Если таблицы нет — insert упадёт. Проверяем insert round-trip.
|
||||
val r = Reflection(
|
||||
id = "m1",
|
||||
conversationId = null,
|
||||
createdAt = kotlin.time.Instant.parse("2026-09-15T12:00:00Z"),
|
||||
turnsAnalyzed = 3,
|
||||
score = 5,
|
||||
summary = "fresh",
|
||||
weakSpots = listOf("none"),
|
||||
)
|
||||
kotlinx.coroutines.runBlocking { stores.reflections.insert(r) }
|
||||
val loaded = kotlinx.coroutines.runBlocking { stores.reflections.get("m1") }
|
||||
assertNotNull(loaded)
|
||||
assertEquals("fresh", loaded.summary)
|
||||
} finally { stores.close() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all stores exposed`() {
|
||||
val stores = SqliteStores.inMemory()
|
||||
try {
|
||||
assertNotNull(stores.conversations)
|
||||
assertNotNull(stores.messages)
|
||||
assertNotNull(stores.workingMemory)
|
||||
assertNotNull(stores.reflections)
|
||||
assertTrue(stores.driver.toString().isNotBlank())
|
||||
} finally { stores.close() }
|
||||
}
|
||||
}
|
||||
-137
@@ -1,137 +0,0 @@
|
||||
package pw.binom.agentik.standalone.persistence
|
||||
import pw.binom.agentik.messageLog.TurnTokens
|
||||
import pw.binom.agentik.messageLog.MessageBodyPayload
|
||||
import pw.binom.agentik.messageLog.encodeBodyPayload
|
||||
import pw.binom.agentik.messageLog.decodeBodyPayload
|
||||
import pw.binom.agentik.messageLog.MessageContext
|
||||
import pw.binom.agentik.messageLog.MessageOrigin
|
||||
import pw.binom.agentik.messageStore.ConversationRecord
|
||||
import pw.binom.agentik.messageLog.MessageRecord
|
||||
import pw.binom.agentik.messageLog.Content
|
||||
import pw.binom.agentik.workingMemory.WorkingMemoryEntry
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.time.Instant
|
||||
import pw.binom.agentik.storage.sqlite.SqliteStores
|
||||
|
||||
class TokenStatsTest {
|
||||
|
||||
@Test
|
||||
fun `tokenStats sums across assistant messages of same conversation`() {
|
||||
val stores = SqliteStores.inMemory()
|
||||
try {
|
||||
kotlinx.coroutines.runBlocking {
|
||||
stores.conversations.upsert(
|
||||
ConversationRecord(
|
||||
id = "c1",
|
||||
title = null,
|
||||
isTemporal = false,
|
||||
createdAt = Instant.parse("2026-09-15T12:00:00Z"),
|
||||
updatedAt = Instant.parse("2026-09-15T12:00:00Z"),
|
||||
)
|
||||
)}
|
||||
kotlinx.coroutines.runBlocking {
|
||||
stores.messages.append(
|
||||
assistant("a1", "c1", 100, 50, Instant.parse("2026-09-15T12:01:00Z"))
|
||||
)
|
||||
stores.messages.append(
|
||||
assistant("a2", "c1", 200, 80, Instant.parse("2026-09-15T12:02:00Z"))
|
||||
)
|
||||
// User без tokens — не должны считаться.
|
||||
stores.messages.append(
|
||||
MessageRecord.UserMessage(
|
||||
id = "u1",
|
||||
conversationId = "c1",
|
||||
content = listOf(Content.Text("hi")),
|
||||
createdAt = Instant.parse("2026-09-15T12:00:30Z"),
|
||||
)
|
||||
)
|
||||
}
|
||||
val stats = kotlinx.coroutines.runBlocking { stores.messages.tokenStats("c1") }
|
||||
assertEquals(2, stats.turns)
|
||||
assertEquals(300L, stats.inputTokens)
|
||||
assertEquals(130L, stats.outputTokens)
|
||||
assertEquals(430L, stats.totalTokens)
|
||||
} finally { stores.close() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tokenStats skips legacy assistant messages without tokens field`() {
|
||||
val stores = SqliteStores.inMemory()
|
||||
try {
|
||||
// Вставляем запись со СТАРЫМ payload-форматом (plain array через
|
||||
// прямой SQL апдейт, минуя типизированный encoder).
|
||||
val id = "legacy"
|
||||
val msgId = "m1"
|
||||
stores.driver.execute(null, "INSERT INTO conversation(id, title, is_temporal, created_at, updated_at) VALUES('c1', NULL, 0, 0, 0)", 0)
|
||||
stores.driver.execute(
|
||||
null,
|
||||
"INSERT INTO message(id, conversation_id, kind, payload_json, created_at) VALUES(?, ?, ?, ?, ?)",
|
||||
5,
|
||||
) {
|
||||
bindString(0, msgId)
|
||||
bindString(1, "c1")
|
||||
bindString(2, "assistant")
|
||||
bindString(3, """[{"kind":"text","body":"legacy"}]""")
|
||||
bindLong(4, Instant.parse("2026-09-15T12:00:00Z").toEpochMilliseconds())
|
||||
}
|
||||
val stats = kotlinx.coroutines.runBlocking { stores.messages.tokenStats("c1") }
|
||||
assertEquals(0, stats.turns, "legacy без tokens не должен считаться")
|
||||
assertEquals(0L, stats.inputTokens)
|
||||
assertEquals(0L, stats.outputTokens)
|
||||
} finally { stores.close() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TurnTokens rejects negative values`() {
|
||||
val tokens = TurnTokens(input = 100, output = 50)
|
||||
assertEquals(100, tokens.input)
|
||||
assertEquals(150, tokens.total)
|
||||
// Sanity для init{}
|
||||
try {
|
||||
TurnTokens(input = -1, output = 50)
|
||||
error("should have thrown")
|
||||
} catch (_: IllegalArgumentException) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `backwards compatible encode-decode round trip preserves both context and tokens`() {
|
||||
val original = MessageBodyPayload(
|
||||
content = listOf(Content.Text("hi")),
|
||||
context = MessageContext(
|
||||
origin = MessageOrigin.USER,
|
||||
description = "x",
|
||||
),
|
||||
tokens = TurnTokens(input = 100, output = 50),
|
||||
)
|
||||
val json = kotlinx.serialization.json.Json.encodeToString(MessageBodyPayload.serializer(), original)
|
||||
val decoded = kotlinx.serialization.json.Json.decodeFromString(MessageBodyPayload.serializer(), json)
|
||||
assertEquals(100, decoded.tokens?.input)
|
||||
assertEquals(50, decoded.tokens?.output)
|
||||
assertEquals(1, decoded.content.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decodeBodyPayload round-trip preserves tokens`() {
|
||||
val tokens = TurnTokens(input = 250, output = 80)
|
||||
val encoded = encodeBodyPayload(
|
||||
content = listOf(Content.Text("ok")),
|
||||
tokens = tokens,
|
||||
)
|
||||
val decoded = decodeBodyPayload(encoded)
|
||||
assertEquals(250, decoded.tokens?.input)
|
||||
assertEquals(80, decoded.tokens?.output)
|
||||
}
|
||||
|
||||
private fun assistant(
|
||||
msgId: String, convId: String, input: Int, output: Int, at: Instant,
|
||||
) = MessageRecord.AssistantMessage(
|
||||
id = msgId,
|
||||
conversationId = convId,
|
||||
content = listOf(Content.Text("reply")),
|
||||
createdAt = at,
|
||||
tokens = TurnTokens(input = input, output = output),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user