From d276148e2cc90c81e9281f67bb3bee3b29343c39 Mon Sep 17 00:00:00 2001 From: kami Date: Sat, 30 May 2026 00:32:38 +0400 Subject: [PATCH] fix: store actual rendered prompt artifact instead of contextpack.toString() Extract message-rendering logic from LlamaCppInferenceProvider into a provider-agnostic PromptRenderer in core:inference. SessionOrchestrator now serializes the rendered ChatMessage list as JSON for the prompt artifact, so the audit trail reflects what the LLM actually saw. Resolves the TODO at SessionOrchestrator.kt:691. --- .../correx/core/inference/PromptRenderer.kt | 41 +++++++++++++++++++ .../orchestration/SessionOrchestrator.kt | 6 ++- .../inference/llama/cpp/LlamaCppApiModels.kt | 7 ++-- .../llama/cpp/LlamaCppInferenceProvider.kt | 39 ++++-------------- 4 files changed, 57 insertions(+), 36 deletions(-) create mode 100644 core/inference/src/main/kotlin/com/correx/core/inference/PromptRenderer.kt diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/PromptRenderer.kt b/core/inference/src/main/kotlin/com/correx/core/inference/PromptRenderer.kt new file mode 100644 index 00000000..dab2275b --- /dev/null +++ b/core/inference/src/main/kotlin/com/correx/core/inference/PromptRenderer.kt @@ -0,0 +1,41 @@ +package com.correx.core.inference + +import com.correx.core.context.model.ContextLayer +import com.correx.core.context.model.ContextPack +import com.correx.core.context.model.EntryRole +import kotlinx.serialization.Serializable + +@Serializable +data class ChatMessage( + val role: String, + val content: String, +) + +object PromptRenderer { + fun render(contextPack: ContextPack): List { + val sorted = contextPack.layers.entries.sortedBy { it.key.ordinal } + val systemContent = sorted + .filter { it.key == ContextLayer.L0 } + .flatMap { it.value } + .joinToString("\n\n") { it.content } + .takeIf { it.isNotBlank() } + val conversationMessages = sorted.filter { it.key != ContextLayer.L0 }.flatMap { (_, entries) -> + entries.map { entry -> + ChatMessage( + role = when (entry.role) { + EntryRole.SYSTEM -> "system" + EntryRole.ASSISTANT -> "assistant" + EntryRole.TOOL -> "tool" + EntryRole.USER -> "user" + }, + content = entry.content, + ) + } + } + val messages = buildList { + systemContent?.let { add(ChatMessage("system", it)) } + addAll(conversationMessages) + } + return messages.ifEmpty { listOf(ChatMessage("user", "")) } + } +} diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt index 8b8d06c6..6db8decc 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt @@ -61,6 +61,7 @@ import com.correx.core.inference.InferenceRepository import com.correx.core.inference.InferenceRequest import com.correx.core.inference.InferenceResponse import com.correx.core.inference.InferenceRouter +import com.correx.core.inference.PromptRenderer import com.correx.core.inference.ResponseFormat import com.correx.core.inference.Tokenizer import com.correx.core.inference.ToolCallRequest @@ -91,6 +92,7 @@ import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import kotlinx.datetime.Clock +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import org.slf4j.LoggerFactory @@ -688,8 +690,8 @@ abstract class SessionOrchestrator( ), ) - // TODO: capture rendered prompt; currently contextpack.toString() - val promptArtifactId = artifactStore.put(contextPack.toString().toByteArray()) + val renderedMessages = PromptRenderer.render(contextPack) + val promptArtifactId = artifactStore.put(Json.encodeToString(renderedMessages).toByteArray()) emit(sessionId, InferenceStartedEvent(requestId, sessionId, stageId, provider.id, promptArtifactId)) if (isCancelled(sessionId)) return InferenceResult.Cancelled diff --git a/infrastructure/inference/llama_cpp/src/main/kotlin/com/correx/infrastructure/inference/llama/cpp/LlamaCppApiModels.kt b/infrastructure/inference/llama_cpp/src/main/kotlin/com/correx/infrastructure/inference/llama/cpp/LlamaCppApiModels.kt index 65c86897..b970ee38 100644 --- a/infrastructure/inference/llama_cpp/src/main/kotlin/com/correx/infrastructure/inference/llama/cpp/LlamaCppApiModels.kt +++ b/infrastructure/inference/llama_cpp/src/main/kotlin/com/correx/infrastructure/inference/llama/cpp/LlamaCppApiModels.kt @@ -1,5 +1,6 @@ package com.correx.infrastructure.inference.llama.cpp +import com.correx.core.inference.ChatMessage import com.correx.core.inference.ToolCallRequest import com.correx.core.inference.ToolDefinition import kotlinx.serialization.SerialName @@ -8,7 +9,7 @@ import kotlinx.serialization.Serializable @Serializable data class ChatCompletionRequest( val model: String, - val messages: List, + val messages: List, val temperature: Double, @SerialName("top_p") val topP: Double, @SerialName("max_tokens") val maxTokens: Int, @@ -20,7 +21,7 @@ data class ChatCompletionRequest( ) @Serializable -data class ChatMessage( +data class LlamaCppChatMessage( val role: String, val content: String?, @SerialName("tool_calls") val toolCalls: List = emptyList(), @@ -36,7 +37,7 @@ data class ChatCompletionResponse( @Serializable data class Choice( - val message: ChatMessage, + val message: LlamaCppChatMessage, @SerialName("finish_reason") val finishReason: String, ) diff --git a/infrastructure/inference/llama_cpp/src/main/kotlin/com/correx/infrastructure/inference/llama/cpp/LlamaCppInferenceProvider.kt b/infrastructure/inference/llama_cpp/src/main/kotlin/com/correx/infrastructure/inference/llama/cpp/LlamaCppInferenceProvider.kt index e52984be..54251a93 100644 --- a/infrastructure/inference/llama_cpp/src/main/kotlin/com/correx/infrastructure/inference/llama/cpp/LlamaCppInferenceProvider.kt +++ b/infrastructure/inference/llama_cpp/src/main/kotlin/com/correx/infrastructure/inference/llama/cpp/LlamaCppInferenceProvider.kt @@ -1,14 +1,12 @@ package com.correx.infrastructure.inference.llama.cpp -import com.correx.core.context.model.ContextLayer -import com.correx.core.context.model.ContextPack -import com.correx.core.context.model.EntryRole import com.correx.core.events.types.ProviderId import com.correx.core.inference.CapabilityScore import com.correx.core.inference.FinishReason import com.correx.core.inference.InferenceProvider import com.correx.core.inference.InferenceRequest import com.correx.core.inference.InferenceResponse +import com.correx.core.inference.PromptRenderer import com.correx.core.inference.ProviderHealth import com.correx.core.inference.ResponseFormat import com.correx.core.inference.TokenUsage @@ -79,7 +77,13 @@ class LlamaCppInferenceProvider( override suspend fun infer(request: InferenceRequest): InferenceResponse { val startTime = System.currentTimeMillis() - val messages = buildMessages(request.contextPack) + val baseMessages = PromptRenderer.render(request.contextPack) + val messages = baseMessages.map { msg -> + LlamaCppChatMessage( + role = msg.role, + content = msg.content, + ) + } val grammar = when (val fmt = request.responseFormat) { is ResponseFormat.Json -> GbnfGrammarConverter.convert(fmt.schema) @@ -142,31 +146,4 @@ class LlamaCppInferenceProvider( } override fun capabilities(): Set = descriptor.capabilities - - private fun buildMessages(contextPack: ContextPack): List { - val sorted = contextPack.layers.entries.sortedBy { it.key.ordinal } - val systemContent = sorted - .filter { it.key == ContextLayer.L0 } - .flatMap { it.value } - .joinToString("\n\n") { it.content } - .takeIf { it.isNotBlank() } - val conversationMessages = sorted.filter { it.key != ContextLayer.L0 }.flatMap { (_, entries) -> - entries.map { entry -> - ChatMessage( - role = when (entry.role) { - EntryRole.SYSTEM -> "system" - EntryRole.ASSISTANT -> "assistant" - EntryRole.TOOL -> "tool" - EntryRole.USER -> "user" - }, - content = entry.content, - ) - } - } - val messages = buildList { - systemContent?.let { add(ChatMessage("system", it)) } - addAll(conversationMessages) - } - return messages.ifEmpty { listOf(ChatMessage("user", "")) } - } } \ No newline at end of file