feat(router): proper grounded system prompts for chat and narration
Replace the placeholder "You are a routing assistant" one-liner with two purpose-written prompts: a conversational prompt that frames the router as correx's operator-facing interface to the event-sourced engine (grounded in workflow state, no fabrication, concise, steering-aware), and a dedicated narrator prompt for buildNarrationContext that asks for one or two present-tense sentences naming the stage and outcome. The longer protected frame shifted token budgets, so the two L2 eviction tests now size their budget from the measured protected-frame + per-entry cost instead of hard-coded magic numbers, making them robust to prompt length.
This commit is contained in:
@@ -93,8 +93,14 @@ class RouterContextBuilderTest {
|
||||
RouterL2Entry(StageId("s3"), summary, StageOutcomeKind.SUCCESS, Instant.parse("2026-01-03T00:00:00Z")),
|
||||
),
|
||||
)
|
||||
// L0 ~30 tokens; budget 145 leaves ~115. Two entries (~55 each = ~110) fit; the third (oldest) is dropped.
|
||||
val pack = runBlocking { builderNoConv.build(state, TokenBudget(limit = 145)) }
|
||||
// Measure the protected frame (L0) and per-entry cost, then size the budget to fit
|
||||
// exactly two of three L2 entries — robust to system-prompt length changes.
|
||||
val measured = runBlocking { builderNoConv.build(state, TokenBudget(limit = 10000)) }
|
||||
val l0Cost = measured.layers[ContextLayer.L0].orEmpty().sumOf { it.tokenEstimate }
|
||||
val entryCost = measured.layers[ContextLayer.L2].orEmpty().first().tokenEstimate
|
||||
val pack = runBlocking {
|
||||
builderNoConv.build(state, TokenBudget(limit = l0Cost + entryCost * 2 + entryCost / 2))
|
||||
}
|
||||
val l2Entries = pack.layers[ContextLayer.L2] ?: emptyList()
|
||||
val retainedIds = l2Entries.map { it.sourceId }.toSet()
|
||||
// Newest two survive; oldest is dropped
|
||||
@@ -218,9 +224,14 @@ class RouterContextBuilderTest {
|
||||
RouterL2Entry(StageId("s3"), summary, StageOutcomeKind.SUCCESS, Instant.parse("2026-01-01T02:00:00Z")),
|
||||
),
|
||||
)
|
||||
// "Stage sN (SUCCESS): " + 100x = ~30 tokens each; L0 ~30 tokens.
|
||||
// Budget = L0 (30) + two entries (30+30=60) = 90. Third entry doesn't fit → oldest (s1) dropped.
|
||||
val pack = runBlocking { builderTight.build(state, TokenBudget(limit = 90)) }
|
||||
// Size the budget from the measured protected frame + per-entry cost to fit exactly two
|
||||
// of three entries; the third (oldest, s1) is dropped. Robust to system-prompt length.
|
||||
val measured = runBlocking { builderTight.build(state, TokenBudget(limit = 10000)) }
|
||||
val l0Cost = measured.layers[ContextLayer.L0].orEmpty().sumOf { it.tokenEstimate }
|
||||
val entryCost = measured.layers[ContextLayer.L2].orEmpty().first().tokenEstimate
|
||||
val pack = runBlocking {
|
||||
builderTight.build(state, TokenBudget(limit = l0Cost + entryCost * 2 + entryCost / 2))
|
||||
}
|
||||
val l2Entries = pack.layers[ContextLayer.L2] ?: emptyList()
|
||||
val retainedIds = l2Entries.map { it.sourceId }
|
||||
// Oldest entry (s1) should be dropped; s2 and s3 retained in chronological order
|
||||
@@ -436,9 +447,11 @@ class RouterContextBuilderTest {
|
||||
val pack = buildPack(state, TokenBudget(limit = 10000))
|
||||
val systemEntry = pack.layers[ContextLayer.L0]?.find { it.sourceType == "systemPrompt" }
|
||||
assertNotNull(systemEntry)
|
||||
assertEquals(
|
||||
"You are a routing assistant. Provide guidance based on workflow state and conversation context.",
|
||||
systemEntry!!.content,
|
||||
val content = systemEntry!!.content
|
||||
assertTrue(content.contains("correx"), "system prompt names the product")
|
||||
assertTrue(
|
||||
content.contains("workflow", ignoreCase = true),
|
||||
"system prompt grounds the router in workflow state",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user