diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 047ee14..add7f14 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,8 @@ sqldelight = "2.3.2" shadow = "8.3.5" jvector = "3.0.6" text-embedding-kmp = "3.0.0-SNAPSHOT" +kotlin-logging = "3.0.5" +logback = "1.5.18" [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } @@ -73,3 +75,7 @@ jvector = { module = "io.github.jbellis:jvector", version.ref = "jvector" } # Артефакты публикуются под именами `-jvm` (KMP convention для JVM-таргета). text-embedding-api = { module = "pw.binom.ai.embeddingtext:api-jvm", version.ref = "text-embedding-kmp" } text-embedding-siglip = { module = "pw.binom.ai.embeddingtext:siglip-jvm", version.ref = "text-embedding-kmp" } + +# --- Логирование: kotlin-logging (тонкая обёртка над slf4j-api) + logback-classic (binding). --- +kotlin-logging = { module = "io.github.microutils:kotlin-logging-jvm", version.ref = "kotlin-logging" } +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } diff --git a/standalone/build.gradle.kts b/standalone/build.gradle.kts index e8f05c9..8d18a79 100644 --- a/standalone/build.gradle.kts +++ b/standalone/build.gradle.kts @@ -77,6 +77,11 @@ kotlin { implementation(libs.ktor.client.cio) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) + + // Логирование: kotlin-logging (тонкая обёртка slf4j-api) + logback-classic + // (binding для JVM; без него slf4j-api NOP-логирует и не падает). + implementation(libs.kotlin.logging) + implementation(libs.logback.classic) } commonTest.dependencies { diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/Main.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/Main.kt index a8eade3..b21a692 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/Main.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/Main.kt @@ -1,5 +1,8 @@ package pw.binom.agentik.standalone +import mu.KotlinLogging + + import io.ktor.server.cio.CIO import io.ktor.server.engine.embeddedServer import io.ktor.server.response.respondText @@ -47,6 +50,7 @@ import java.io.File * - Skills: AGENTIK_SKILLS_DIR= (папка с SKILL.md / *.yaml) * - AGENTIK_SYSTEM_PROMPT (default: встроенный `Ты полезный ассистент...`) */ +private val log = KotlinLogging.logger {} fun main() { val config = AgentikConfig.fromEnv() @@ -55,7 +59,7 @@ fun main() { val mcpRegistry = McpRegistry.fromConfig(config.mcp) val skills = config.skillsDir?.let { dir -> val result = SkillLoader.loadDirectory(File(dir)) - result.errors.forEach { System.err.println("[agentik] skill '${it.path}': ${it.message}") } + result.errors.forEach { log.warn { "skill '${it.path}': ${it.message}" } } result.catalog } ?: SkillCatalog.EMPTY @@ -65,7 +69,7 @@ fun main() { val soulBody = config.soulPath?.let { path -> val file = File(path) if (!file.exists() || !file.isFile) { - System.err.println("[agentik] SOUL file not found: $path") + log.warn { "SOUL file not found: $path" } null } else { file.readText(Charsets.UTF_8) diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ChatConversation.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ChatConversation.kt index 1fe0cae..087c98d 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ChatConversation.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/ChatConversation.kt @@ -1,5 +1,7 @@ package pw.binom.agentik.standalone.agent +import mu.KotlinLogging + import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -223,7 +225,7 @@ class ChatConversation( when (c) { is Content.Text -> LiteContentPart.Text(c.body) is Content.Image -> { - System.err.println("[agentik] dropping image input (v1 text-only): mime=${c.mime}, ${c.data.size} bytes") + log.warn { "dropping image input (v1 text-only): mime=${c.mime}, ${c.data.size} bytes" } null } } @@ -288,7 +290,7 @@ class ChatConversation( } if (loopGuard >= MAX_TOOL_LOOPS) { - System.err.println("[agentik] tool loop hit MAX_TOOL_LOOPS=$MAX_TOOL_LOOPS for $id — bailing") + log.warn { "tool loop hit MAX_TOOL_LOOPS=$MAX_TOOL_LOOPS for $id — bailing" } } val assistantId = newId("msg") @@ -337,7 +339,7 @@ class ChatConversation( val notes = try { prefetcher.prefetch(userText, topK = 10) } catch (e: Throwable) { - System.err.println("[agentik] memory prefetch failed: ${e.message}") + log.warn(e) { "memory prefetch failed: ${e.message}" } return null } if (notes.isEmpty()) return null @@ -427,7 +429,7 @@ class ChatConversation( } if (pendingUser != null) paired.add(pendingUser) if (paired.isEmpty()) { - System.err.println("[agentik] compactPreTurn: nothing to compact for $id") + log.info { "compactPreTurn: nothing to compact for $id" } return } @@ -436,7 +438,7 @@ class ChatConversation( } catch (e: kotlinx.coroutines.CancellationException) { throw e } catch (e: Throwable) { - System.err.println("[agentik] context summarization failed for $id: ${e.message}") + log.warn(e) { "context summarization failed for $id: ${e.message}" } return } if (summaryText.isBlank()) return @@ -457,16 +459,16 @@ class ChatConversation( for (n in decision.toSave) { val note = materializeReviewNote(n, conversationId = null) runCatching { store.upsert(note) } - .onFailure { System.err.println("[agentik] pre-compaction upsert failed: ${it.message}") } + .onFailure { log.warn(it) { "pre-compaction upsert failed: ${it.message}" } } } for (delId in decision.toDelete) { runCatching { store.delete(delId) } - .onFailure { System.err.println("[agentik] pre-compaction delete failed: ${it.message}") } + .onFailure { log.warn(it) { "pre-compaction delete failed: ${it.message}" } } } } catch (e: kotlinx.coroutines.CancellationException) { throw e } catch (e: Throwable) { - System.err.println("[agentik] pre-compaction review failed for $id: ${e.message}") + log.warn(e) { "pre-compaction review failed for $id: ${e.message}" } } } @@ -485,7 +487,7 @@ class ChatConversation( toolsChars = toolsChars, ) if (after.toDouble() / window >= compressionThreshold) { - System.err.println("[agentik] compactPreTurn: still over threshold for $id (estimated=$after, window=$window, threshold=$compressionThreshold). Consider raising contextWindow or lowering threshold.") + log.warn { "compactPreTurn: still over threshold for $id (estimated=$after, window=$window, threshold=$compressionThreshold). Consider raising contextWindow or lowering threshold." } } } @@ -543,14 +545,14 @@ class ChatConversation( for (n in decision.toSave) { val note = materializeReviewNote(n, conversationId = null) runCatching { store.upsert(note) } - .onFailure { System.err.println("[agentik] review upsert failed: ${it.message}") } + .onFailure { log.warn(it) { "review upsert failed: ${it.message}" } } } for (id in decision.toDelete) { runCatching { store.delete(id) } - .onFailure { System.err.println("[agentik] review delete failed: ${it.message}") } + .onFailure { log.warn(it) { "review delete failed: ${it.message}" } } } } catch (e: Throwable) { - System.err.println("[agentik] review failed for $convId: ${e.message}") + log.warn(e) { "review failed for $convId: ${e.message}" } } } } @@ -582,13 +584,13 @@ class ChatConversation( val tool = toolsByName[call.name] val resultText: String = if (tool == null) { - System.err.println("[agentik] tool '${call.name}' requested but not registered") + log.warn { "tool '${call.name}' requested but not registered" } "[tool not found: ${call.name}]" } else { try { tool.tool.invoke(argsJson).ifBlank { "" } } catch (e: Throwable) { - System.err.println("[agentik] tool '${call.name}' threw: ${e.message}") + log.warn(e) { "tool '${call.name}' threw: ${e.message}" } "[tool error: ${e.message ?: e.javaClass.simpleName}]" } } @@ -707,6 +709,8 @@ class ChatConversation( } companion object { + private val log = KotlinLogging.logger {} + private const val MAX_TOOL_LOOPS = 16 /** Сколько последних ходов оставляем нетронутыми при compaction. */ private const val KEEP_RECENT_TURNS = 4 diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/memory/Curator.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/memory/Curator.kt index a9ef444..ee6bbfb 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/memory/Curator.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/agent/memory/Curator.kt @@ -1,5 +1,7 @@ package pw.binom.agentik.standalone.agent.memory +import mu.KotlinLogging + import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -32,12 +34,13 @@ class Curator( private val clock: Clock = Clock.System, private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO), ) { + private val log = KotlinLogging.logger {} private var job: Job? = null fun start() { if (job != null) return job = scope.launch { - println("[Curator] started (interval=$interval, maxAge=$maxAge, maxUseCount=$maxUseCount)") + log.info { "started (interval=$interval, maxAge=$maxAge, maxUseCount=$maxUseCount)" } while (isActive) { runPass() delay(interval) @@ -53,7 +56,7 @@ class Curator( now = clock.now(), ) if (archived > 0) { - println("[Curator] archived $archived stale notes (maxAge=$maxAge, maxUseCount=$maxUseCount)") + log.info { "archived $archived stale notes (maxAge=$maxAge, maxUseCount=$maxUseCount)" } } return archived } diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/mcp/McpConfig.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/mcp/McpConfig.kt index 2e56fa9..07b8cb1 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/mcp/McpConfig.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/mcp/McpConfig.kt @@ -1,5 +1,6 @@ package pw.binom.agentik.standalone.mcp + import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -57,13 +58,14 @@ data class McpConfig( val isEmpty: Boolean get() = servers.isEmpty() companion object { + private val log = mu.KotlinLogging.logger {} private val json = Json { ignoreUnknownKeys = true } fun fromEnv(env: (String) -> String? = System::getenv): McpConfig { val path = env("AGENTIK_MCP_CONFIG")?.takeIf { it.isNotBlank() } ?: return empty() val file = File(path) if (!file.exists()) { - System.err.println("[agentik] AGENTIK_MCP_CONFIG points to missing file: $path") + log.warn { "AGENTIK_MCP_CONFIG points to missing file: $path" } return empty() } return fromJson(file.readText()) @@ -96,7 +98,7 @@ data class McpConfig( ?: emptyMap() return McpServerSpec.Stdio(name = name, command = command, args = args, env = env) } - System.err.println("[agentik] MCP server '$name' has neither 'url' nor 'command' — skipped") + log.warn { "MCP server '$name' has neither 'url' nor 'command' — skipped" } return null } } diff --git a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/mcp/McpRegistry.kt b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/mcp/McpRegistry.kt index 1fde3b4..aecbaad 100644 --- a/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/mcp/McpRegistry.kt +++ b/standalone/src/jvmMain/kotlin/pw/binom/agentik/standalone/mcp/McpRegistry.kt @@ -1,5 +1,7 @@ package pw.binom.agentik.standalone.mcp +import mu.KotlinLogging + import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.defaultRequest @@ -42,6 +44,7 @@ import java.util.concurrent.ConcurrentHashMap * * [close] убивает stdio-процессы и закрывает HTTP-клиент. */ +private val log = KotlinLogging.logger {} class McpRegistry( private val servers: List, private val httpClient: HttpClient = defaultHttpClient(), @@ -72,10 +75,10 @@ class McpRegistry( val server = connectOne(spec) connected[spec.name] = server }.onFailure { e -> - System.err.println("[agentik] MCP server '${spec.name}' failed to connect: ${e.message}") + log.warn { "MCP server '${spec.name}' failed to connect: ${e.message}" } } } - System.err.println("[agentik] MCP registry: ${connected.size}/${servers.size} servers connected, ${allTools.size} tools total") + log.warn { "MCP registry: ${connected.size}/${servers.size} servers connected, ${allTools.size} tools total" } } } @@ -84,7 +87,7 @@ class McpRegistry( val transport: Transport = when (spec) { is McpServerSpec.Stdio -> { val cmd = (listOf(spec.command) + spec.args).joinToString(" ") - System.err.println("[agentik] MCP stdio '$spec.name': $cmd") + log.warn { "MCP stdio '$spec.name': $cmd" } val pb = ProcessBuilder(buildList { add(spec.command); addAll(spec.args) }) .redirectErrorStream(false) spec.env.forEach { (k, v) -> pb.environment()[k] = v } @@ -97,7 +100,7 @@ class McpRegistry( ) } is McpServerSpec.Http -> { - System.err.println("[agentik] MCP http '$spec.name': ${spec.url}") + log.warn { "MCP http '$spec.name': ${spec.url}" } StreamableHttpClientTransport( client = httpClient.config { if (spec.headers.isNotEmpty()) { @@ -230,7 +233,7 @@ internal class McpLiteToolAdapter( val parsed = json.parseToJsonElement(raw) if (parsed !is JsonObject) emptyMap() else parsed.toAnyMap() } catch (e: Throwable) { - System.err.println("[agentik] MCP tool '$toolName' got invalid args JSON: ${e.message}") + log.warn { "MCP tool '$toolName' got invalid args JSON: ${e.message}" } emptyMap() } } diff --git a/standalone/src/jvmMain/resources/logback.xml b/standalone/src/jvmMain/resources/logback.xml new file mode 100644 index 0000000..b213be7 --- /dev/null +++ b/standalone/src/jvmMain/resources/logback.xml @@ -0,0 +1,23 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n + + + + + + + + + + + + +