docs+test(context-engine): document select_context ordering/cache contract; add cache-stability + downstream-sanitizer tests

- context_engine.py: document that select_context() runs before cache-control
  and all request sanitizers, so (a) replacements still pass host validation
  and (b) the no-op default keeps the request byte-stable (AGENTS.md prompt-
  cache invariant). Note the hook is evaluated per provider request.
- tests: no-op path is byte-stable for cache-control; a role-unusual
  replacement is passed through for the existing downstream sanitizers to
  normalize (select_context does structural validation only).
This commit is contained in:
xue xinglong 2026-07-13 13:09:29 +08:00 committed by Teknium
parent 589cbafb87
commit 71220cdf5b
2 changed files with 62 additions and 0 deletions

View file

@ -250,6 +250,21 @@ class ContextEngine(ABC):
message and intentionally never rewrites the list, to preserve the
cache prefix), ``select_context()`` may *replace* the message list.
Ordering / cache contract: the host runs this hook **before** prompt
cache-control and **before** every request sanitizer (orphaned-tool
cleanup, thinking-only/role normalization, whitespace/JSON
normalization). So (a) whatever the hook returns still passes through
the same validation as any request a malformed replacement cannot
reach the provider and (b) prompt-cache stability (an AGENTS.md
invariant) is preserved: the default no-op leaves the request
byte-identical, so cache behaviour is unchanged for the built-in
compressor and any non-implementing engine. An engine that *does*
replace the list changes its own cache prefix by definition; that is
the engine's concern, and cache-control breakpoints are re-derived on
the selected list. The hook is evaluated per provider request (so it
re-runs on retries within a turn), consistent with "select the context
for THIS request".
Args:
request_messages: The assembled request message list (system
prompt + history + any ephemeral prefill), in OpenAI format.

View file

@ -192,6 +192,53 @@ def test_persisted_history_not_mutated():
assert HISTORY == history_snapshot
# -- cache-stability + downstream-sanitizer contract -----------------------
def test_noop_preserves_request_byte_stable_for_cache():
"""No-op default must leave the request byte-identical.
Prompt-cache stability is a host invariant (AGENTS.md): the hook runs
before cache-control, so a no-op engine must not perturb the list
otherwise cache breakpoints would shift for every existing engine. The
host returns the *same object*, so cache-control sees identical input.
"""
snapshot = [dict(m) for m in REQUEST]
agent = _agent_with(_MinimalEngine()) # default select_context -> None
out = _apply_context_engine_selection(
agent, REQUEST, HISTORY, HISTORY[-1], logger=MagicMock()
)
assert out is REQUEST # same object -> byte-stable for cache-control
assert REQUEST == snapshot # unperturbed
def test_role_unusual_replacement_passed_through_for_downstream_sanitizers():
"""The hook does structural validation only; role/tool normalization is
deferred to the existing downstream sanitizers.
A `system -> user -> user` replacement (the exact shape flagged as a
role-alternation risk on sibling PRs) is well-formed structurally, so the
host returns it verbatim. Role-pairing/orphaned-tool cleanup runs *after*
this hook in the request pipeline (`_sanitize_api_messages`,
`_drop_thinking_only_and_merge_users`), so select_context cannot emit a
malformed request that bypasses validation.
"""
role_unusual = [
{"role": "system", "content": "sys"},
{"role": "user", "content": "a"},
{"role": "user", "content": "b"},
]
class _Engine(_MinimalEngine):
def select_context(self, request_messages, **kwargs):
return role_unusual
agent = _agent_with(_Engine())
out = _apply_context_engine_selection(
agent, REQUEST, HISTORY, HISTORY[-1], logger=MagicMock()
)
assert out is role_unusual # accepted structurally; downstream sanitizers normalize
# -- on_turn_complete (post-turn observation) ------------------------------
def test_default_on_turn_complete_is_noop():