From 16c798b5c0d49b079a64f868454a6cd3e88ee791 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 14 Apr 2026 15:51:01 +0000 Subject: [PATCH] fix: add git and reset tool name aliases for trinity-large-thinking Add 'git' and 'reset' tool name aliases that map to 'terminal' to fix errors like 'Tool 'git' not found' and 'Tool 'reset' not found' seen in trinity-large-thinking evaluation. For these aliases, the tool name is prepended to the command argument (e.g., 'git status' from tool_name='git' + command='status'). Unlike 'bash' which passes through the command directly, these tools are themselves commands that should be combined with their arguments. Also adds 'git' to _SHELL_TOOL_FALLBACK_COMMANDS so 'git' without arguments still falls back to terminal. Co-authored-by: openhands --- openhands-sdk/openhands/sdk/agent/utils.py | 25 ++++++++- .../sdk/agent/test_tool_call_compatibility.py | 56 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/openhands-sdk/openhands/sdk/agent/utils.py b/openhands-sdk/openhands/sdk/agent/utils.py index 3591515d09..a94b53e817 100644 --- a/openhands-sdk/openhands/sdk/agent/utils.py +++ b/openhands-sdk/openhands/sdk/agent/utils.py @@ -175,13 +175,21 @@ def fix_malformed_tool_arguments( "command": "terminal", "execute": "terminal", "execute_bash": "terminal", + "git": "terminal", + "reset": "terminal", "str_replace": "file_editor", "str_replace_editor": "file_editor", } +# Terminal aliases that prepend the tool name to the command argument. +# Unlike 'bash' which passes through the command directly, these tools +# (e.g., 'git', 'reset') are themselves commands that should be combined +# with their arguments (e.g., 'git status', 'reset clear'). +_TERMINAL_COMMAND_PREFIX_ALIASES = frozenset({"git", "reset"}) + # This fallback is intentionally tiny: it only accepts exact, bare command names # that are useful as read-only defaults when some models emit them as tool names. -_SHELL_TOOL_FALLBACK_COMMANDS = frozenset({"find", "ls", "pwd"}) +_SHELL_TOOL_FALLBACK_COMMANDS = frozenset({"find", "git", "ls", "pwd"}) # Typo normalization for common mistakes in security_risk field _SECURITY_RISK_TYPOS = {"security_rort", "securtiy_risk", "security_riks"} @@ -306,6 +314,21 @@ def normalize_tool_call( alias_target = TOOL_NAME_ALIASES.get(tool_name) if alias_target and alias_target in available_tools: normalized_tool_name = alias_target + # For terminal alias with prefix, combine tool name with command + if ( + alias_target == "terminal" + and tool_name in _TERMINAL_COMMAND_PREFIX_ALIASES + ): + original_command = arguments.get("command") + normalized_arguments = { + key: value + for key, value in arguments.items() + if key in {"security_risk", "summary"} + } + if original_command: + normalized_arguments["command"] = f"{tool_name} {original_command}" + else: + normalized_arguments["command"] = tool_name elif "terminal" in available_tools: terminal_command = _maybe_rewrite_as_terminal_command( tool_name, diff --git a/tests/sdk/agent/test_tool_call_compatibility.py b/tests/sdk/agent/test_tool_call_compatibility.py index e687746f45..ed5e29bb45 100644 --- a/tests/sdk/agent/test_tool_call_compatibility.py +++ b/tests/sdk/agent/test_tool_call_compatibility.py @@ -470,3 +470,59 @@ def test_explicitly_registered_tool_not_hijacked_by_alias(): "str_replace", {"old_str": "x", "new_str": "y"}, available_tools ) assert tool_name == "file_editor", "str_replace alias should map to file_editor" + + +def test_git_alias_executes_terminal_tool(tmp_path): + """Test that 'git' tool name is aliased to 'terminal'.""" + events = _run_tool_call( + tmp_path, + tool_name="git", + arguments={"command": "status"}, + tool_names=(TERMINAL_TOOL_SPEC,), + ) + + action_event = next(e for e in events if isinstance(e, ActionEvent)) + errors = [e for e in events if isinstance(e, AgentErrorEvent)] + + assert not errors + assert action_event.tool_name == TERMINAL_TOOL_NAME + assert action_event.tool_call.name == TERMINAL_TOOL_NAME + assert action_event.action is not None + assert getattr(action_event.action, "command") == "git status" + + +def test_reset_alias_executes_terminal_tool(tmp_path): + """Test that 'reset' tool name is aliased to 'terminal'.""" + events = _run_tool_call( + tmp_path, + tool_name="reset", + arguments={"command": "clear"}, + tool_names=(TERMINAL_TOOL_SPEC,), + ) + + action_event = next(e for e in events if isinstance(e, ActionEvent)) + errors = [e for e in events if isinstance(e, AgentErrorEvent)] + + assert not errors + assert action_event.tool_name == TERMINAL_TOOL_NAME + assert action_event.tool_call.name == TERMINAL_TOOL_NAME + assert action_event.action is not None + assert getattr(action_event.action, "command") == "reset clear" + + +def test_shell_tool_name_git_falls_back_to_terminal(tmp_path): + """Test that 'git' without arguments falls back to terminal.""" + events = _run_tool_call( + tmp_path, + tool_name="git", + arguments={}, + tool_names=(TERMINAL_TOOL_SPEC,), + ) + + action_event = next(e for e in events if isinstance(e, ActionEvent)) + errors = [e for e in events if isinstance(e, AgentErrorEvent)] + + assert not errors + assert action_event.tool_name == TERMINAL_TOOL_NAME + assert action_event.action is not None + assert getattr(action_event.action, "command") == "git"