Skip to content

Commit bcb9b25

Browse files
authored
Ensure tool calls and reserved names aren't overwritten + add logger metadata (#109)
* ensure tool calls and reserved names aren't overwritten * add logger examples + extra logging func
1 parent 90bd477 commit bcb9b25

14 files changed

Lines changed: 250 additions & 37 deletions

File tree

‎README.md‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,20 @@ If you use this code or repository in your research, please cite:
135135
}
136136
```
137137

138+
## Optional: Trajectory metadata and logging
139+
`RLMChatCompletion` has an optional `metadata` field (default empty) that can hold the full trajectory (run config + all iterations and sub-calls) so you can reconstruct the run. Pass an `RLMLogger` to capture it:
140+
141+
- **In-memory only** (trajectory on `completion.metadata`): `logger=RLMLogger()` (no `log_dir`).
142+
- **Also save to disk** (JSONL for the visualizer): `logger=RLMLogger(log_dir="./logs")`.
143+
138144
## Optional Debugging: Visualizing RLM Trajectories
139-
We additionally provide a simple visualizer tool to examine and view the code, sub-LM, and root-LM calls of an RLM trajectory. To save log files (`.jsonl`) on every completion call that can be viewed in the visualizer, initialize the `RLMLogger` object and pass it into the `RLM` on initialization:
145+
We provide a simple visualizer to inspect code, sub-LM, and root-LM calls. Use `RLMLogger(log_dir="./logs")` so each completion writes a `.jsonl` file:
140146
```python
141147
from rlm.logger import RLMLogger
142148
from rlm import RLM
143149

