2727 build_user_prompt ,
2828)
2929from rlm .utils .rlm_utils import filter_sensitive_keys
30+ from rlm .utils .token_utils import count_tokens , get_context_limit
3031
3132
3233class RLM :
@@ -54,6 +55,8 @@ def __init__(
5455 persistent : bool = False ,
5556 custom_tools : dict [str , Any ] | None = None ,
5657 custom_sub_tools : dict [str , Any ] | None = None ,
58+ compaction : bool = False ,
59+ compaction_threshold_pct : float = 0.85 ,
5760 ):
5861 """
5962 Args:
@@ -74,6 +77,10 @@ def __init__(
7477 values are callable functions. These are injected into the REPL globals.
7578 custom_sub_tools: Dict of custom tools for sub-agents (llm_query calls). If None, inherits
7679 from custom_tools. Pass an empty dict {} to disable tools for sub-agents.
80+ compaction: If True, keep full root model history in REPL variable `history` and compact
81+ when root context reaches compaction_threshold_pct of the model's context limit.
82+ compaction_threshold_pct: When compaction is on, trigger summarization when root
83+ message token count reaches this fraction of the model context limit (default 0.85).
7784 """
7885 # Store config for spawning per-completion
7986 self .backend = backend
@@ -98,6 +105,9 @@ def __init__(
98105 # Sub-tools: if None, inherit from custom_tools; if {}, no tools for sub-agents
99106 self .custom_sub_tools = custom_sub_tools if custom_sub_tools is not None else custom_tools
100107
108+ self .compaction = compaction
109+ self .compaction_threshold_pct = compaction_threshold_pct
110+
101111 self .depth = depth
102112 self .max_depth = max_depth
103113 self .max_iterations = max_iterations
@@ -181,6 +191,8 @@ def _spawn_completion_context(self, prompt: str | dict[str, Any]):
181191 env_kwargs ["custom_tools" ] = self .custom_tools
182192 if self .custom_sub_tools is not None :
183193 env_kwargs ["custom_sub_tools" ] = self .custom_sub_tools
194+ if self .compaction and self .environment_type == "local" :
195+ env_kwargs ["compaction" ] = True
184196 environment : BaseEnv = get_environment (self .environment_type , env_kwargs )
185197
186198 if self .persistent :
@@ -204,7 +216,11 @@ def _setup_prompt(self, prompt: str | dict[str, Any]) -> list[dict[str, Any]]:
204216 query_metadata = metadata ,
205217 custom_tools = self .custom_tools ,
206218 )
207-
219+ if self .compaction :
220+ message_history [0 ]["content" ] += (
221+ "\n \n The full conversation history (trajectory segments and any summaries) "
222+ "is available in the REPL variable `history` as a list."
223+ )
208224 return message_history
209225
210226 def completion (
@@ -236,6 +252,18 @@ def completion(
236252 message_history = self ._setup_prompt (prompt )
237253
238254 for i in range (self .max_iterations ):
255+ if self .compaction and hasattr (environment , "append_compaction_entry" ):
256+ current_tokens , threshold_tokens , max_tokens = self ._get_compaction_status (
257+ message_history
258+ )
259+ self .verbose .print_compaction_status (
260+ current_tokens , threshold_tokens , max_tokens
261+ )
262+ if current_tokens >= threshold_tokens :
263+ self .verbose .print_compaction ()
264+ message_history = self ._compact_history (
265+ lm_handler , environment , message_history
266+ )
239267 # Current prompt = message history + additional prompt suffix
240268 context_count = (
241269 environment .get_context_count ()
@@ -257,8 +285,14 @@ def completion(
257285 environment = environment ,
258286 )
259287
260- # Check if RLM is done and has a final answer.
261- final_answer = find_final_answer (iteration .response , environment = environment )
288+ # Check if RLM is done and has a final answer. Prefer FINAL_VAR result from REPL execution.
289+ final_answer = None
290+ for block in iteration .code_blocks :
291+ if getattr (block .result , "final_answer" , None ):
292+ final_answer = block .result .final_answer
293+ break
294+ if final_answer is None :
295+ final_answer = find_final_answer (iteration .response , environment = environment )
262296 iteration .final_answer = final_answer
263297
264298 # If logger is used, log the iteration.
@@ -294,6 +328,8 @@ def completion(
294328
295329 # Update message history with the new messages.
296330 message_history .extend (new_messages )
331+ if self .compaction and hasattr (environment , "append_compaction_entry" ):
332+ environment .append_compaction_entry (new_messages )
297333
298334 # Default behavior: we run out of iterations, provide one final answer
299335 time_end = time .perf_counter ()
@@ -317,6 +353,50 @@ def completion(
317353 metadata = self .logger .get_trajectory () if self .logger else None ,
318354 )
319355
356+ def _get_compaction_status (self , message_history : list [dict [str , Any ]]) -> tuple [int , int , int ]:
357+ """Return (current_tokens, threshold_tokens, max_tokens) for compaction."""
358+ model_name = (
359+ self .backend_kwargs .get ("model_name" , "unknown" ) if self .backend_kwargs else "unknown"
360+ )
361+ max_tokens = get_context_limit (model_name )
362+ current_tokens = count_tokens (message_history , model_name )
363+ threshold_tokens = int (self .compaction_threshold_pct * max_tokens )
364+ return current_tokens , threshold_tokens , max_tokens
365+
366+ def _should_compact (self , message_history : list [dict [str , Any ]]) -> bool :
367+ """True when root message history is at or over the compaction threshold."""
368+ current_tokens , threshold_tokens , _ = self ._get_compaction_status (message_history )
369+ return current_tokens >= threshold_tokens
370+
371+ def _compact_history (
372+ self ,
373+ lm_handler : LMHandler ,
374+ environment : BaseEnv ,
375+ message_history : list [dict [str , Any ]],
376+ ) -> list [dict [str , Any ]]:
377+ """
378+ Summarize current trajectory, append summary to REPL history, and return
379+ a short message_history with the summary as the new starting point.
380+ """
381+ summary_prompt = message_history + [
382+ {
383+ "role" : "user" ,
384+ "content" : "Very concisely summarize what you have been doing so far in 1–3 short paragraphs. Be extremely brief. This summary will be used to continue the conversation." ,
385+ }
386+ ]
387+ summary = lm_handler .completion (summary_prompt )
388+ if hasattr (environment , "append_compaction_entry" ):
389+ environment .append_compaction_entry ({"type" : "summary" , "content" : summary })
390+ # Keep system + initial assistant (metadata), then summary + continue
391+ new_history = message_history [:2 ] + [
392+ {"role" : "assistant" , "content" : summary },
393+ {
394+ "role" : "user" ,
395+ "content" : "Continue from the above summary. The full history (including this summary) is in the REPL variable `history`. Your next action:" ,
396+ },
397+ ]
398+ return new_history
399+
320400 def _completion_turn (
321401 self ,
322402 prompt : str | dict [str , Any ],
0 commit comments