fix(context): preserve chronological turn order through pack assembly

Tool-calling loops were rendered as all-assistant-calls-then-all-tool-results
with the original task last, causing the model to re-issue the same tool call
indefinitely. Two stacked reorderings caused it:

- DefaultContextPackBuilder grouped entries by sourceType for per-type
  compression, discarding a,t,a,t interleaving.
- PromptRenderer forced L1 (the live user turn) to render last, pushing the
  task after the entire transcript.
- SessionOrchestrator's tool loop re-fed currentContext.layers.values.flatten()
  (grouped by layer) each round, compounding the scramble.

Add a chronological `ordinal` to ContextEntry, stamped by the builder from
input order and restored after grouping/compression (the compressor preserves
entry identity, so ordinals survive). PromptRenderer now orders non-system
messages by ordinal, with the old L1-last layer priority kept only as a
tiebreak so router chat (ordinal 0) is unchanged. The orchestrator keeps a
running ordered accumulator instead of reading back the grouped pack.

Adds builder + renderer ordering regression tests.
This commit is contained in:
2026-06-03 22:22:52 +04:00
parent 2bef4b96a5
commit 8fe3a504bb
6 changed files with 135 additions and 25 deletions
@@ -281,11 +281,16 @@ abstract class SessionOrchestrator(
val schemaEntries = buildSchemaEntries(responseFormat, stageId)
val steeringEntries = buildSteeringNoteEntries(sessionId)
// Maintain the running transcript in true chronological order. Reading it back
// from currentContext.layers (a Map grouped by layer) would scramble turn order
// across rounds; instead we grow our own ordered list and let the builder restamp
// ordinals from it each round.
var accumulatedEntries = systemPrompt + schemaEntries + promptEntries + steeringEntries
val contextPack = contextPackBuilder.build(
id = ContextPackId(UUID.randomUUID().toString()),
sessionId = sessionId,
stageId = stageId,
entries = systemPrompt + schemaEntries + promptEntries + steeringEntries,
entries = accumulatedEntries,
budget = TokenBudget(limit = stageConfig.tokenBudget),
)
emitContextTruncationIfNeeded(sessionId, stageId, contextPack)
@@ -324,12 +329,12 @@ abstract class SessionOrchestrator(
retryable = true,
)
}
val allEntries = currentContext.layers.values.flatten() + toolEntries
accumulatedEntries = accumulatedEntries + toolEntries
currentContext = contextPackBuilder.build(
id = ContextPackId(UUID.randomUUID().toString()),
sessionId = sessionId,
stageId = stageId,
entries = allEntries,
entries = accumulatedEntries,
budget = TokenBudget(limit = stageConfig.tokenBudget),
)
emitContextTruncationIfNeeded(sessionId, stageId, currentContext)