From 4dd6eb3965c87142134a75195e90ec3cfa342ed9 Mon Sep 17 00:00:00 2001 From: rasdani <73563550+rasdani@users.noreply.github.com> Date: Wed, 31 Dec 2025 22:34:21 +0000 Subject: [PATCH 1/3] filter func signature + add test --- tests/test_stateful_tool_env.py | 13 +++++++++++++ verifiers/envs/stateful_tool_env.py | 21 ++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/tests/test_stateful_tool_env.py b/tests/test_stateful_tool_env.py index 532ca6913..4269adba4 100644 --- a/tests/test_stateful_tool_env.py +++ b/tests/test_stateful_tool_env.py @@ -85,6 +85,19 @@ def test_stateful_tool_env_add_tool_skips_args(self, mock_stateful_tool_env): assert mock_stateful_tool_env.skipped_args["secret_tool"] == ["secret"] assert "secret_tool" in mock_stateful_tool_env.tool_map + def test_add_tool_skips_dict_type_args(self, mock_stateful_tool_env): + def tool_with_dict(command: str, state: dict | None = None) -> str: + return command + + mock_stateful_tool_env.add_tool(tool_with_dict, args_to_skip=["state"]) + + schema = next( + t + for t in mock_stateful_tool_env.oai_tools + if t["function"]["name"] == "tool_with_dict" + ) + assert "state" not in schema["function"]["parameters"]["properties"] + @pytest.mark.asyncio async def test_tool_env_tool_invalid_json_arguments( self, mock_openai_client, sample_chat_dataset diff --git a/verifiers/envs/stateful_tool_env.py b/verifiers/envs/stateful_tool_env.py index 8dbccbd1b..208e5902c 100644 --- a/verifiers/envs/stateful_tool_env.py +++ b/verifiers/envs/stateful_tool_env.py @@ -1,3 +1,4 @@ +import inspect import json from abc import abstractmethod from typing import Callable, cast @@ -11,6 +12,24 @@ from verifiers.utils.tool_utils import convert_func_to_oai_tool +def filter_signature(func, args_to_skip): + if not args_to_skip: + return func + sig = inspect.signature(func) + target = getattr(func, "__func__", func) + target.__signature__ = sig.replace( + parameters=[ + p + for n, p in sig.parameters.items() + if n not in args_to_skip and n != "self" + ] + ) + target.__annotations__ = { + k: v for k, v in target.__annotations__.items() if k not in args_to_skip + } + return func + + class StatefulToolEnv(vf.ToolEnv): def __init__( self, @@ -48,7 +67,7 @@ def add_tool(self, tool: Callable, args_to_skip: list[str] = []): Assumes all non-skipped args use standard JSON types (no remaining $ref/$defs). """ self.tools.append(tool) - oai_tool = convert_func_to_oai_tool(tool) + oai_tool = convert_func_to_oai_tool(filter_signature(tool, args_to_skip)) assert "function" in oai_tool assert "parameters" in oai_tool["function"] params = oai_tool["function"]["parameters"] From 6697842bd62479830a1f30c9827a478b46b3c6fa Mon Sep 17 00:00:00 2001 From: rasdani <73563550+rasdani@users.noreply.github.com> Date: Fri, 2 Jan 2026 19:18:17 +0000 Subject: [PATCH 2/3] dont mutate original function --- tests/test_stateful_tool_env.py | 58 +++++++++++++++++++++++++++++ verifiers/envs/stateful_tool_env.py | 23 +++++++++--- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/tests/test_stateful_tool_env.py b/tests/test_stateful_tool_env.py index 4269adba4..9ce0b1bfb 100644 --- a/tests/test_stateful_tool_env.py +++ b/tests/test_stateful_tool_env.py @@ -98,6 +98,64 @@ def tool_with_dict(command: str, state: dict | None = None) -> str: ) assert "state" not in schema["function"]["parameters"]["properties"] + def test_add_tool_does_not_mutate_original_signature(self, mock_stateful_tool_env): + """Verify that add_tool with args_to_skip doesn't mutate the original function.""" + import inspect + + def my_tool(command: str, hidden: int, visible: bool = True) -> str: + """A tool with multiple parameters.""" + return command + + original_params = list(inspect.signature(my_tool).parameters.keys()) + original_annotations = dict(my_tool.__annotations__) + + mock_stateful_tool_env.add_tool(my_tool, args_to_skip=["hidden"]) + + # Original function signature should be unchanged + assert list(inspect.signature(my_tool).parameters.keys()) == original_params + assert my_tool.__annotations__ == original_annotations + assert "hidden" in inspect.signature(my_tool).parameters + + # But schema should have hidden removed + schema = next( + t + for t in mock_stateful_tool_env.oai_tools + if t["function"]["name"] == "my_tool" + ) + assert "hidden" not in schema["function"]["parameters"]["properties"] + assert "command" in schema["function"]["parameters"]["properties"] + + def test_add_tool_does_not_mutate_bound_method_signature( + self, mock_stateful_tool_env + ): + """Verify that add_tool with args_to_skip doesn't mutate bound method signatures.""" + import inspect + + class ToolProvider: + def my_tool(self, command: str, hidden: int, visible: bool = True) -> str: + """A tool with multiple parameters.""" + return command + + bound_method = ToolProvider().my_tool + original_params = list(inspect.signature(bound_method).parameters.keys()) + + mock_stateful_tool_env.add_tool(bound_method, args_to_skip=["hidden"]) + + # Original bound method signature should be unchanged + assert ( + list(inspect.signature(bound_method).parameters.keys()) == original_params + ) + assert "hidden" in inspect.signature(bound_method).parameters + + # But schema should have hidden removed + schema = next( + t + for t in mock_stateful_tool_env.oai_tools + if t["function"]["name"] == "my_tool" + ) + assert "hidden" not in schema["function"]["parameters"]["properties"] + assert "command" in schema["function"]["parameters"]["properties"] + @pytest.mark.asyncio async def test_tool_env_tool_invalid_json_arguments( self, mock_openai_client, sample_chat_dataset diff --git a/verifiers/envs/stateful_tool_env.py b/verifiers/envs/stateful_tool_env.py index 208e5902c..a3d041b48 100644 --- a/verifiers/envs/stateful_tool_env.py +++ b/verifiers/envs/stateful_tool_env.py @@ -13,21 +13,34 @@ def filter_signature(func, args_to_skip): + """Return a wrapper with filtered signature for schema generation. + + Does not mutate the original function. + """ if not args_to_skip: return func sig = inspect.signature(func) - target = getattr(func, "__func__", func) - target.__signature__ = sig.replace( + filtered_sig = sig.replace( parameters=[ p for n, p in sig.parameters.items() if n not in args_to_skip and n != "self" ] ) - target.__annotations__ = { - k: v for k, v in target.__annotations__.items() if k not in args_to_skip + filtered_annotations = { + k: v + for k, v in getattr(func, "__annotations__", {}).items() + if k not in args_to_skip } - return func + + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + wrapper.__name__ = getattr(func, "__name__", "unknown") + wrapper.__doc__ = getattr(func, "__doc__", None) + wrapper.__signature__ = filtered_sig + wrapper.__annotations__ = filtered_annotations + return wrapper class StatefulToolEnv(vf.ToolEnv): From 0860463477858dcc35f14cd60a15e807381f81f9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 7 Jan 2026 20:11:39 +0000 Subject: [PATCH 3/3] Refactor: Use setattr for wrapper attributes in stateful_tool_env Co-authored-by: daniel --- verifiers/envs/stateful_tool_env.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/verifiers/envs/stateful_tool_env.py b/verifiers/envs/stateful_tool_env.py index a3d041b48..90381bc8d 100644 --- a/verifiers/envs/stateful_tool_env.py +++ b/verifiers/envs/stateful_tool_env.py @@ -36,10 +36,10 @@ def filter_signature(func, args_to_skip): def wrapper(*args, **kwargs): return func(*args, **kwargs) - wrapper.__name__ = getattr(func, "__name__", "unknown") - wrapper.__doc__ = getattr(func, "__doc__", None) - wrapper.__signature__ = filtered_sig - wrapper.__annotations__ = filtered_annotations + setattr(wrapper, "__name__", getattr(func, "__name__", "unknown")) + setattr(wrapper, "__doc__", getattr(func, "__doc__", None)) + setattr(wrapper, "__signature__", filtered_sig) + setattr(wrapper, "__annotations__", filtered_annotations) return wrapper