1313 RLMIteration ,
1414 RLMMetadata ,
1515)
16- from rlm .environments import BaseEnv , get_environment
16+ from rlm .environments import BaseEnv , SupportsPersistence , get_environment
1717from rlm .logger import RLMLogger , VerbosePrinter
1818from rlm .utils .parsing import (
1919 find_code_blocks ,
@@ -51,6 +51,7 @@ def __init__(
5151 other_backend_kwargs : list [dict [str , Any ]] | None = None ,
5252 logger : RLMLogger | None = None ,
5353 verbose : bool = False ,
54+ persistent : bool = False ,
5455 ):
5556 """
5657 Args:
@@ -66,6 +67,7 @@ def __init__(
6667 other_backend_kwargs: The kwargs to pass to the other client backends (ordered to match other_backends).
6768 logger: The logger to use for the RLM.
6869 verbose: Whether to print verbose output in rich to console.
70+ persistent: If True, reuse the environment across completion() calls for multi-turn conversations.
6971 """
7072 # Store config for spawning per-completion
7173 self .backend = backend
@@ -84,6 +86,14 @@ def __init__(
8486 self .logger = logger
8587 self .verbose = VerbosePrinter (enabled = verbose )
8688
89+ # Persistence support
90+ self .persistent = persistent
91+ self ._persistent_env : SupportsPersistence | None = None
92+
93+ # Validate persistence support at initialization
94+ if self .persistent :
95+ self ._validate_persistent_environment_support ()
96+
8797 # Log metadata if logger is provided
8898 if self .logger or verbose :
8999 metadata = RLMMetadata (
@@ -108,7 +118,9 @@ def __init__(
108118 def _spawn_completion_context (self , prompt : str | dict [str , Any ]):
109119 """
110120 Spawn an LM handler and environment for a single completion call.
111- Cleans up both when the context exits.
121+
122+ When persistent=True, the environment is reused across calls.
123+ When persistent=False (default), creates fresh environment each call.
112124 """
113125 # Create client and wrap in handler
114126 client : BaseLM = get_client (self .backend , self .backend_kwargs )
@@ -122,20 +134,32 @@ def _spawn_completion_context(self, prompt: str | dict[str, Any]):
122134
123135 lm_handler .start ()
124136
125- # Pass handler address to environment so it can make llm_query() calls
126- env_kwargs = self .environment_kwargs .copy ()
127- env_kwargs ["lm_handler_address" ] = (lm_handler .host , lm_handler .port )
128- env_kwargs ["context_payload" ] = prompt
137+ # Environment: reuse if persistent, otherwise create fresh
138+ if self .persistent and self ._persistent_env is not None :
139+ environment = self ._persistent_env
140+ # Defensive check: ensure environment supports persistence methods
141+ if not self ._env_supports_persistence (environment ):
142+ raise RuntimeError (
143+ f"Persistent environment of type '{ type (environment ).__name__ } ' does not "
144+ f"implement required methods (update_handler_address, add_context, get_context_count). "
145+ f"This should have been caught at initialization."
146+ )
147+ environment .update_handler_address ((lm_handler .host , lm_handler .port ))
148+ environment .add_context (prompt )
149+ else :
150+ env_kwargs = self .environment_kwargs .copy ()
151+ env_kwargs ["lm_handler_address" ] = (lm_handler .host , lm_handler .port )
152+ env_kwargs ["context_payload" ] = prompt
153+ environment : BaseEnv = get_environment (self .environment_type , env_kwargs )
129154
130- # Initialize the environment
131- environment : BaseEnv = get_environment ( self .environment_type , env_kwargs )
155+ if self . persistent :
156+ self ._persistent_env = environment
132157
133158 try :
134159 yield lm_handler , environment
135160 finally :
136- # Cleanup
137161 lm_handler .stop ()
138- if hasattr (environment , "cleanup" ):
162+ if not self . persistent and hasattr (environment , "cleanup" ):
139163 environment .cleanup ()
140164
141165 def _setup_prompt (self , prompt : str | dict [str , Any ]) -> list [dict [str , Any ]]:
@@ -177,7 +201,19 @@ def completion(
177201
178202 for i in range (self .max_iterations ):
179203 # Current prompt = message history + additional prompt suffix
180- current_prompt = message_history + [build_user_prompt (root_prompt , i )]
204+ context_count = (
205+ environment .get_context_count ()
206+ if isinstance (environment , SupportsPersistence )
207+ else 1
208+ )
209+ history_count = (
210+ environment .get_history_count ()
211+ if isinstance (environment , SupportsPersistence )
212+ else 0
213+ )
214+ current_prompt = message_history + [
215+ build_user_prompt (root_prompt , i , context_count , history_count )
216+ ]
181217
182218 iteration : RLMIteration = self ._completion_turn (
183219 prompt = current_prompt ,
@@ -201,6 +237,11 @@ def completion(
201237 usage = lm_handler .get_usage_summary ()
202238 self .verbose .print_final_answer (final_answer )
203239 self .verbose .print_summary (i + 1 , time_end - time_start , usage .to_dict ())
240+
241+ # Store message history in persistent environment
242+ if self .persistent and isinstance (environment , SupportsPersistence ):
243+ environment .add_history (message_history )
244+
204245 return RLMChatCompletion (
205246 root_model = self .backend_kwargs .get ("model_name" , "unknown" )
206247 if self .backend_kwargs
@@ -223,6 +264,11 @@ def completion(
223264 usage = lm_handler .get_usage_summary ()
224265 self .verbose .print_final_answer (final_answer )
225266 self .verbose .print_summary (self .max_iterations , time_end - time_start , usage .to_dict ())
267+
268+ # Store message history in persistent environment
269+ if self .persistent and isinstance (environment , SupportsPersistence ):
270+ environment .add_history (message_history )
271+
226272 return RLMChatCompletion (
227273 root_model = self .backend_kwargs .get ("model_name" , "unknown" )
228274 if self .backend_kwargs
@@ -292,3 +338,47 @@ def _fallback_answer(self, message: str | dict[str, Any]) -> str:
292338 client : BaseLM = get_client (self .backend , self .backend_kwargs )
293339 response = client .completion (message )
294340 return response
341+
342+ def _validate_persistent_environment_support (self ) -> None :
343+ """
344+ Validate that the configured environment type supports persistent mode.
345+
346+ Persistent mode requires environments to implement:
347+ - update_handler_address(address): Update LM handler address between calls
348+ - add_context(payload, index): Add new context for multi-turn conversations
349+ - get_context_count(): Return the number of loaded contexts
350+
351+ Currently only 'local' (LocalREPL) supports these methods.
352+
353+ Raises:
354+ ValueError: If the environment type does not support persistent mode.
355+ """
356+ # Known environments that support persistence
357+ persistent_supported_environments = {"local" }
358+
359+ if self .environment_type not in persistent_supported_environments :
360+ raise ValueError (
361+ f"persistent=True is not supported for environment type '{ self .environment_type } '. "
362+ f"Persistent mode requires environments that implement update_handler_address(), "
363+ f"add_context(), and get_context_count(). "
364+ f"Supported environments: { sorted (persistent_supported_environments )} "
365+ )
366+
367+ @staticmethod
368+ def _env_supports_persistence (env : BaseEnv ) -> bool :
369+ """Check if an environment instance supports persistent mode methods."""
370+ return isinstance (env , SupportsPersistence )
371+
372+ def close (self ) -> None :
373+ """Clean up persistent environment. Call when done with multi-turn conversations."""
374+ if self ._persistent_env is not None :
375+ if hasattr (self ._persistent_env , "cleanup" ):
376+ self ._persistent_env .cleanup ()
377+ self ._persistent_env = None
378+
379+ def __enter__ (self ) -> "RLM" :
380+ return self
381+
382+ def __exit__ (self , exc_type , exc_val , exc_tb ) -> bool :
383+ self .close ()
384+ return False
0 commit comments