mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
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:
parent
589cbafb87
commit
71220cdf5b
2 changed files with 62 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue