11"""Abstract interface for conversation history management."""
22
3+ import logging
4+ import warnings
35from abc import ABC , abstractmethod
46from typing import TYPE_CHECKING , Any
57
8+ from ...hooks .events import BeforeModelCallEvent
69from ...hooks .registry import HookProvider , HookRegistry
710from ...types .content import Message
811
912if TYPE_CHECKING :
1013 from ...agent .agent import Agent
14+ from ...models .model import Model
15+
16+ logger = logging .getLogger (__name__ )
17+
18+ # Track whether the context_window_limit warning has been emitted
19+ _context_window_limit_warned = False
1120
1221
1322class ConversationManager (ABC , HookProvider ):
@@ -24,6 +33,11 @@ class ConversationManager(ABC, HookProvider):
2433 lifecycle events. Derived classes that override register_hooks must call the base implementation to ensure proper
2534 hook registration.
2635
36+ Optionally, a manager can enable proactive compression by setting ``compression_threshold``
37+ in the constructor. When set, the base class registers a ``BeforeModelCallEvent`` hook that
38+ checks projected input tokens against the model's context window limit and calls
39+ :meth:`reduce_on_threshold` when the threshold is exceeded.
40+
2741 Example:
2842 ```python
2943 class MyConversationManager(ConversationManager):
@@ -33,34 +47,117 @@ def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
3347 ```
3448 """
3549
36- def __init__ (self ) -> None :
50+ def __init__ (self , * , compression_threshold : float | None = None ) -> None :
3751 """Initialize the ConversationManager.
3852
53+ Args:
54+ compression_threshold: Ratio of context window usage that triggers proactive compression.
55+ Value between 0 (exclusive) and 1 (inclusive). For example, 0.7 means compress when 70%
56+ of the context window is used. When not set, proactive compression is disabled and only
57+ reactive overflow recovery is used.
58+
59+ Raises:
60+ ValueError: If compression_threshold is not in the valid range (0, 1].
61+
3962 Attributes:
4063 removed_message_count: The messages that have been removed from the agents messages array.
4164 These represent messages provided by the user or LLM that have been removed, not messages
4265 included by the conversation manager through something like summarization.
4366 """
67+ if compression_threshold is not None and (compression_threshold <= 0 or compression_threshold > 1 ):
68+ raise ValueError (
69+ f"compression_threshold must be between 0 (exclusive) and 1 (inclusive), got { compression_threshold } "
70+ )
71+
4472 self .removed_message_count = 0
73+ self ._compression_threshold = compression_threshold
74+
75+ def reduce_on_threshold (self , agent : "Agent" , model : "Model" , ** kwargs : Any ) -> bool :
76+ """Proactively reduce the conversation history before a model call.
77+
78+ Called when projected input tokens exceed the configured compression_threshold
79+ of the model's context window limit. Subclasses implement this to reduce
80+ context before the model call, avoiding overflow errors.
81+
82+ The default implementation returns False. Subclasses that support proactive
83+ compression should override this method.
84+
85+ Args:
86+ agent: The agent whose conversation history will be reduced.
87+ The agent's messages list should be modified in-place.
88+ model: The model instance for the upcoming call.
89+ **kwargs: Additional keyword arguments for future extensibility.
90+
91+ Returns:
92+ True if the history was reduced, False otherwise.
93+ """
94+ return False
4595
4696 def register_hooks (self , registry : HookRegistry , ** kwargs : Any ) -> None :
4797 """Register hooks for agent lifecycle events.
4898
99+ When ``compression_threshold`` is configured and the subclass overrides
100+ ``reduce_on_threshold``, registers a ``BeforeModelCallEvent`` hook for
101+ proactive compression.
102+
49103 Derived classes that override this method must call the base implementation to ensure proper hook
50104 registration chain.
51105
52106 Args:
53107 registry: The hook registry to register callbacks with.
54108 **kwargs: Additional keyword arguments for future extensibility.
109+ """
110+ if self ._compression_threshold is None :
111+ return
55112
56- Example:
57- ```python
58- def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
59- super().register_hooks(registry, **kwargs)
60- registry.add_callback(SomeEvent, self.on_some_event)
61- ```
113+ # Check if the subclass actually overrides reduce_on_threshold
114+ has_override = type (self ).reduce_on_threshold is not ConversationManager .reduce_on_threshold
115+ if not has_override :
116+ logger .warning (
117+ "conversation_manager=<%s> | compression_threshold is configured but reduce_on_threshold is not"
118+ " implemented, proactive compression is disabled" ,
119+ type (self ).__name__ ,
120+ )
121+ return
122+
123+ registry .add_callback (BeforeModelCallEvent , self ._on_before_model_call_threshold )
124+
125+ def _on_before_model_call_threshold (self , event : BeforeModelCallEvent ) -> None :
126+ """Handle BeforeModelCallEvent for proactive compression.
127+
128+ Args:
129+ event: The before model call event.
62130 """
63- pass
131+ global _context_window_limit_warned # noqa: PLW0603
132+
133+ context_window_limit = event .agent .model .context_window_limit
134+ if context_window_limit is None :
135+ if not _context_window_limit_warned :
136+ _context_window_limit_warned = True
137+ warnings .warn (
138+ "context_window_limit is not set on the model, proactive compression is disabled."
139+ " Set context_window_limit in your model config" ,
140+ stacklevel = 2 ,
141+ )
142+ return
143+
144+ if event .projected_input_tokens is None :
145+ return
146+
147+ ratio = event .projected_input_tokens / context_window_limit
148+ if ratio >= self ._compression_threshold : # type: ignore[operator]
149+ logger .debug (
150+ "projected_tokens=<%s>, limit=<%s>, ratio=<%.2f>, compression_threshold=<%s>"
151+ " | compression threshold exceeded, reducing context" ,
152+ event .projected_input_tokens ,
153+ context_window_limit ,
154+ ratio ,
155+ self ._compression_threshold ,
156+ )
157+ try :
158+ self .reduce_on_threshold (agent = event .agent , model = event .agent .model )
159+ except Exception :
160+ logger .debug ("proactive compression failed, will proceed with model call" , exc_info = True )
64161
65162 def restore_from_session (self , state : dict [str , Any ]) -> list [Message ] | None :
66163 """Restore the Conversation Manager's state from a session.
0 commit comments