feat(context): role means message type, layer means pinning (#312/#313)
The system block is now for content that does not change during a run. Anything the run mutates renders as a user message — both because a mutating system prefix defeats prompt caching and because models under-weight system-folded content against the trailing user turn. PromptRenderer: role alone decides the message type; the old `layer == L0 ||` clause is gone. That clause was silently overriding role on four packs (InferenceSummarizer, SemanticReviewerImpl, CapabilityGapReflectorImpl, Talkie session-naming) which are L0+USER prompts — they were rendering as a system-only request with no user turn at all. Re-roled SYSTEM -> USER, all mutable within a run: recoveryTicket, remainingDelta, groundingFeedback, rejectionFeedback (trailing slot), plus verifiedBaseline, promotedConcept, claimedTask, clarificationAnswer, locked steeringNote, factSheet (inline, still L0-pinned). Left SYSTEM (immutable): systemPrompt, operatingGuidance, schemaInstruction, projectProfile, operatorProfile, agentInstructions, successfulPlanShape. Trailing-slot scarcity guard: repairMandateSourceTypes joined ALL matches, so a recovery stage on a retry with an unmet delta would stack three competing mandates. Now a precedence list emits exactly one (recoveryTicket > retryFeedback > groundingFeedback > rejectionFeedback) with remainingDelta appended as the completion signal. ContextClassifier keys STATIC on layer alone — L0 means pinned/never pruned regardless of role, so the re-roled L0 entries don't fall through to FREEFORM and get token-pruned. Also fixes a stale ContextFeedbackTest assertion (expected a CAS hash the producer deliberately stopped emitting) and a stale initialIntent doc comment claiming L0/SYSTEM where the code says L1/USER. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -168,4 +168,57 @@ class PromptRendererOrderingTest {
|
||||
messages.map { it.role to it.content },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an L0 USER entry renders as a user message, not folded into system`() {
|
||||
// #312: role alone decides the message type. A mutating L0 entry (verified baseline,
|
||||
// claimed task, steering note) stays pinned by its layer but must not enter the cached
|
||||
// system prefix. Also covers the summarizer/reviewer packs, which are L0+USER prompts
|
||||
// that used to render as a system-only request with no user turn at all.
|
||||
val pack = ContextPack(
|
||||
id = ContextPackId("p"),
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
layers = mapOf(
|
||||
ContextLayer.L0 to listOf(
|
||||
entry("sys", ContextLayer.L0, EntryRole.SYSTEM, "systemPrompt"),
|
||||
entry("baseline", ContextLayer.L0, EntryRole.USER, "verifiedBaseline"),
|
||||
),
|
||||
),
|
||||
budgetUsed = 20,
|
||||
budgetLimit = 4000,
|
||||
)
|
||||
assertEquals(
|
||||
listOf("system" to "sys", "user" to "baseline"),
|
||||
PromptRenderer.render(pack).map { it.role to it.content },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only the highest-precedence repair mandate renders, with the delta appended`() {
|
||||
// #312 scarcity guard: a recovery stage on a retry with an unmet delta would otherwise
|
||||
// stack three competing "do this next" blocks and the trailing slot stops being
|
||||
// authoritative. recoveryTicket outranks retryFeedback; remainingDelta is not a
|
||||
// competing mandate (it is the completion signal) so it appends rather than displacing.
|
||||
val pack = ContextPack(
|
||||
id = ContextPackId("p"),
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
layers = mapOf(
|
||||
ContextLayer.L1 to listOf(
|
||||
entry("task", ContextLayer.L1, EntryRole.USER, "agentPrompt"),
|
||||
entry("retry", ContextLayer.L1, EntryRole.USER, "retryFeedback"),
|
||||
entry("ticket", ContextLayer.L1, EntryRole.USER, "recoveryTicket"),
|
||||
entry("delta", ContextLayer.L1, EntryRole.USER, "remainingDelta"),
|
||||
),
|
||||
),
|
||||
budgetUsed = 40,
|
||||
budgetLimit = 4000,
|
||||
)
|
||||
val messages = PromptRenderer.render(pack)
|
||||
assertEquals("ticket\n\ndelta", messages.last().content)
|
||||
assertEquals("user", messages.last().role)
|
||||
// The losing mandate is dropped entirely — it must not leak back into the inline flow.
|
||||
assertEquals(false, messages.any { it.content.contains("retry") })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user