From 71220cdf5b7ea75f5eb76e4c3ed0c462aa50f1b8 Mon Sep 17 00:00:00 2001 From: xue xinglong Date: Mon, 13 Jul 2026 13:09:29 +0800 Subject: [PATCH] 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). --- agent/context_engine.py | 15 ++++++ .../test_context_engine_select_context.py | 47 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/agent/context_engine.py b/agent/context_engine.py index 8cc9d512788e..91c055383b76 100644 --- a/agent/context_engine.py +++ b/agent/context_engine.py @@ -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. diff --git a/tests/agent/test_context_engine_select_context.py b/tests/agent/test_context_engine_select_context.py index 55319d2f33d5..3927ef9aa54f 100644 --- a/tests/agent/test_context_engine_select_context.py +++ b/tests/agent/test_context_engine_select_context.py @@ -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():