diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index 8e50acaaf3f..d4cf149df67 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -926,8 +926,14 @@ def _redecorate_prompt_cache_for_provider( if ( prepared is not None and getattr(agent, "provider", None) == "moa" - and guidance ): + # No `and guidance` here: guidance=None is a real prepared shape + # (all-references-failed / silent degraded policy builds the + # prepared request without attaching guidance), and the MoA facade + # sends prepared["messages"] — not api_kwargs["messages"] — so the + # rebase must refresh the prepared object even when there is no + # guidance to re-attach. rebase_prepared_request handles falsy + # guidance by copying the messages and skipping the attach. completions = getattr(getattr(agent.client, "chat", None), "completions", None) rebase = getattr(completions, "rebase_prepared_request", None) if callable(rebase): diff --git a/tests/agent/test_failover_identity.py b/tests/agent/test_failover_identity.py index 884fcbb44ca..afaab22e75e 100644 --- a/tests/agent/test_failover_identity.py +++ b/tests/agent/test_failover_identity.py @@ -376,3 +376,37 @@ class TestRedecoratePromptCacheOnPolicyChange: else: assert guidance in content + + def test_moa_no_guidance_prepared_messages_still_refreshed(self): + # guidance=None is a real prepared shape (all references failed / + # silent degraded policy). The MoA facade sends prepared["messages"], + # so the rebase must refresh the prepared object even without + # guidance — otherwise the aggregator ships the STALE decoration + # and #72626 persists for the no-guidance MoA sub-path. + prompt = "sys" + decorated = apply_anthropic_cache_control( + [ + {"role": "system", "content": prompt}, + {"role": "user", "content": "task"}, + ], + native_anthropic=True, + ) + + class _Completions: + def rebase_prepared_request(self, prepared, messages): + # Mirrors MoAChatCompletions.rebase_prepared_request with + # falsy guidance: copy messages, skip the attach. + return {**prepared, "messages": [dict(m) for m in messages]} + + # Policy change while staying on moa: cache-on -> cache-off. + agent = _cache_agent(use_caching=False, prompt=prompt, provider="moa") + agent.client = SimpleNamespace(chat=SimpleNamespace(completions=_Completions())) + prepared = {"guidance": None, "messages": decorated} + out, new_prepared = _redecorate_prompt_cache_for_provider( + agent, decorated, moa_prepared=prepared + ) + assert new_prepared is not None + # The prepared object the facade will send must carry the + # redecorated (stripped) messages, not the stale decorated list. + assert _count_cache_markers(new_prepared["messages"]) == 0 + assert new_prepared["messages"] == out