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.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
@@ -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<ChatMessage>,
val messages: List<LlamaCppChatMessage>,
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<ToolCallRequest> = emptyList(),
@@ -36,7 +37,7 @@ data class ChatCompletionResponse(
@Serializable
data class Choice(
val message: ChatMessage,
val message: LlamaCppChatMessage,
@SerialName("finish_reason") val finishReason: String,
)
@@ -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<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", "")) }
}
}