Repository navigation
fix(python): summarizing conversation manager falls back to truncation instead of failing every turn - #5603
Conversation
…ger cannot summarize When reactive overflow recovery fails to summarize (too few messages, or the summary call itself overflows), truncate the largest text block in a non-pinned message to a head+tail preview instead of re-raising. The error propagates only once nothing is left to truncate, so an oversized paste no longer permanently fails every later turn and the retry loop stays bounded. Fixes strands-agents#5598
…reductions, count removals after the summary succeeds
mkmeral
left a comment
There was a problem hiding this comment.
@strandly-the-agent update pls
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Assessment: Request Changes Solid, well-tested fix for #5598 — the truncation fallback, the per-invocation reduction cap, and moving Review themes
Nice work bounding the loop and the careful session-offset handling — once the function is made private and the cap only counts non-progress, this looks good to go. |
…on cap with a no-progress guard in _summarize_oldest
|
Assessment: Approve Re-reviewed at Resolution of prior feedback
Nice iteration — removing the arbitrary cap in favor of a precise no-progress condition is the better design. LGTM. |
…th ContextWindowOverflowException
|
Assessment: Approve Re-reviewed at Details
All feedback from prior rounds remains resolved. LGTM. |
…n instead of failing every turn (strands-agents#5603) Co-authored-by: strandly-the-agent <strandly-the-agent@users.noreply.github.com>
Requested by: @ekumar96 — cut PRs for the ContextOffloader / SummarizingConversationManager fixes blocking the AgentCore Harness launch (Slack, 2026-10-27).
Human Overview
Description
When reactive overflow recovery could not summarize (nothing old enough, or the summary call itself overflowed because one message is bigger than the window),
reduce_contextre-raised, the turn failed, and every later turn — evenSay hello.— failed the same way.Now, in reactive mode only, a summarization that fails with
ContextWindowOverflowException(nothing old enough to summarize, or the summary call itself overflows) falls back to_truncate_oversized_text_blocks(newcompression/truncate_text.py, private): every untruncated text block in a non-pinned message whose truncation saves more than 2,000 chars is replaced in one pass by a 2,000-char head +... [truncated: N chars removed] ...+ 2,000-char tail (the sliding-window marker format). The error propagates when nothing qualifies, and any other summarization error (throttling, network) is re-raised untouched — truncation is lossy and a retry may still succeed. Two further fixes keep the overflow retry loop bounded without any per-invocation state:_summarize_oldestnow raisesContextWindowOverflowExceptionwhen the only message old enough to summarize is the previous summary (re-summarizing it never shrinks the history — this is the pre-existing no-progress loop onmain), so every reactive reduction either removes a message, truncates a block, or raises; andremoved_message_count/_summary_messageare updated only after the summary call succeeds, so a failed summary can no longer persist an inflated session offset.Truncation edits
agent.messagesonly — a session store keeps the original content (documented onreduce_context).Related Issues
Fixes #5598
Documentation PR
None: internal behaviour of an existing manager; docstrings updated.
Type of Change
Bug fix
Testing
New: helper unit tests (
test_truncate_text.py, 8 cases incl. marker-in-user-text, pinned, no re-truncate, no-op onpreserve_chars<=0); manager tests for fallback on "insufficient messages", fallback on a summary call that overflows, no truncation on a throttled summary call, re-raise once nothing is left, summary-only range raises instead of re-summarizing (and is still folded in when real messages accompany it),removed_message_countunchanged on failure, proactive path untouched; end-to-endAgenttest with a size-aware mock model: a 50k-char paste is truncated andSay hello.answers in 2 model calls.pyteston summarizing/conversation_manager/session suites after round 3: 403 passed; round 2 across agent suites too: 591 passed, 1 pre-existing unrelated failure (test_baggage_attributes_stamped_on_agent_span, fails on the branch base too). ruff check/format + mypy clean;run-analysis.mjs: size/m.Repro of the reviewer's scenarios on the fixed branch (
repro.pyin the artifacts): 12×9k-char history with a 20k window now stops cleanly after 21 model calls (main: RecursionError after ~1,000 calls once the fallback made the loop reachable); giant paste +FileSessionManagerrecovers in 3 calls withremoved_message_count=0and 14/14 messages restored.I ran
hatch run prepare(sandbox has no hatch; ran the equivalent ruff/mypy/pytest directly)Additional Details
Independent review (fresh-context reviewer) and ledger
Round 1 verdict: changes requested — all 🔴/🟡 addressed in the second commit:
removed_message_countwas advanced before_generate_summary, so a fallback success persisted an inflated offset and messages vanished on session restore — bookkeeping moved after the summary call; test added.main, now reachable) — per-invocation cap of 10 reactive reductions; test added. The underlying no-progress loop onmain(summarizing only the summary itself whenlen(messages) == preserve_recent_messages + 1) is worth its own issue.preserve_chars.... [truncated:substring made user text containing it un-truncatable — exact marker regex.preserve_chars=0grew the text; non-dict content blocks raised inside the handler;__init__docstring; literal assertions — all fixed.except Exceptionwas initially not taken; done in round 3 (f5e274f) atmkmeral's request — the fallback now triggers only onContextWindowOverflowException.advanced-tier adversarial pass was also spawned but timed out (900s) before reporting; the reviewer's own adversarial probes above covered the same ground.Round 2 (after
mkmeral's review, commit0d8b0eb): helper made private; the per-invocation reduction cap (_reactive_reductions, reset inapply_management) replaced by the no-progress guard in_summarize_oldest, which needs no reset and also protects directreduce_contextcallers and subclasses that overrideapply_management. Fresh-context reviewer verdict: approve, no 🔴. Termination fuzzed over ~10,750 configurations (plain/tool-pair histories, pins,pin_first, ratio/preserve extremes): 0 non-terminating cases with the guard, max 16 reductions; the guard-stripped copy had ~4,100 no-progress fixpoints. Guard verified to fire after aFileSessionManagerrestore (dict equality; the restored summary carries its tracking id). Two 🟡 noted, not changed:[summary] + preserve_recent_messagesrecents,mainre-summarized the summary on every proactive trigger (one model call, lossy, no structural gain); now that logs a warning and leaves the history alone. Intentional — happy to scope the guard to the reactive path if preferred.Checklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.