Skip to content

Commit 1bb0112

Browse files
BinglanLiclaude
andcommitted
feat: REPL namespace isolation (Feature 3)
Each BaseAgent instance now owns an isolated Python execution environment: - `self._repl_namespace`: per-instance dict passed to exec(); variables set in one agent's <execute> blocks are invisible to other agents - `self._plot_capture`: per-instance PlotCapture replacing module-level _captured_plots; each agent captures matplotlib output independently - `run_python_repl(command, namespace=None)`: new namespace param; falls back to _persistent_namespace when None for backward compatibility - `inject_custom_functions_to_repl(fns, namespace=None)`: new namespace param; custom tools are injected into the per-instance namespace - NodeExecutor.execute() passes agent._repl_namespace to run_python_repl and calls agent._plot_capture.apply_patches() before each Python execution - 24 new tests in test_repl_isolation.py; updated test_add_tool.py and test_mcp_integration.py to reflect per-instance injection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3cf7b72 commit 1bb0112

11 files changed

Lines changed: 499 additions & 117 deletions

BaseAgent/base_agent.py

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
from BaseAgent.config import default_config
4141
from BaseAgent.resource_manager import ResourceManager
4242
from BaseAgent.retriever import ToolRetriever
43-
from BaseAgent.tools.support_tools import run_python_repl
43+
from BaseAgent.tools.support_tools import PlotCapture, run_python_repl
4444
from BaseAgent.env_desc import data_lake_items, libraries
4545
from BaseAgent.utils.tool_bridge import inject_custom_functions_to_repl
4646
from BaseAgent.utils.formatting import pretty_print
@@ -113,6 +113,10 @@ def __init__(
113113
self._run_config: dict | None = None
114114
self.state = AgentState(input=[], next_step=None, pending_code=None, pending_language=None)
115115
self._usage_metrics = []
116+
# Per-instance REPL namespace — isolates variables from other BaseAgent instances
117+
self._repl_namespace: dict = {}
118+
# Per-instance plot capture — isolates matplotlib output from other instances
119+
self._plot_capture = PlotCapture()
116120
#TODO: add self-critic mode
117121
self.self_critic = False
118122

@@ -1408,40 +1412,27 @@ def _map_langgraph_event(self, event: dict) -> "AgentEvent | None":
14081412
return None
14091413

14101414
def _clear_execution_plots(self):
1411-
"""
1412-
Clear execution plots before new execution.
1413-
1414-
This function clears any previously captured plots from the execution environment
1415-
before starting a new execution. This prevents old plots from appearing in
1416-
new execution results.
1417-
1418-
Note:
1419-
This function calls the clear_captured_plots utility function and handles
1420-
any exceptions gracefully to prevent execution failures.
1421-
"""
1415+
"""Clear the per-instance plot capture buffer before a new execution."""
14221416
try:
1423-
from BaseAgent.tools.support_tools import clear_captured_plots
1424-
1425-
clear_captured_plots()
1417+
self._plot_capture.clear()
14261418
except Exception as e:
14271419
print(f"Warning: Could not clear execution plots: {e}")
14281420

14291421

14301422
def _inject_custom_functions_to_repl(self):
1423+
"""Inject custom tools into the per-instance REPL namespace.
1424+
1425+
Makes custom tools added via ``add_tool()`` / ``add_mcp()`` callable
1426+
inside ``<execute>`` blocks. Uses ``self._repl_namespace`` so that
1427+
injected functions are isolated to this agent instance.
14311428
"""
1432-
Inject custom functions into the Python REPL execution environment.
1433-
This makes custom tools from ResourceManager available during code execution.
1434-
"""
1435-
# Get custom tools from ResourceManager
14361429
custom_tools = self.resource_manager.collection.custom_tools
1437-
1438-
# Extract callable functions from CustomTool objects
1439-
custom_functions = {}
1440-
for tool in custom_tools:
1441-
if tool.function is not None:
1442-
custom_functions[tool.name] = tool.function
1443-
1444-
inject_custom_functions_to_repl(custom_functions)
1430+
custom_functions = {
1431+
tool.name: tool.function
1432+
for tool in custom_tools
1433+
if tool.function is not None
1434+
}
1435+
inject_custom_functions_to_repl(custom_functions, namespace=self._repl_namespace)
14451436

14461437

14471438
def _record_usage(self, usage):

BaseAgent/nodes.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,15 @@ def execute(self, state: "AgentState") -> "AgentState":
144144
stripped_code = stripped_code.replace("\n", " ")
145145
result = run_with_timeout(run_bash_script, [stripped_code], timeout=timeout)
146146
else:
147-
# Python: clear previous plots and inject custom functions
148-
agent._clear_execution_plots()
147+
# Python: clear plots, activate per-instance patches, inject custom tools
148+
agent._plot_capture.clear()
149+
agent._plot_capture.apply_patches()
149150
agent._inject_custom_functions_to_repl()
150-
result = run_with_timeout(run_python_repl, [code], timeout=timeout)
151+
result = run_with_timeout(
152+
run_python_repl,
153+
[code, agent._repl_namespace],
154+
timeout=timeout,
155+
)
151156

152157
if len(result) > 10000:
153158
result = (
@@ -159,13 +164,10 @@ def execute(self, state: "AgentState") -> "AgentState":
159164
if not hasattr(agent, "_execution_results"):
160165
agent._execution_results = []
161166

162-
# Get any plots that were generated during this execution
167+
# Get any plots captured during this execution (per-instance buffer)
163168
execution_plots = []
164169
try:
165-
from BaseAgent.tools.support_tools import get_captured_plots
166-
167-
current_plots = get_captured_plots()
168-
execution_plots = current_plots.copy()
170+
execution_plots = agent._plot_capture.get_plots()
169171
except Exception as e:
170172
print(f"Warning: Could not capture plots from execution: {e}")
171173
execution_plots = []

BaseAgent/tests/test_add_tool.py

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -104,45 +104,38 @@ def untyped_func(a, b, c=10):
104104

105105
class TestREPLInjection:
106106
"""Test REPL namespace injection."""
107-
108-
def test_repl_injection(self, base_agent: BaseAgent, clear_repl_namespace):
109-
"""Test that custom tools are available in REPL execution."""
110-
# Add a custom function
107+
108+
def test_repl_injection(self, base_agent: BaseAgent):
109+
"""Custom tools are injected into the agent's per-instance namespace."""
111110
def multiply(a: float, b: float) -> float:
112111
"""Multiply two numbers."""
113112
return a * b
114-
113+
115114
base_agent.add_tool(multiply)
116-
117-
# Inject into REPL
118115
base_agent._inject_custom_functions_to_repl()
119-
120-
# Check if it's in the persistent namespace
121-
assert 'multiply' in _persistent_namespace
122-
result = _persistent_namespace['multiply'](5, 7)
123-
assert result == 35
124-
125-
def test_multiple_functions_injection(self, base_agent: BaseAgent, math_functions: tuple, clear_repl_namespace):
126-
"""Test injecting multiple functions into REPL."""
116+
117+
# Injected into the per-instance namespace (not global)
118+
ns = base_agent._repl_namespace
119+
assert 'multiply' in ns
120+
assert ns['multiply'](5, 7) == 35
121+
122+
def test_multiple_functions_injection(self, base_agent: BaseAgent, math_functions: tuple):
123+
"""Multiple custom tools are all injected into the per-instance namespace."""
127124
add_func, mult_func, pow_func = math_functions
128-
129-
# Add all functions
125+
130126
base_agent.add_tool(add_func)
131127
base_agent.add_tool(mult_func)
132128
base_agent.add_tool(pow_func)
133-
134-
# Inject into REPL
135129
base_agent._inject_custom_functions_to_repl()
136-
137-
# Verify all are in namespace
138-
assert 'add_numbers' in _persistent_namespace
139-
assert 'multiply_numbers' in _persistent_namespace
140-
assert 'power' in _persistent_namespace
141-
142-
# Verify they're callable
143-
assert _persistent_namespace['add_numbers'](5, 3) == 8
144-
assert _persistent_namespace['multiply_numbers'](5, 3) == 15
145-
assert _persistent_namespace['power'](5, 2) == 25
130+
131+
ns = base_agent._repl_namespace
132+
assert 'add_numbers' in ns
133+
assert 'multiply_numbers' in ns
134+
assert 'power' in ns
135+
136+
assert ns['add_numbers'](5, 3) == 8
137+
assert ns['multiply_numbers'](5, 3) == 15
138+
assert ns['power'](5, 2) == 25
146139

147140

148141
class TestPromptGeneration:

BaseAgent/tests/test_mcp_integration.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -99,25 +99,20 @@ def test_mcp_tools_selected_by_default(self, base_agent: BaseAgent, mcp_config_p
9999
class TestMCPREPLInjection:
100100
"""Test MCP tool injection into REPL namespace."""
101101

102-
def test_mcp_repl_injection(self, base_agent: BaseAgent, mcp_config_path: Path, skip_if_no_config, clear_repl_namespace):
103-
"""Test that MCP tools are available in REPL."""
104-
# Add MCP
102+
def test_mcp_repl_injection(self, base_agent: BaseAgent, mcp_config_path: Path, skip_if_no_config):
103+
"""Test that MCP tools are injected into the per-instance REPL namespace."""
105104
base_agent.add_mcp(mcp_config_path)
106-
107-
# Inject into REPL
108105
base_agent._inject_custom_functions_to_repl()
109-
110-
# Check namespace
106+
111107
mcp_tools = base_agent.resource_manager.collection.custom_tools
112-
113108
_assert_mcp_tools_loaded(mcp_tools)
114-
# At least one tool should be in namespace
115-
tools_in_namespace = [tool for tool in mcp_tools if tool.name in _persistent_namespace]
116-
assert len(tools_in_namespace) > 0, "No MCP tools found in REPL namespace"
117-
118-
# Verify callable
109+
110+
ns = base_agent._repl_namespace
111+
tools_in_namespace = [tool for tool in mcp_tools if tool.name in ns]
112+
assert len(tools_in_namespace) > 0, "No MCP tools found in per-instance REPL namespace"
113+
119114
for tool in tools_in_namespace:
120-
assert callable(_persistent_namespace[tool.name]), f"Tool {tool.name} in namespace is not callable"
115+
assert callable(ns[tool.name]), f"Tool {tool.name} in namespace is not callable"
121116

122117

123118
class TestMCPPromptGeneration:

0 commit comments

Comments
 (0)