diff --git a/src/rlm/tools/ipython.py b/src/rlm/tools/ipython.py index 230506e..0eccf8f 100644 --- a/src/rlm/tools/ipython.py +++ b/src/rlm/tools/ipython.py @@ -74,6 +74,19 @@ def schema(self) -> dict[str, Any]: return schema def execute(self, args: dict[str, Any], context: ToolContext) -> ToolOutcome: + # A missing "code" key used to fall through to "" and execute nothing + # silently — the model would then hit NameErrors on state it thought it + # created. Laguna in particular emits the arg under "cmd". Turn any such + # call into a loud, learnable error instead of a no-op. + if "code" not in args or args.get("code") in (None, ""): + wrong = ", ".join(repr(k) for k in args if k != "timeout") or "none" + return ToolOutcome( + content=( + "Error: the ipython tool requires a non-empty 'code' argument; " + f"got keys: {wrong}. Pass your Python under the 'code' key." + ), + metric_events=[IpythonExecuted(input_chars=0, input_loc=0)], + ) code = args.get("code", "") if not isinstance(code, str): code = str(code) diff --git a/tests/test_tools.py b/tests/test_tools.py index 3cd6ce6..58e8244 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -92,3 +92,33 @@ def test_ipython_kernel_does_not_inherit_parent_stdio(session, capfd): assert captured.out == "" assert captured.err == "" assert result.strip() == "4 14" + + +def _ipython_ctx(): + from rlm.tools.base import ToolContext + from rlm.types import RLMMetrics, TokenUsage + + return ToolContext( + messages=[], + metrics=RLMMetrics(), + total_usage=TokenUsage(), + last_prompt_tokens=0, + exec_timeout=30, + repl=None, # error paths return before touching the REPL + ) + + +def test_ipython_missing_code_key_errors_not_noop(): + """A tool call under the wrong key (e.g. Laguna's 'cmd') must error loudly.""" + from rlm.tools.ipython import IpythonTool + + out = IpythonTool().execute({"cmd": "print(1)"}, _ipython_ctx()) + assert "Error" in out.content and "code" in out.content + assert "'cmd'" in out.content + + +def test_ipython_empty_code_errors(): + from rlm.tools.ipython import IpythonTool + + out = IpythonTool().execute({"code": ""}, _ipython_ctx()) + assert "Error" in out.content and "code" in out.content