144150
logger = RLMLogger(log_dir="./logs")
145-
rlm = RLM(
146-
...
147-
logger=logger
148-
)
151+
rlm = RLM(..., logger=logger)
149152
```
150153

151154
To run the visualizer locally, we use Node.js and shadcn/ui:

‎examples/logger_example.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
Logger example: same structure as quickstart, with a different prompt.
3+
Shows trajectory capture in completion.metadata (and optional save to disk).
4+
"""
5+
6+
import os
7+
8+
from dotenv import load_dotenv
9+
10+
from rlm import RLM
11+
from rlm.logger import RLMLogger
12+
13+
load_dotenv()
14+
15+
# In-memory only: trajectory on completion.metadata (no disk write)
16+
logger = RLMLogger()
17+
# To also save JSONL for the visualizer: logger = RLMLogger(log_dir="./logs")
18+
19+
rlm = RLM(
20+
backend="portkey",
21+
backend_kwargs={
22+
"model_name": "@openai/gpt-5-nano",
23+
"api_key": os.getenv("PORTKEY_API_KEY"),
24+
},
25+
environment="local",
26+
environment_kwargs={},
27+
max_depth=1,
28+
logger=logger,
29+
verbose=True,
30+
)
31+
32+
prompt = "Compute the sum 1 + 2 + 3 + 4 + 5 and print the result using a REPL block, then return it with FINAL_VAR."
33+
result = rlm.completion(prompt)
34+
35+
print("Response:", result.response)
36+
if result.metadata:
37+
traj = result.metadata
38+
print("Trajectory: run_metadata +", len(traj.get("iterations", [])), "iterations")
39+
print("Trajectory:", traj)
40+
if traj.get("iterations"):
41+
first = traj["iterations"][0]
42+
print(" First iteration keys:", list(first.keys()))
43+
else:
44+
print("Trajectory: (none — no logger or metadata not captured)")
45+
46+
print("Full response:", result)

‎rlm/core/rlm.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ def completion(
229229
if self.depth >= self.max_depth:
230230
return self._fallback_answer(prompt)
231231

232+
if self.logger:
233+
self.logger.clear_iterations()
234+
232235
with self._spawn_completion_context(prompt) as (lm_handler, environment):
233236
message_history = self._setup_prompt(prompt)
234237

@@ -283,6 +286,7 @@ def completion(
283286
response=final_answer,
284287
usage_summary=usage,
285288
execution_time=time_end - time_start,
289+
metadata=self.logger.get_trajectory() if self.logger else None,
286290
)
287291

288292
# Format the iteration for the next prompt.
@@ -310,6 +314,7 @@ def completion(
310314
response=final_answer,
311315
usage_summary=usage,
312316
execution_time=time_end - time_start,
317+
metadata=self.logger.get_trajectory() if self.logger else None,
313318
)
314319

315320
def _completion_turn(

‎rlm/core/types.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,21 @@ class RLMChatCompletion:
9696
response: str
9797
usage_summary: UsageSummary
9898
execution_time: float
99+
metadata: dict | None = (
100+
None # Full trajectory (run_metadata + iterations) when logger captures it
101+
)
99102

100103
def to_dict(self):
101-
return {
104+
out = {
102105
"root_model": self.root_model,
103106
"prompt": self.prompt,
104107
"response": self.response,
105108
"usage_summary": self.usage_summary.to_dict(),
106109
"execution_time": self.execution_time,
107110
}
111+
if self.metadata is not None:
112+
out["metadata"] = self.metadata
113+
return out
108114

109115
@classmethod
110116
def from_dict(cls, data: dict) -> "RLMChatCompletion":
@@ -114,6 +120,7 @@ def from_dict(cls, data: dict) -> "RLMChatCompletion":
114120
response=data.get("response"),
115121
usage_summary=UsageSummary.from_dict(data.get("usage_summary")),
116122
execution_time=data.get("execution_time"),
123+
metadata=data.get("metadata"),
117124
)
118125

119126

‎rlm/environments/base_env.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@
88
# Custom Tools Support
99
# =============================================================================
1010

11-
# Reserved names that cannot be overridden by custom tools.
12-
# These are the built-in REPL functions that all environments must provide.
11+
# Reserved names: cannot be overridden by custom tools, and are restored after each
12+
# code execution to prevent namespace corruption (e.g. context = "...", llm_query = ...).
1313
RESERVED_TOOL_NAMES: frozenset[str] = frozenset(
1414
{
1515
"llm_query",
1616
"llm_query_batched",
1717
"FINAL_VAR",
1818
"SHOW_VARS",
1919
"context",
20+
"history",
2021
}
2122
)
2223

@@ -173,7 +174,7 @@ class SupportsCustomTools(Protocol):
173174
The following names cannot be used as custom tool names:
174175
- llm_query, llm_query_batched: Single LM completion functions (no tool access)
175176
- FINAL_VAR, SHOW_VARS: Built-in helper functions
176-
- context: The input context variable
177+
- context, history: The input context and conversation history variables
177178
178179
EXAMPLE:
179180
custom_tools = {

‎rlm/environments/daytona_repl.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,12 @@ def SHOW_VARS():
349349
sys.stdout = old_stdout
350350
sys.stderr = old_stderr
351351
352+
# Restore scaffold aliases if overwritten by executed code
353+
if "context_0" in _locals:
354+
_locals["context"] = _locals["context_0"]
355+
if "history_0" in _locals:
356+
_locals["history"] = _locals["history_0"]
357+
352358
save_state(_locals)
353359
354360
result = {{

‎rlm/environments/docker_repl.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ def SHOW_VARS():
173173
finally:
174174
sys.stdout, sys.stderr = old_stdout, old_stderr
175175
176+
# Restore scaffold aliases if overwritten by executed code
177+
if "context_0" in _locals:
178+
_locals["context"] = _locals["context_0"]
179+
if "history_0" in _locals:
180+
_locals["history"] = _locals["history_0"]
181+
176182
save_state(_locals)
177183
print(json.dumps({{"stdout": stdout_buf.getvalue(), "stderr": stderr_buf.getvalue(), "locals": {{k: repr(v) for k, v in _locals.items() if not k.startswith("_")}}}}, ensure_ascii=False))
178184
'''

‎rlm/environments/e2b_repl.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,12 @@ def FINAL_VAR(variable_name):
241241
sys.stdout = old_stdout
242242
sys.stderr = old_stderr
243243
244+
# Restore scaffold aliases if overwritten by executed code
245+
if "context_0" in _locals:
246+
_locals["context"] = _locals["context_0"]
247+
if "history_0" in _locals:
248+
_locals["history"] = _locals["history_0"]
249+
244250
save_state(_locals)
245251
246252
result = {{

‎rlm/environments/local_repl.py‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313

1414
from rlm.core.comms_utils import LMRequest, send_lm_request, send_lm_request_batched
1515
from rlm.core.types import REPLResult, RLMChatCompletion
16-
from rlm.environments.base_env import NonIsolatedEnv, extract_tool_value, validate_custom_tools
16+
from rlm.environments.base_env import (
17+
RESERVED_TOOL_NAMES,
18+
NonIsolatedEnv,
19+
extract_tool_value,
20+
validate_custom_tools,
21+
)
1722

1823
# =============================================================================
1924
# Safe Builtins
@@ -375,6 +380,22 @@ def _temp_cwd(self):
375380
finally:
376381
os.chdir(old_cwd)
377382

383+
def _restore_scaffold(self) -> None:
384+
"""Restore scaffold names after execution so overwrites (e.g. context = 'x') don't persist."""
385+
for name in RESERVED_TOOL_NAMES:
386+
if name == "llm_query":
387+
self.globals["llm_query"] = self._llm_query
388+
elif name == "llm_query_batched":
389+
self.globals["llm_query_batched"] = self._llm_query_batched
390+
elif name == "FINAL_VAR":
391+
self.globals["FINAL_VAR"] = self._final_var
392+
elif name == "SHOW_VARS":
393+
self.globals["SHOW_VARS"] = self._show_vars
394+
elif name == "context" and "context_0" in self.locals:
395+
self.locals["context"] = self.locals["context_0"]
396+
elif name == "history" and "history_0" in self.locals:
397+
self.locals["history"] = self.locals["history_0"]
398+
378399
def execute_code(self, code: str) -> REPLResult:
379400
"""Execute code in the persistent namespace and return result."""
380401
start_time = time.perf_counter()
@@ -392,6 +413,9 @@ def execute_code(self, code: str) -> REPLResult:
392413
if key not in self.globals and not key.startswith("_"):
393414
self.locals[key] = value
394415

416+
# Restore scaffold so model overwrites (context = ..., llm_query = ...) don't persist
417+
self._restore_scaffold()
418+
395419
stdout = stdout_buf.getvalue()
396420
stderr = stderr_buf.getvalue()
397421
except Exception as e:

‎rlm/environments/modal_repl.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,12 @@ def SHOW_VARS():
261261
sys.stdout = old_stdout
262262
sys.stderr = old_stderr
263263
264+
# Restore scaffold aliases if overwritten by executed code
265+
if "context_0" in _locals:
266+
_locals["context"] = _locals["context_0"]
267+
if "history_0" in _locals:
268+
_locals["history"] = _locals["history_0"]
269+
264270
save_state(_locals)
265271
266272
result = {{

0 commit comments

Comments
 (0)