diff --git a/pydantic_ai_harness/compaction/_limit_warner.py b/pydantic_ai_harness/compaction/_limit_warner.py index 7dee099..9eb5e0d 100644 --- a/pydantic_ai_harness/compaction/_limit_warner.py +++ b/pydantic_ai_harness/compaction/_limit_warner.py @@ -121,6 +121,13 @@ class LimitWarner(AbstractCapability[AgentDepsT]): if not isinstance(msg, ModelRequest): cleaned.append(msg) continue + # An already-empty request has no warnings to strip; keep it. pydantic-ai's + # empty-response retry appends a `ModelRequest` with no parts, and dropping it + # would leave history ending on a `ModelResponse`, which fails the next + # request's "must end with a ModelRequest" precondition. + if not msg.parts: + cleaned.append(msg) + continue parts = [p for p in msg.parts if not self._is_marker_part(p)] if not parts: continue diff --git a/tests/compaction/test_compaction.py b/tests/compaction/test_compaction.py index e8a3c86..cd3d80b 100644 --- a/tests/compaction/test_compaction.py +++ b/tests/compaction/test_compaction.py @@ -807,6 +807,20 @@ class TestLimitWarnerEdgeCases: assert isinstance(first, ModelRequest) assert len(first.parts) == 1 + @pytest.mark.anyio + async def test_keeps_already_empty_request(self): + """An already-empty ModelRequest must survive stripping so history still ends with a + ModelRequest. pydantic-ai's empty-response retry appends an empty ModelRequest; + dropping it left history ending on a ModelResponse and crashed the next request with + `Processed history must end with a ModelRequest`.""" + lw = LimitWarner(max_iterations=100) + messages: list[ModelMessage] = [_user('real'), ModelResponse(parts=[]), ModelRequest(parts=[])] + rc = _make_request_context(messages) + ctx = _make_ctx(requests=5) + result = await lw.before_model_request(ctx, rc) + assert len(result.messages) == 3 + assert isinstance(result.messages[-1], ModelRequest) + @pytest.mark.anyio async def test_context_warning_below_threshold(self): """Context window should not warn when below threshold."""