mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 09:25:14 +03:00
fix(langchain): stop SummarizationMiddleware pulling closed turns back into retention
`_snap_cutoff_past_open_thinking_turn` scanned backward from the candidate cutoff even when that cutoff already landed on a `HumanMessage`/`SystemMessage` - a boundary right after a completed assistant turn, with nothing open to preserve. For a history like `Human -> AI(thinking+tool) -> Tool -> Human` with the cutoff on the final human message, the scan walked through the finished turn and, because it contained a thinking block, incorrectly pulled the cutoff back to the earlier `AIMessage`, retaining an old turn beyond the configured retention budget (and undoing the orphan-tool forward fallback). The helper now returns the cutoff unchanged whenever `messages[cutoff_index]` is itself a human/system boundary, before any backward scanning happens. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
1 parent
2509af93a9
commit
258917a879
2 files changed
+41
No files matched your search
@@ -844,6 +844,13 @@ class SummarizationMiddleware(AgentMiddleware[AgentState[ResponseT], ContextT, R
|
||||
if cutoff_index <= 0 or cutoff_index >= len(messages):
|
||||
return cutoff_index
|
||||
|
||||
# A cutoff that already lands on a HumanMessage/SystemMessage is itself a
|
||||
# turn boundary right after the preceding (completed) assistant turn -
|
||||
# there's nothing open to preserve, so leave it alone rather than scanning
|
||||
# backward into that already-finished turn.
|
||||
if isinstance(messages[cutoff_index], (HumanMessage, SystemMessage)):
|
||||
return cutoff_index
|
||||
|
||||
turn_start = cutoff_index
|
||||
while turn_start > 0:
|
||||
previous_message = messages[turn_start - 1]
|
||||
|
||||
+34
@@ -1017,6 +1017,40 @@ def test_summarization_middleware_find_safe_cutoff_point_no_snap_without_thinkin
|
||||
assert middleware._find_safe_cutoff_point(messages, 3) == 3
|
||||
|
||||
|
||||
def test_summarization_middleware_find_safe_cutoff_point_closed_turn_not_reopened() -> None:
|
||||
"""A cutoff already on a Human/System boundary must not be pulled backward.
|
||||
|
||||
`Human -> AI(thinking+tool) -> Tool -> Human` with the cutoff on the final
|
||||
`HumanMessage` is already a safe boundary right after a *completed* assistant
|
||||
turn. Scanning backward from there (as opposed to refusing to scan at all)
|
||||
would walk through that finished turn and, because it contains a thinking
|
||||
block, incorrectly pull the cutoff back to the earlier `AIMessage` - retaining
|
||||
an old turn beyond the configured retention budget.
|
||||
"""
|
||||
model = FakeToolCallingModel()
|
||||
middleware = SummarizationMiddleware(
|
||||
model=model, trigger=("messages", 10), keep=("messages", 2)
|
||||
)
|
||||
|
||||
def thinking_block(text: str) -> dict[str, str]:
|
||||
return {"type": "thinking", "thinking": text, "signature": "sig"}
|
||||
|
||||
messages: list[AnyMessage] = [
|
||||
HumanMessage(content="first request", id="h0"), # index 0
|
||||
AIMessage(
|
||||
content=[thinking_block("plan")],
|
||||
tool_calls=[{"name": "get_data", "args": {}, "id": "call_get"}],
|
||||
id="a1",
|
||||
), # index 1
|
||||
ToolMessage(content="raw data", tool_call_id="call_get", id="t1"), # index 2
|
||||
HumanMessage(content="second request", id="h1"), # index 3 - naive cutoff, closed turn
|
||||
]
|
||||
|
||||
# The candidate cutoff (index 3) already sits right after the completed turn
|
||||
# (index 1-2). It must be returned unchanged, not pulled back to index 1.
|
||||
assert middleware._find_safe_cutoff_point(messages, 3) == 3
|
||||
|
||||
|
||||
def test_summarization_middleware_zero_and_negative_target_tokens() -> None:
|
||||
"""Test handling of edge cases with target token calculations."""
|
||||
# Test with very small fraction that rounds to zero
|
||||
|
||||
Reference in new issue
Block a user