diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt b/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt index 0f8c5954..1140358e 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt @@ -14,10 +14,12 @@ import com.correx.core.config.CorrexConfig import com.correx.core.config.ProviderConfig import com.correx.core.context.builder.DefaultContextPackBuilder import com.correx.core.context.compression.DefaultContextCompressor +import com.correx.core.context.model.TokenBudget import com.correx.core.events.EventDispatcher import com.correx.core.events.stores.EventStore import com.correx.core.inference.CapabilityScore import com.correx.core.inference.DefaultInferenceRouter +import com.correx.core.inference.GenerationConfig import com.correx.core.inference.InferenceProjector import com.correx.core.inference.InferenceRepository import com.correx.core.inference.InferenceRouter @@ -246,10 +248,26 @@ fun main() { // persisted vectors survive restart but the metadata map does not (no-op for // non-rehydratable backends like in_memory). runBlocking { L3MetadataRehydrator(eventStore).rehydrate(l3MemoryStore) } + // Map the config-file [router] block onto the domain RouterConfig the facade uses. + val domainRouterConfig = RouterConfig( + conversationKeepLast = routerConfig.conversationKeepLast, + retrievalK = routerConfig.retrievalK, + tokenBudget = TokenBudget(limit = routerConfig.tokenBudget), + generationConfig = GenerationConfig( + temperature = routerConfig.generation.temperature, + topP = routerConfig.generation.topP, + maxTokens = routerConfig.generation.maxTokens, + ), + narrationGenerationConfig = GenerationConfig( + temperature = routerConfig.narration.temperature, + topP = routerConfig.narration.topP, + maxTokens = routerConfig.narration.maxTokens, + ), + ) val routerFacade = InfrastructureModule.createRouterFacade( eventStore = eventStore, inferenceRouter = inferenceRouter, - config = RouterConfig(), + config = domainRouterConfig, tokenizer = firstProvider.tokenizer, embedder = embedder, l3MemoryStore = l3MemoryStore, @@ -289,6 +307,7 @@ fun main() { modelSwapper = modelSwapper, resourceProbe = resourceProbe, workspaceResolver = workspaceResolver, + narrationMaxPerRun = routerConfig.narration.maxPerRun, ) module.start() log.info("==============================") diff --git a/core/config/src/main/kotlin/com/correx/core/config/ConfigLoader.kt b/core/config/src/main/kotlin/com/correx/core/config/ConfigLoader.kt index e2fbfa8d..b7175d5c 100644 --- a/core/config/src/main/kotlin/com/correx/core/config/ConfigLoader.kt +++ b/core/config/src/main/kotlin/com/correx/core/config/ConfigLoader.kt @@ -233,8 +233,11 @@ object ConfigLoader { val toolsFileReadSection = sections["tools.file_read"] ?: emptyMap() val toolsFileWriteSection = sections["tools.file_write"] ?: emptyMap() val toolsFileEditSection = sections["tools.file_edit"] ?: emptyMap() + val routerSection = sections["router"] ?: emptyMap() val routerEmbedderSection = sections["router.embedder"] ?: emptyMap() val routerL3Section = sections["router.l3"] ?: emptyMap() + val routerGenerationSection = sections["router.generation"] ?: emptyMap() + val routerNarrationSection = sections["router.narration"] ?: emptyMap() val modelsSection = sections["models"] ?: emptyMap() val server = ServerConfig( @@ -368,9 +371,27 @@ object ConfigLoader { bitWidth = asInt(routerL3Section["bit_width"], 4), ) + val generation = GenerationSettings( + temperature = asDouble(routerGenerationSection["temperature"], 0.7), + topP = asDouble(routerGenerationSection["top_p"], 0.9), + maxTokens = asInt(routerGenerationSection["max_tokens"], 512), + ) + + val narration = NarrationSettings( + temperature = asDouble(routerNarrationSection["temperature"], 0.7), + topP = asDouble(routerNarrationSection["top_p"], 0.9), + maxTokens = asInt(routerNarrationSection["max_tokens"], 1024), + maxPerRun = asInt(routerNarrationSection["max_per_run"], 100), + ) + val router = RouterConfig( embedder = embedder, l3 = l3, + conversationKeepLast = asInt(routerSection["conversation_keep_last"], 6), + retrievalK = asInt(routerSection["retrieval_k"], 5), + tokenBudget = asInt(routerSection["token_budget"], 4096), + generation = generation, + narration = narration, ) val models = modelsList.mapNotNull { modelMap -> @@ -457,6 +478,16 @@ object ConfigLoader { } } + private fun asDouble(value: Any?, default: Double = 0.0): Double { + return when (value) { + is Double -> value + is Int -> value.toDouble() + is Long -> value.toDouble() + is String -> value.toDoubleOrNull() ?: default + else -> default + } + } + private fun asBoolean(value: Any?, default: Boolean = false): Boolean { return when (value) { is Boolean -> value diff --git a/core/config/src/main/kotlin/com/correx/core/config/CorrexConfig.kt b/core/config/src/main/kotlin/com/correx/core/config/CorrexConfig.kt index fa1eb6b1..b54ddc26 100644 --- a/core/config/src/main/kotlin/com/correx/core/config/CorrexConfig.kt +++ b/core/config/src/main/kotlin/com/correx/core/config/CorrexConfig.kt @@ -89,6 +89,29 @@ data class ProviderConfig( data class RouterConfig( val embedder: EmbedderConfig = EmbedderConfig(), val l3: L3Config = L3Config(), + val conversationKeepLast: Int = 6, + val retrievalK: Int = 5, + val tokenBudget: Int = 4096, + val generation: GenerationSettings = GenerationSettings(), + val narration: NarrationSettings = NarrationSettings(), +) + +/** Sampling/length knobs for router chat + steering replies. */ +@Serializable +data class GenerationSettings( + val temperature: Double = 0.7, + val topP: Double = 0.9, + val maxTokens: Int = 512, +) + +/** Router narration knobs. Larger maxTokens than chat so reasoning models can + * finish "thinking" and still emit the line; maxPerRun caps narrations per run. */ +@Serializable +data class NarrationSettings( + val temperature: Double = 0.7, + val topP: Double = 0.9, + val maxTokens: Int = 1024, + val maxPerRun: Int = 100, ) @Serializable diff --git a/core/config/src/test/kotlin/com/correx/core/config/ConfigLoaderTest.kt b/core/config/src/test/kotlin/com/correx/core/config/ConfigLoaderTest.kt index 0a58c989..0d2070ff 100644 --- a/core/config/src/test/kotlin/com/correx/core/config/ConfigLoaderTest.kt +++ b/core/config/src/test/kotlin/com/correx/core/config/ConfigLoaderTest.kt @@ -415,4 +415,51 @@ class ConfigLoaderTest { assertEquals("/home/user/projects", result.tools.allowedWorkspaceRoots[0]) assertEquals("/tmp/work", result.tools.allowedWorkspaceRoots[1]) } + + @Test + fun `parseToml parses router generation and narration knobs`() { + val toml = """ + [router] + conversation_keep_last = 8 + retrieval_k = 3 + token_budget = 8192 + + [router.generation] + temperature = 0.4 + top_p = 0.8 + max_tokens = 600 + + [router.narration] + temperature = 0.55 + top_p = 0.95 + max_tokens = 2048 + max_per_run = 25 + """.trimIndent() + + val parseTomlMethod = ConfigLoader::class.java.getDeclaredMethod("parseToml", String::class.java) + parseTomlMethod.isAccessible = true + val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig + + assertEquals(8, result.router.conversationKeepLast) + assertEquals(3, result.router.retrievalK) + assertEquals(8192, result.router.tokenBudget) + assertEquals(0.4, result.router.generation.temperature) + assertEquals(0.8, result.router.generation.topP) + assertEquals(600, result.router.generation.maxTokens) + assertEquals(0.55, result.router.narration.temperature) + assertEquals(2048, result.router.narration.maxTokens) + assertEquals(25, result.router.narration.maxPerRun) + } + + @Test + fun `parseToml uses router defaults when sections absent`() { + val parseTomlMethod = ConfigLoader::class.java.getDeclaredMethod("parseToml", String::class.java) + parseTomlMethod.isAccessible = true + val result = parseTomlMethod.invoke(ConfigLoader, "[server]\nhost = \"x\"") as CorrexConfig + + assertEquals(6, result.router.conversationKeepLast) + assertEquals(512, result.router.generation.maxTokens) + assertEquals(1024, result.router.narration.maxTokens) + assertEquals(100, result.router.narration.maxPerRun) + } } diff --git a/docs/sample-config.toml b/docs/sample-config.toml index a7e549e7..bcc440a9 100644 --- a/docs/sample-config.toml +++ b/docs/sample-config.toml @@ -74,6 +74,26 @@ capabilities = { General = 1.0, Coding = 0.7, Reasoning = 0.6, Summarization = 0 # capabilities = { General = 1.0, Coding = 0.7, Reasoning = 0.6, Summarization = 0.8, ToolCalling = 0.5 } # Router configuration (optional, defaults shown below) +[router] +conversation_keep_last = 6 # how many recent chat turns to keep in context +retrieval_k = 5 # L3 memory hits to retrieve per query +token_budget = 4096 # context budget for router prompts + +# Sampling/length for router chat + steering replies. +[router.generation] +temperature = 0.7 +top_p = 0.9 +max_tokens = 512 + +# Router narration (the live workflow commentary). max_tokens is higher than chat so +# reasoning models can finish "thinking" and still emit the line; max_per_run caps how +# many narrations fire per workflow run. +[router.narration] +temperature = 0.7 +top_p = 0.9 +max_tokens = 1024 +max_per_run = 100 + [router.embedder] backend = "noop" # or "llamacpp" dimension = 1536