mirror of
https://github.com/pydantic/pydantic-ai-harness.git
synced 2026-07-21 02:45:34 +00:00
Keep already-empty ModelRequests in LimitWarner._strip_old_warnings (#311)
LimitWarner rebuilds history each request, dropping any ModelRequest left with no parts after warning markers are stripped. An already-empty ModelRequest -- which pydantic-ai's empty-response retry appends when a model returns an empty response against a non-optional output type -- has no parts to begin with, so it was dropped too. That left history ending on a ModelResponse, and the next request failed the _agent_graph precondition with `UserError: Processed history must end with a ModelRequest`, killing the run. Keep a request that was already empty; only drop one that became empty because every part was a warning marker. Regression test covers the empty-response-retry tail. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
67f8d870a5
commit
e4839c654b
@@ -121,6 +121,13 @@ class LimitWarner(AbstractCapability[AgentDepsT]):
|
|||||||
if not isinstance(msg, ModelRequest):
|
if not isinstance(msg, ModelRequest):
|
||||||
cleaned.append(msg)
|
cleaned.append(msg)
|
||||||
continue
|
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)]
|
parts = [p for p in msg.parts if not self._is_marker_part(p)]
|
||||||
if not parts:
|
if not parts:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -807,6 +807,20 @@ class TestLimitWarnerEdgeCases:
|
|||||||
assert isinstance(first, ModelRequest)
|
assert isinstance(first, ModelRequest)
|
||||||
assert len(first.parts) == 1
|
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
|
@pytest.mark.anyio
|
||||||
async def test_context_warning_below_threshold(self):
|
async def test_context_warning_below_threshold(self):
|
||||||
"""Context window should not warn when below threshold."""
|
"""Context window should not warn when below threshold."""
|
||||||
|
|||||||
Reference in New Issue
Block a user