fix(inference): fold non-L0 SYSTEM entries into the leading system message
Strict chat templates (Qwen3.5) reject any system message after index 0. Recalled L3 memory, L2 stage summaries, and retrieval entries carry EntryRole.SYSTEM and rendered as standalone mid-conversation system turns, making llama-server 400 on every router turn once L3 recall returned hits.
This commit is contained in:
@@ -23,15 +23,18 @@ object PromptRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun render(contextPack: ContextPack): List<ChatMessage> {
|
fun render(contextPack: ContextPack): List<ChatMessage> {
|
||||||
val sorted = contextPack.layers.entries.sortedBy { it.key.ordinal }
|
// Every SYSTEM-role entry folds into the single leading system message, whatever its
|
||||||
val systemContent = sorted
|
// layer (L0 additionally folds regardless of role). Strict chat templates (e.g. Qwen)
|
||||||
.filter { it.key == ContextLayer.L0 }
|
// reject any system message that is not the first message, so recalled memory, stage
|
||||||
.flatMap { it.value }
|
// summaries, and retrieval entries must never render as standalone system turns.
|
||||||
.joinToString("\n\n") { it.content }
|
val (systemEntries, conversationEntries) = contextPack.layers.entries
|
||||||
.takeIf { it.isNotBlank() }
|
|
||||||
val conversationMessages = contextPack.layers.entries
|
|
||||||
.filter { it.key != ContextLayer.L0 }
|
|
||||||
.flatMap { (layer, entries) -> entries.map { layer to it } }
|
.flatMap { (layer, entries) -> entries.map { layer to it } }
|
||||||
|
.partition { (layer, entry) -> layer == ContextLayer.L0 || entry.role == EntryRole.SYSTEM }
|
||||||
|
val systemContent = systemEntries
|
||||||
|
.sortedWith(compareBy({ it.first.ordinal }, { it.second.ordinal }))
|
||||||
|
.joinToString("\n\n") { it.second.content }
|
||||||
|
.takeIf { it.isNotBlank() }
|
||||||
|
val conversationMessages = conversationEntries
|
||||||
.sortedWith(compareBy({ it.second.ordinal }, { layerPriority(it.first) }))
|
.sortedWith(compareBy({ it.second.ordinal }, { layerPriority(it.first) }))
|
||||||
.map { (_, entry) -> entry.toChatMessage() }
|
.map { (_, entry) -> entry.toChatMessage() }
|
||||||
val messages = buildList {
|
val messages = buildList {
|
||||||
|
|||||||
@@ -67,6 +67,29 @@ class PromptRendererOrderingTest {
|
|||||||
budgetLimit = 4000,
|
budgetLimit = 4000,
|
||||||
)
|
)
|
||||||
val messages = PromptRenderer.render(pack)
|
val messages = PromptRenderer.render(pack)
|
||||||
assertEquals(listOf("memory", "question"), messages.map { it.content })
|
assertEquals(listOf("system" to "memory", "user" to "question"), messages.map { it.role to it.content })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `non-L0 SYSTEM entries fold into the single leading system message`() {
|
||||||
|
// Strict chat templates (Qwen) 400 on any system message after index 0. Recalled L3
|
||||||
|
// memory and L2 summaries carry EntryRole.SYSTEM and must merge into the system block.
|
||||||
|
val pack = ContextPack(
|
||||||
|
id = ContextPackId("p"),
|
||||||
|
sessionId = sessionId,
|
||||||
|
stageId = stageId,
|
||||||
|
layers = mapOf(
|
||||||
|
ContextLayer.L0 to listOf(entry("sys", ContextLayer.L0, EntryRole.SYSTEM, "systemPrompt")),
|
||||||
|
ContextLayer.L1 to listOf(entry("question", ContextLayer.L1, EntryRole.USER, "chat")),
|
||||||
|
ContextLayer.L3 to listOf(entry("recalled", ContextLayer.L3, EntryRole.SYSTEM, "recalledMemory")),
|
||||||
|
),
|
||||||
|
budgetUsed = 30,
|
||||||
|
budgetLimit = 4000,
|
||||||
|
)
|
||||||
|
val messages = PromptRenderer.render(pack)
|
||||||
|
assertEquals(
|
||||||
|
listOf("system" to "sys\n\nrecalled", "user" to "question"),
|
||||||
|
messages.map { it.role to it.content },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user