Description
Problem
The stock before-phase default (ContextWindowCompactionStrategy) escalates from tool-result collapse straight to deleting the oldest groups at 80% of budget. The middle options the library already ships — SummarizationStrategy (degrade content gracefully) and SelectiveToolCallCompactionStrategy (drop stale tool traffic first) — are not reachable from any composed default, so a long conversation loses history that a cheaper, less destructive stage could have preserved.
TokenBudgetComposedStrategy is close but not usable as the harness before-phase:
- its strict fallback loop can exclude every group, including system groups (
_compaction.py ~1189-1209) — an empty projection the provider rejects (contents are required / Messages are required);
- stages are not individually budget-gated: there is no "stop escalating as soon as the target is met" contract, no per-stage keep-N configuration, and no observability of what each stage did.
Requested
A composed strategy (or an extension of TokenBudgetComposedStrategy) with:
- Ordered stages with early-stop: run stage N only if included tokens still exceed the target after stage N-1; re-annotate/re-tokenize between stages (inserted summaries carry
token_count=None and silently under-count otherwise).
- Per-stage configuration: keep-N for tool-result/selective stages, target count for summarization, and a separate (higher) threshold for the destructive truncation stage.
- A floor: never produce a projection with zero non-system messages.
- Observability: a log line (or callback) per stage that changed — counts, not content.
- Loop-aware after-phase cadence: harness auto-loops re-invoke the agent per iteration, so
after_run digestion fires per iteration — at small keep-N a model's fresh parallel tool results are digested to assistant text before its next roundtrip reads them (observed live: the model concluded its tools returned "no results" and looped re-searching). The composition should digest at user-turn boundaries or debounce while the loop will immediately continue.
- Bounded summarization input — now split out as its own upstream bug report, filed as microsoft/agent-framework#7214:
SummarizationStrategy ships the whole transcript unbounded and silently stops contributing past the provider cap; any composition should design the bound in from the start.
Evidence it matters (live, harness + AG-UI deployment)
- Escalation preserved the task: with the composed ladder, an over-budget research conversation summarized down (S2: 20→5 messages, 106,949→11,191 est. tokens) and the model still recalled the opening request's verbatim marker afterwards. Stock behavior (delete-oldest) removed the opening request in the same scenario.
- Scale run (906 messages / est. 1.1M tokens, 1M-context model): the ladder kept every run alive (zero run errors) where a single-stage design had nothing between "collapse tool results" and "delete history". The same run — and a second incident on a 262k-cap model (2026-07-17) — surfaced the unbounded-summarizer-input defect, filed separately as microsoft/agent-framework#7214.
Workaround (application-side, today)
ats.compaction.EscalatingCompactionStrategy: composes the three stock strategies plus a first-user-preserving truncation (separate FR, filed as microsoft/agent-framework#7216), gated on input_budget = window − output, early-stopping at 50%, truncating only above 80%, with INFO/DEBUG logging and a chunking + memoizing client in front of the summarizer (bounded map-reduce summarize requests; the before phase otherwise re-summarizes identical content every roundtrip under per-service-call persistence). All of it is composition glue that would be deleted if upstream shipped what's requested above.
Code Sample
Language/SDK
Both
Description
Problem
The stock before-phase default (
ContextWindowCompactionStrategy) escalates from tool-result collapse straight to deleting the oldest groups at 80% of budget. The middle options the library already ships —SummarizationStrategy(degrade content gracefully) andSelectiveToolCallCompactionStrategy(drop stale tool traffic first) — are not reachable from any composed default, so a long conversation loses history that a cheaper, less destructive stage could have preserved.TokenBudgetComposedStrategyis close but not usable as the harness before-phase:_compaction.py~1189-1209) — an empty projection the provider rejects (contents are required/Messages are required);Requested
A composed strategy (or an extension of
TokenBudgetComposedStrategy) with:token_count=Noneand silently under-count otherwise).after_rundigestion fires per iteration — at small keep-N a model's fresh parallel tool results are digested to assistant text before its next roundtrip reads them (observed live: the model concluded its tools returned "no results" and looped re-searching). The composition should digest at user-turn boundaries or debounce while the loop will immediately continue.SummarizationStrategyships the whole transcript unbounded and silently stops contributing past the provider cap; any composition should design the bound in from the start.Evidence it matters (live, harness + AG-UI deployment)
Workaround (application-side, today)
ats.compaction.EscalatingCompactionStrategy: composes the three stock strategies plus a first-user-preserving truncation (separate FR, filed as microsoft/agent-framework#7216), gated oninput_budget = window − output, early-stopping at 50%, truncating only above 80%, with INFO/DEBUG logging and a chunking + memoizing client in front of the summarizer (bounded map-reduce summarize requests; the before phase otherwise re-summarizes identical content every roundtrip under per-service-call persistence). All of it is composition glue that would be deleted if upstream shipped what's requested above.Code Sample
Language/SDK
Both