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.
This commit is contained in:
2026-05-30 00:32:38 +04:00
parent f922b855eb
commit d276148e2c
4 changed files with 57 additions and 36 deletions
@@ -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<ChatMessage> {
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", "")) }
}
}
@@ -61,6 +61,7 @@ import com.correx.core.inference.InferenceRepository
import com.correx.core.inference.InferenceRequest import com.correx.core.inference.InferenceRequest
import com.correx.core.inference.InferenceResponse import com.correx.core.inference.InferenceResponse
import com.correx.core.inference.InferenceRouter import com.correx.core.inference.InferenceRouter
import com.correx.core.inference.PromptRenderer
import com.correx.core.inference.ResponseFormat import com.correx.core.inference.ResponseFormat
import com.correx.core.inference.Tokenizer import com.correx.core.inference.Tokenizer
import com.correx.core.inference.ToolCallRequest import com.correx.core.inference.ToolCallRequest
@@ -91,6 +92,7 @@ import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -688,8 +690,8 @@ abstract class SessionOrchestrator(
), ),
) )
// TODO: capture rendered prompt; currently contextpack.toString() val renderedMessages = PromptRenderer.render(contextPack)
val promptArtifactId = artifactStore.put(contextPack.toString().toByteArray()) val promptArtifactId = artifactStore.put(Json.encodeToString(renderedMessages).toByteArray())
emit(sessionId, InferenceStartedEvent(requestId, sessionId, stageId, provider.id, promptArtifactId)) emit(sessionId, InferenceStartedEvent(requestId, sessionId, stageId, provider.id, promptArtifactId))
if (isCancelled(sessionId)) return InferenceResult.Cancelled if (isCancelled(sessionId)) return InferenceResult.Cancelled
@@ -1,5 +1,6 @@
package com.correx.infrastructure.inference.llama.cpp package com.correx.infrastructure.inference.llama.cpp
import com.correx.core.inference.ChatMessage
import com.correx.core.inference.ToolCallRequest import com.correx.core.inference.ToolCallRequest
import com.correx.core.inference.ToolDefinition import com.correx.core.inference.ToolDefinition
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
@@ -8,7 +9,7 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class ChatCompletionRequest( data class ChatCompletionRequest(
val model: String, val model: String,
val messages: List<ChatMessage>, val messages: List<LlamaCppChatMessage>,
val temperature: Double, val temperature: Double,
@SerialName("top_p") val topP: Double, @SerialName("top_p") val topP: Double,
@SerialName("max_tokens") val maxTokens: Int, @SerialName("max_tokens") val maxTokens: Int,
@@ -20,7 +21,7 @@ data class ChatCompletionRequest(
) )
@Serializable @Serializable
data class ChatMessage( data class LlamaCppChatMessage(
val role: String, val role: String,
val content: String?, val content: String?,
@SerialName("tool_calls") val toolCalls: List<ToolCallRequest> = emptyList(), @SerialName("tool_calls") val toolCalls: List<ToolCallRequest> = emptyList(),
@@ -36,7 +37,7 @@ data class ChatCompletionResponse(
@Serializable @Serializable
data class Choice( data class Choice(
val message: ChatMessage, val message: LlamaCppChatMessage,
@SerialName("finish_reason") val finishReason: String, @SerialName("finish_reason") val finishReason: String,
) )
@@ -1,14 +1,12 @@
package com.correx.infrastructure.inference.llama.cpp 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.events.types.ProviderId
import com.correx.core.inference.CapabilityScore import com.correx.core.inference.CapabilityScore
import com.correx.core.inference.FinishReason import com.correx.core.inference.FinishReason
import com.correx.core.inference.InferenceProvider import com.correx.core.inference.InferenceProvider
import com.correx.core.inference.InferenceRequest import com.correx.core.inference.InferenceRequest
import com.correx.core.inference.InferenceResponse import com.correx.core.inference.InferenceResponse
import com.correx.core.inference.PromptRenderer
import com.correx.core.inference.ProviderHealth import com.correx.core.inference.ProviderHealth
import com.correx.core.inference.ResponseFormat import com.correx.core.inference.ResponseFormat
import com.correx.core.inference.TokenUsage import com.correx.core.inference.TokenUsage
@@ -79,7 +77,13 @@ class LlamaCppInferenceProvider(
override suspend fun infer(request: InferenceRequest): InferenceResponse { override suspend fun infer(request: InferenceRequest): InferenceResponse {
val startTime = System.currentTimeMillis() 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) { val grammar = when (val fmt = request.responseFormat) {
is ResponseFormat.Json -> GbnfGrammarConverter.convert(fmt.schema) is ResponseFormat.Json -> GbnfGrammarConverter.convert(fmt.schema)
@@ -142,31 +146,4 @@ class LlamaCppInferenceProvider(
} }
override fun capabilities(): Set<CapabilityScore> = descriptor.capabilities override fun capabilities(): Set<CapabilityScore> = descriptor.capabilities
private fun buildMessages(contextPack: ContextPack): List<ChatMessage> {
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", "")) }
}
} }