fix(router,kernel): narration user turn + stage transition ordering
Two issues surfaced during live QA once workflows began progressing past the
first stage:
1. Narration prompts went out with no user message, so the chat template
rejected them ("No user query found in messages"). buildNarrationContext
filed the trigger instruction (a user turn) under ContextLayer.L0, and
PromptRenderer folds every L0 entry into the system message regardless of
role. Move the trigger to L1 so it renders as the user turn.
2. TransitionExecutedEvent was emitted after the next stage had already run:
executeMove called enterStage (which executes the stage) before advanceStage
(which emits the transition), so the event log showed a stage running before
the event marking entry into it. Emit the transition before entering.
Adds a regression test asserting a rendered narration prompt carries a user
message.
This commit is contained in:
+6
-7
@@ -204,13 +204,12 @@ class DefaultSessionOrchestrator(
|
||||
)
|
||||
}
|
||||
|
||||
return when (val result = enterStage(ctx, nextStageId, effectives)) {
|
||||
is StepResult.Continue -> StepResult.Continue(
|
||||
result.ctx.copy(
|
||||
currentStageId = advanceStage(ctx.sessionId, ctx.currentStageId, decision),
|
||||
),
|
||||
)
|
||||
|
||||
// Emit the transition before executing the next stage. Otherwise the next stage's
|
||||
// events (InferenceStarted, artifacts, …) are recorded ahead of the TransitionExecuted
|
||||
// that marks entering it, so the log shows the stage running before it was entered.
|
||||
val advancedTo = advanceStage(ctx.sessionId, ctx.currentStageId, decision)
|
||||
return when (val result = enterStage(ctx.copy(currentStageId = advancedTo), nextStageId, effectives)) {
|
||||
is StepResult.Continue -> StepResult.Continue(result.ctx)
|
||||
is StepResult.Terminal -> result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,11 +300,14 @@ class DefaultRouterContextBuilder(
|
||||
layer = ContextLayer.L0,
|
||||
role = EntryRole.SYSTEM,
|
||||
)
|
||||
// L1, not L0: PromptRenderer folds every L0 entry into the single system message
|
||||
// regardless of role, so an L0 user turn would vanish into the system block and the
|
||||
// request would go out with no user message (the chat template then rejects it).
|
||||
val triggerEntry = buildContextEntry(
|
||||
sourceType = "narrationTrigger",
|
||||
sourceId = trigger.kind,
|
||||
content = trigger.instruction,
|
||||
layer = ContextLayer.L0,
|
||||
layer = ContextLayer.L1,
|
||||
role = EntryRole.USER,
|
||||
)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.correx.core.events.types.StageId
|
||||
import com.correx.core.inference.CapabilityScore
|
||||
import com.correx.core.inference.Embedder
|
||||
import com.correx.core.inference.FinishReason
|
||||
import com.correx.core.inference.PromptRenderer
|
||||
import com.correx.core.inference.InferenceProvider
|
||||
import com.correx.core.inference.InferenceRequest
|
||||
import com.correx.core.inference.InferenceResponse
|
||||
@@ -207,6 +208,26 @@ class RouterNarrationTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rendered narration prompt carries a user message`(): Unit = runBlocking {
|
||||
// Regression: the trigger instruction must render as a user turn. When it was filed
|
||||
// under L0 it folded into the system message, leaving zero user messages and the chat
|
||||
// template rejected the request ("No user query found in messages").
|
||||
val builder = DefaultRouterContextBuilder(RouterConfig())
|
||||
val state = RouterState(sessionId = SessionId("s"), workflowStatus = WorkflowStatus.RUNNING)
|
||||
val instruction = "Tell the user the stage just completed."
|
||||
val trigger = NarrationTrigger(kind = "stage_completed", instruction = instruction)
|
||||
val pack = builder.buildNarrationContext(state, trigger, TokenBudget(limit = 5000))
|
||||
|
||||
val messages = PromptRenderer.render(pack)
|
||||
val userMessages = messages.filter { it.role == "user" }
|
||||
assertTrue(userMessages.isNotEmpty(), "narration prompt must contain a user message")
|
||||
assertTrue(
|
||||
userMessages.any { it.content.contains(instruction) },
|
||||
"the trigger instruction must be the user turn",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildNarrationContext stays within budget`(): Unit = runBlocking {
|
||||
val builder = DefaultRouterContextBuilder(RouterConfig())
|
||||
|
||||
Reference in New Issue
Block a user