diff --git a/rlm/environments/local_repl.py b/rlm/environments/local_repl.py index 5aa1b69..57301e4 100644 --- a/rlm/environments/local_repl.py +++ b/rlm/environments/local_repl.py @@ -1,6 +1,5 @@ import copy import io -import json import os import shutil import sys @@ -250,22 +249,16 @@ def add_context( var_name = f"context_{context_index}" + # Store context directly in locals (with isolation for mutable types) if isinstance(context_payload, str): - context_path = os.path.join(self.temp_dir, f"context_{context_index}.txt") - with open(context_path, "w") as f: - f.write(context_payload) - self.execute_code(f"with open(r'{context_path}', 'r') as f:\n {var_name} = f.read()") + self.locals[var_name] = context_payload else: - context_path = os.path.join(self.temp_dir, f"context_{context_index}.json") - with open(context_path, "w") as f: - json.dump(context_payload, f) - self.execute_code( - f"import json\nwith open(r'{context_path}', 'r') as f:\n {var_name} = json.load(f)" - ) + # Dicts/lists - deep copy for isolation as per SupportsPersistence protocol + self.locals[var_name] = copy.deepcopy(context_payload) # Alias context_0 as 'context' for backward compatibility if context_index == 0: - self.execute_code(f"context = {var_name}") + self.locals["context"] = self.locals[var_name] self._context_count = max(self._context_count, context_index + 1) return context_index diff --git a/tests/test_local_repl_persistent.py b/tests/test_local_repl_persistent.py index f654679..30ede83 100644 --- a/tests/test_local_repl_persistent.py +++ b/tests/test_local_repl_persistent.py @@ -63,6 +63,40 @@ def test_context_alias_points_to_first(self): assert repl.locals["is_first"] is True repl.cleanup() + def test_context_dict_is_copy(self): + """Test that dict context is a deep copy, not a reference.""" + repl = LocalREPL() + + context_dict = {"key": "original", "nested": {"inner": "value"}} + repl.add_context(context_dict) + + # Modify the original after adding + context_dict["key"] = "modified" + context_dict["nested"]["inner"] = "modified_inner" + + # Stored context should remain unchanged + assert repl.locals["context_0"]["key"] == "original" + assert repl.locals["context_0"]["nested"]["inner"] == "value" + + repl.cleanup() + + def test_context_list_is_copy(self): + """Test that list context is a deep copy, not a reference.""" + repl = LocalREPL() + + context_list = [{"item": "original"}, [1, 2, 3]] + repl.add_context(context_list) + + # Modify the original after adding + context_list[0]["item"] = "modified" + context_list[1].append(4) + + # Stored context should remain unchanged + assert repl.locals["context_0"][0]["item"] == "original" + assert repl.locals["context_0"][1] == [1, 2, 3] + + repl.cleanup() + class TestLocalREPLHistory: """Tests for message history storage in LocalREPL for persistent sessions."""