-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
fix: refine context compression fallback #8992
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a4fe923
c81262c
36b1743
6564c82
d41658e
a9d543c
57bda47
69b3568
e6cfd6e
877f895
7f028cc
8ce2f09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |||||||||||||||||||||||||||||||||||||||
| from ..message import Message | ||||||||||||||||||||||||||||||||||||||||
| from .compressor import LLMSummaryCompressor, TruncateByTurnsCompressor | ||||||||||||||||||||||||||||||||||||||||
| from .config import ContextConfig | ||||||||||||||||||||||||||||||||||||||||
| from .round_utils import count_conversation_rounds | ||||||||||||||||||||||||||||||||||||||||
| from .token_counter import EstimateTokenCounter | ||||||||||||||||||||||||||||||||||||||||
| from .truncator import ContextTruncator | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -53,18 +54,50 @@ async def process( | |||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||
| The processed message list. | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
| result, _ = await self.process_with_meta(messages, trusted_token_usage) | ||||||||||||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| async def process_with_meta( | ||||||||||||||||||||||||||||||||||||||||
| self, messages: list[Message], trusted_token_usage: int = 0 | ||||||||||||||||||||||||||||||||||||||||
| ) -> tuple[list[Message], bool]: | ||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||
| result = messages | ||||||||||||||||||||||||||||||||||||||||
| was_hard_truncated = False | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| # 1. 基于轮次的截断 (Enforce max turns) | ||||||||||||||||||||||||||||||||||||||||
| if self.config.enforce_max_turns != -1: | ||||||||||||||||||||||||||||||||||||||||
| result = self.truncator.truncate_by_turns( | ||||||||||||||||||||||||||||||||||||||||
| result, | ||||||||||||||||||||||||||||||||||||||||
| keep_most_recent_turns=self.config.enforce_max_turns, | ||||||||||||||||||||||||||||||||||||||||
| drop_turns=self.config.truncate_turns, | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| turn_count = count_conversation_rounds(result) | ||||||||||||||||||||||||||||||||||||||||
| if turn_count > self.config.enforce_max_turns: | ||||||||||||||||||||||||||||||||||||||||
| if isinstance(self.compressor, LLMSummaryCompressor): | ||||||||||||||||||||||||||||||||||||||||
| logger.debug( | ||||||||||||||||||||||||||||||||||||||||
| "Turn limit (%s) exceeded (%s turns), " | ||||||||||||||||||||||||||||||||||||||||
| "delegating to LLM summary compressor instead of " | ||||||||||||||||||||||||||||||||||||||||
| "hard truncation.", | ||||||||||||||||||||||||||||||||||||||||
| self.config.enforce_max_turns, | ||||||||||||||||||||||||||||||||||||||||
| turn_count, | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| compressed = await self.compressor(result) | ||||||||||||||||||||||||||||||||||||||||
| if self.compressor.last_call_failed: | ||||||||||||||||||||||||||||||||||||||||
| logger.warning( | ||||||||||||||||||||||||||||||||||||||||
| "LLM summary compression failed; falling back " | ||||||||||||||||||||||||||||||||||||||||
| "to turn-based truncation to bound context " | ||||||||||||||||||||||||||||||||||||||||
| "size.", | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| result = self.truncator.truncate_by_turns( | ||||||||||||||||||||||||||||||||||||||||
| result, | ||||||||||||||||||||||||||||||||||||||||
| keep_most_recent_turns=self.config.enforce_max_turns, | ||||||||||||||||||||||||||||||||||||||||
| drop_turns=self.config.truncate_turns, | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| was_hard_truncated = True | ||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||
| result = compressed | ||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||
| result = self.truncator.truncate_by_turns( | ||||||||||||||||||||||||||||||||||||||||
| result, | ||||||||||||||||||||||||||||||||||||||||
| keep_most_recent_turns=self.config.enforce_max_turns, | ||||||||||||||||||||||||||||||||||||||||
| drop_turns=self.config.truncate_turns, | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| was_hard_truncated = True | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+60
to
+99
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When both async def process_with_meta(
self, messages: list[Message], trusted_token_usage: int = 0
) -> tuple[list[Message], bool]:
try:
result = messages
was_hard_truncated = False
already_compressed = False
if self.config.enforce_max_turns != -1:
turn_count = count_conversation_rounds(result)
if turn_count > self.config.enforce_max_turns:
if isinstance(self.compressor, LLMSummaryCompressor):
logger.debug(
"Turn limit (%s) exceeded (%s turns), "
"delegating to LLM summary compressor instead of "
"hard truncation.",
self.config.enforce_max_turns,
turn_count,
)
compressed = await self.compressor(result)
if self.compressor.last_call_failed:
logger.warning(
"LLM summary compression failed; falling back "
"to turn-based truncation to bound context "
"size.",
)
result = self.truncator.truncate_by_turns(
result,
keep_most_recent_turns=self.config.enforce_max_turns,
drop_turns=self.config.truncate_turns,
)
was_hard_truncated = True
else:
result = compressed
already_compressed = True
else:
result = self.truncator.truncate_by_turns(
result,
keep_most_recent_turns=self.config.enforce_max_turns,
drop_turns=self.config.truncate_turns,
)
was_hard_truncated = True |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| # 2. 基于 token 的压缩 | ||||||||||||||||||||||||||||||||||||||||
| if self.config.max_context_tokens > 0: | ||||||||||||||||||||||||||||||||||||||||
| total_tokens = self.token_counter.count_tokens( | ||||||||||||||||||||||||||||||||||||||||
| result, trusted_token_usage | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -73,16 +106,19 @@ async def process( | |||||||||||||||||||||||||||||||||||||||
| if self.compressor.should_compress( | ||||||||||||||||||||||||||||||||||||||||
| result, total_tokens, self.config.max_context_tokens | ||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||
| result = await self._run_compression(result, total_tokens) | ||||||||||||||||||||||||||||||||||||||||
| result, compression_was_lossy = await self._run_compression( | ||||||||||||||||||||||||||||||||||||||||
| result, total_tokens | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| was_hard_truncated = was_hard_truncated or compression_was_lossy | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
106
to
+112
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pass the
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||||||||||||
| return result, was_hard_truncated | ||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||
| logger.error(f"Error during context processing: {e}", exc_info=True) | ||||||||||||||||||||||||||||||||||||||||
| return messages | ||||||||||||||||||||||||||||||||||||||||
| return messages, False | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| async def _run_compression( | ||||||||||||||||||||||||||||||||||||||||
| self, messages: list[Message], prev_tokens: int | ||||||||||||||||||||||||||||||||||||||||
| ) -> list[Message]: | ||||||||||||||||||||||||||||||||||||||||
| ) -> tuple[list[Message], bool]: | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
119
to
+121
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update the signature of
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
| Compress/truncate the messages. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -95,7 +131,12 @@ async def _run_compression( | |||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
| logger.debug("Compress triggered, starting compression...") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| messages = await self.compressor(messages) | ||||||||||||||||||||||||||||||||||||||||
| compressed = await self.compressor(messages) | ||||||||||||||||||||||||||||||||||||||||
| was_lossy = ( | ||||||||||||||||||||||||||||||||||||||||
| not isinstance(self.compressor, LLMSummaryCompressor) | ||||||||||||||||||||||||||||||||||||||||
| or self.compressor.last_call_failed | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| messages = compressed | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
132
to
+139
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Skip LLM compression and directly mark as lossy if
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| # double check | ||||||||||||||||||||||||||||||||||||||||
| tokens_after_summary = self.token_counter.count_tokens(messages) | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -113,9 +154,24 @@ async def _run_compression( | |||||||||||||||||||||||||||||||||||||||
| messages, tokens_after_summary, self.config.max_context_tokens | ||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||||||||||||||||||||
| "Context still exceeds max tokens after compression, applying halving truncation..." | ||||||||||||||||||||||||||||||||||||||||
| "Context still exceeds max tokens after compression, applying hard truncation..." | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| # still need compress, truncate by half | ||||||||||||||||||||||||||||||||||||||||
| messages = self.truncator.truncate_by_halving(messages) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return messages | ||||||||||||||||||||||||||||||||||||||||
| was_lossy = True | ||||||||||||||||||||||||||||||||||||||||
| while self.compressor.should_compress( | ||||||||||||||||||||||||||||||||||||||||
| messages, tokens_after_summary, self.config.max_context_tokens | ||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||
| truncated = self.truncator.truncate_by_dropping_oldest_turns( | ||||||||||||||||||||||||||||||||||||||||
| messages, | ||||||||||||||||||||||||||||||||||||||||
| drop_turns=self.config.truncate_turns, | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| if truncated == messages: | ||||||||||||||||||||||||||||||||||||||||
| truncated = self.truncator.truncate_by_halving(messages) | ||||||||||||||||||||||||||||||||||||||||
| if truncated == messages: | ||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||
| next_tokens = self.token_counter.count_tokens(truncated) | ||||||||||||||||||||||||||||||||||||||||
| if next_tokens >= tokens_after_summary: | ||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||
| messages = truncated | ||||||||||||||||||||||||||||||||||||||||
| tokens_after_summary = next_tokens | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return messages, was_lossy | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,5 @@ | ||||||||||||||||||||||
| from ..message import Message | ||||||||||||||||||||||
| from .round_utils import count_conversation_rounds, split_into_rounds | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| class ContextTruncator: | ||||||||||||||||||||||
|
|
@@ -120,15 +121,20 @@ def truncate_by_turns( | |||||||||||||||||||||
| return messages | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| system_messages, non_system_messages = self._split_system_rest(messages) | ||||||||||||||||||||||
| rounds = split_into_rounds(non_system_messages) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if len(non_system_messages) // 2 <= keep_most_recent_turns: | ||||||||||||||||||||||
| if count_conversation_rounds(non_system_messages) <= keep_most_recent_turns: | ||||||||||||||||||||||
| return messages | ||||||||||||||||||||||
|
Comment on lines
+124
to
127
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calling
Suggested change
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| num_to_keep = keep_most_recent_turns - drop_turns + 1 | ||||||||||||||||||||||
| if num_to_keep <= 0: | ||||||||||||||||||||||
| truncated_contexts = [] | ||||||||||||||||||||||
| else: | ||||||||||||||||||||||
| truncated_contexts = non_system_messages[-num_to_keep * 2 :] | ||||||||||||||||||||||
| truncated_contexts = [ | ||||||||||||||||||||||
| segment | ||||||||||||||||||||||
| for round_segments in rounds[-num_to_keep:] | ||||||||||||||||||||||
| for segment in round_segments | ||||||||||||||||||||||
| ] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Find the first user message | ||||||||||||||||||||||
| index = next( | ||||||||||||||||||||||
|
|
@@ -153,11 +159,16 @@ def truncate_by_dropping_oldest_turns( | |||||||||||||||||||||
| return messages | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| system_messages, non_system_messages = self._split_system_rest(messages) | ||||||||||||||||||||||
| rounds = split_into_rounds(non_system_messages) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if len(non_system_messages) // 2 <= drop_turns: | ||||||||||||||||||||||
| if count_conversation_rounds(non_system_messages) <= drop_turns: | ||||||||||||||||||||||
| truncated_non_system = [] | ||||||||||||||||||||||
|
Comment on lines
+162
to
165
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid redundant
Suggested change
|
||||||||||||||||||||||
| else: | ||||||||||||||||||||||
| truncated_non_system = non_system_messages[drop_turns * 2 :] | ||||||||||||||||||||||
| truncated_non_system = [ | ||||||||||||||||||||||
| segment | ||||||||||||||||||||||
| for round_segments in rounds[drop_turns:] | ||||||||||||||||||||||
| for segment in round_segments | ||||||||||||||||||||||
| ] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Find the first user message | ||||||||||||||||||||||
| index = next( | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (complexity): Consider refactoring the new control flow into a single internal implementation with small helpers for turn limits and truncation while centralizing lossy/flag handling at the top level.
You can keep the new behavior but reduce the branching/flag complexity by:
1. Unify
process/process_with_metavia a small result objectInstead of two public entry points with tuple plumbing, move the core logic into a single internal method returning a small result object.
processandprocess_with_metajust adapt that.That keeps the behavior identical, but there’s only one place (
_process_impl) where control flow and flags are managed.2. Extract turn-limit logic into a helper
This pulls the policy (LLM vs truncation, last_call_failed) out of the main flow and returns both the messages and whether we hard truncated.
Then
_process_impljust wires this together:This flattens
process_with_metaand keeps policy logic isolated.3. Extract truncation loop into a helper and keep
was_lossytop-levelYou can keep
_run_compressionfocused on “run compressor + decide if truncation is needed”, and move the truncation loop into a small helper. Also, setwas_lossyonce when you decide to truncate, instead of toggling inside the loop.And then the truncation loop becomes a self-contained helper with clear guards:
Behavior stays the same, but:
_run_compressionis now a linear sequence with one loop delegated to a named helper.was_lossyis only managed in_run_compressionat two points: post-LLM decision and when truncation kicks in._progressive_truncate_until_within_limit, which can be tested independently.