Repository: All-Hands-AI/OpenHands
Pull request #7951: Possibility to disable default tools
Author: login: MischaPanch
Created: 2025-04-19T16:25:34Z | Merged: 2025-04-20T00:14:47Z

- [x] This change is worth documenting at https://docs.all-hands.dev/
- [x] Include this change in the Release Notes. If checked, you **must** provide an **end-user friendly** description for your change below

**End-user friendly description of the problem this fixes or functionality that this introduces.**
It is now possible to disable default tools. On config level this is backwards compatible and just adds a few more options


---
**Give a summary of what the PR does, explaining any non-trivial design decisions.**

I did a few minor improvements to the code:

1. Docstrings of AgentConfig entries were moved to the actual entries so that an IDE can resolve them. Default mentions were removed (they are a duplication and one of them was already out of sync, there is really not much value to them).
2. get_tools was removed and turned into a private method. Tests of it were removed, they were not really testing anything. Instead a test was added that an agent with default config indeed has the default tools (also if it was up to me, I'd remove that test as well - configuration can't and shouldn't be tested)
---
**Link of any specific issues this addresses.**

Closes: #7936 


Diff:
diff --git a/config.template.toml b/config.template.toml
index 09d42b3cb5d3..ef7e4d973288 100644
--- a/config.template.toml
+++ b/config.template.toml
@@ -221,9 +221,22 @@ enable_browsing = true
 # Whether the LLM draft editor is enabled
 enable_llm_editor = false
 
+# Whether the standard editor tool (str_replace_editor) is enabled
+# Only has an effect if enable_llm_editor is False
+enable_editor = true
+
 # Whether the IPython tool is enabled
 enable_jupyter = true
 
+# Whether the command tool is enabled
+enable_cmd = true
+
+# Whether the think tool is enabled
+enable_think = true
+
+# Whether the finish tool is enabled
+enable_finish = true
+
 # LLM config group to use
 #llm_config = 'your-llm-config-group'
 
diff --git a/openhands/agenthub/codeact_agent/codeact_agent.py b/openhands/agenthub/codeact_agent/codeact_agent.py
index ab75be6e393d..7ab23e43160a 100644
--- a/openhands/agenthub/codeact_agent/codeact_agent.py
+++ b/openhands/agenthub/codeact_agent/codeact_agent.py
@@ -1,7 +1,19 @@
 import os
 from collections import deque
 
+from litellm import ChatCompletionToolParam
+
 import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
+from openhands.agenthub.codeact_agent.tools.bash import create_cmd_run_tool
+from openhands.agenthub.codeact_agent.tools.browser import BrowserTool
+from openhands.agenthub.codeact_agent.tools.finish import FinishTool
+from openhands.agenthub.codeact_agent.tools.ipython import IPythonTool
+from openhands.agenthub.codeact_agent.tools.llm_based_edit import LLMBasedFileEditTool
+from openhands.agenthub.codeact_agent.tools.str_replace_editor import (
+    create_str_replace_editor_tool,
+)
+from openhands.agenthub.codeact_agent.tools.think import ThinkTool
+from openhands.agenthub.codeact_agent.tools.web_read import WebReadTool
 from openhands.controller.agent import Agent
 from openhands.controller.state.state import State
 from openhands.core.config import AgentConfig
@@ -67,15 +79,7 @@ def __init__(
         super().__init__(llm, config)
         self.pending_actions: deque[Action] = deque()
         self.reset()
-
-        built_in_tools = codeact_function_calling.get_tools(
-            enable_browsing=self.config.enable_browsing,
-            enable_jupyter=self.config.enable_jupyter,
-            enable_llm_editor=self.config.enable_llm_editor,
-            llm=self.llm,
-        )
-
-        self.tools = built_in_tools
+        self.tools = self._get_tools()
 
         self.prompt_manager = PromptManager(
             prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
@@ -89,6 +93,40 @@ def __init__(
 
         self.response_to_actions_fn = codeact_function_calling.response_to_actions
 
+    def _get_tools(self) -> list[ChatCompletionToolParam]:
+        SIMPLIFIED_TOOL_DESCRIPTION_LLM_SUBSTRS = ['gpt-', 'o3', 'o1']
+
+        use_simplified_tool_desc = False
+        if self.llm is not None:
+            use_simplified_tool_desc = any(
+                model_substr in self.llm.config.model
+                for model_substr in SIMPLIFIED_TOOL_DESCRIPTION_LLM_SUBSTRS
+            )
+
+        tools = []
+        if self.config.enable_cmd:
+            tools.append(
+                create_cmd_run_tool(use_simplified_description=use_simplified_tool_desc)
+            )
+        if self.config.enable_think:
+            tools.append(ThinkTool)
+        if self.config.enable_finish:
+            tools.append(FinishTool)
+        if self.config.enable_browsing:
+            tools.append(WebReadTool)
+            tools.append(BrowserTool)
+        if self.config.enable_jupyter:
+            tools.append(IPythonTool)
+        if self.config.enable_llm_editor:
+            tools.append(LLMBasedFileEditTool)
+        elif self.config.enable_editor:
+            tools.append(
+                create_str_replace_editor_tool(
+                    use_simplified_description=use_simplified_tool_desc
+                )
+            )
+        return tools
+
     def reset(self) -> None:
         """Resets the CodeAct Agent."""
         super().reset()
diff --git a/openhands/agenthub/codeact_agent/function_calling.py b/openhands/agenthub/codeact_agent/function_calling.py
index 92e6ae5ecd0d..1b8a34fff257 100644
--- a/openhands/agenthub/codeact_agent/function_calling.py
+++ b/openhands/agenthub/codeact_agent/function_calling.py
@@ -6,7 +6,6 @@
 import json
 
 from litellm import (
-    ChatCompletionToolParam,
     ModelResponse,
 )
 
@@ -41,7 +40,6 @@
 from openhands.events.action.mcp import McpAction
 from openhands.events.event import FileEditSource, FileReadSource
 from openhands.events.tool import ToolCallMetadata
-from openhands.llm import LLM
 from openhands.mcp import MCPClientTool
 
 
@@ -237,39 +235,3 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
 
     assert len(actions) >= 1
     return actions
-
-
-def get_tools(
-    enable_browsing: bool = False,
-    enable_llm_editor: bool = False,
-    enable_jupyter: bool = False,
-    llm: LLM | None = None,
-) -> list[ChatCompletionToolParam]:
-    SIMPLIFIED_TOOL_DESCRIPTION_LLM_SUBSTRS = ['gpt-', 'o3', 'o1']
-
-    use_simplified_tool_desc = False
-    if llm is not None:
-        use_simplified_tool_desc = any(
-            model_substr in llm.config.model
-            for model_substr in SIMPLIFIED_TOOL_DESCRIPTION_LLM_SUBSTRS
-        )
-
-    tools = [
-        create_cmd_run_tool(use_simplified_description=use_simplified_tool_desc),
-        ThinkTool,
-        FinishTool,
-    ]
-    if enable_browsing:
-        tools.append(WebReadTool)
-        tools.append(BrowserTool)
-    if enable_jupyter:
-        tools.append(IPythonTool)
-    if enable_llm_editor:
-        tools.append(LLMBasedFileEditTool)
-    else:
-        tools.append(
-            create_str_replace_editor_tool(
-                use_simplified_description=use_simplified_tool_desc
-            )
-        )
-    return tools
diff --git a/openhands/core/config/agent_config.py b/openhands/core/config/agent_config.py
index 62a4910299d9..9de3d26277fd 100644
--- a/openhands/core/config/agent_config.py
+++ b/openhands/core/config/agent_config.py
@@ -7,28 +7,30 @@
 
 
 class AgentConfig(BaseModel):
-    """Configuration for the agent.
-
-    Attributes:
-        enable_browsing: Whether browsing delegate is enabled in the action space. Default is False. Only works with function calling.
-        enable_llm_editor: Whether LLM editor is enabled in the action space. Default is False. Only works with function calling.
-        enable_jupyter: Whether Jupyter is enabled in the action space. Default is False.
-        llm_config: The name of the llm config to use. If specified, this will override global llm config.
-        enable_prompt_extensions: Whether to use prompt extensions (e.g., microagents, inject runtime info). Default is True.
-        disabled_microagents: A list of microagents to disable (by name, without .py extension, e.g. ["github", "lint"]). Default is None.
-        condenser: Configuration for the memory condenser. Default is NoOpCondenserConfig.
-        enable_history_truncation: Whether history should be truncated to continue the session when hitting LLM context length limit.
-        enable_som_visual_browsing: Whether to enable SoM (Set of Marks) visual browsing. Default is False.
-    """
-
     llm_config: str | None = Field(default=None)
+    """The name of the llm config to use. If specified, this will override global llm config."""
     enable_browsing: bool = Field(default=True)
+    """Whether to enable browsing tool"""
     enable_llm_editor: bool = Field(default=False)
+    """Whether to enable LLM editor tool"""
+    enable_editor: bool = Field(default=True)
+    """Whether to enable the standard editor tool (str_replace_editor), only has an effect if enable_llm_editor is False."""
     enable_jupyter: bool = Field(default=True)
+    """Whether to enable Jupyter tool"""
+    enable_cmd: bool = Field(default=True)
+    """Whether to enable bash tool"""
+    enable_think: bool = Field(default=True)
+    """Whether to enable think tool"""
+    enable_finish: bool = Field(default=True)
+    """Whether to enable finish tool"""
     enable_prompt_extensions: bool = Field(default=True)
+    """Whether to enable prompt extensions"""
     disabled_microagents: list[str] = Field(default_factory=list)
+    """A list of microagents to disable (by name, without .py extension, e.g. ["github", "lint"]). Default is None."""
     enable_history_truncation: bool = Field(default=True)
+    """Whether history should be truncated to continue the session when hitting LLM context length limit."""
     enable_som_visual_browsing: bool = Field(default=True)
+    """Whether to enable SoM (Set of Marks) visual browsing."""
     condenser: CondenserConfig = Field(
         default_factory=lambda: NoOpCondenserConfig(type='noop')
     )
diff --git a/tests/unit/test_agents.py b/tests/unit/test_agents.py
index c31380529279..b72ef9d68832 100644
--- a/tests/unit/test_agents.py
+++ b/tests/unit/test_agents.py
@@ -5,9 +5,6 @@
 from litellm import ChatCompletionMessageToolCall
 
 from openhands.agenthub.codeact_agent.codeact_agent import CodeActAgent
-from openhands.agenthub.codeact_agent.function_calling import (
-    get_tools as codeact_get_tools,
-)
 from openhands.agenthub.codeact_agent.function_calling import (
     response_to_actions as codeact_response_to_actions,
 )
@@ -24,9 +21,6 @@
     _BROWSER_DESCRIPTION,
     _BROWSER_TOOL_DESCRIPTION,
 )
-from openhands.agenthub.readonly_agent.function_calling import (
-    get_tools as readonly_get_tools,
-)
 from openhands.agenthub.readonly_agent.function_calling import (
     response_to_actions as readonly_response_to_actions,
 )
@@ -72,6 +66,22 @@ def agent(agent_class) -> Union[CodeActAgent, ReadOnlyAgent]:
     return agent
 
 
+def test_agent_with_default_config_has_default_tools():
+    config = AgentConfig()
+    codeact_agent = CodeActAgent(llm=LLM(LLMConfig()), config=config)
+    assert len(codeact_agent.tools) > 0
+    default_tool_names = [tool['function']['name'] for tool in codeact_agent.tools]
+    assert {
+        'browser',
+        'execute_bash',
+        'execute_ipython_cell',
+        'finish',
+        'str_replace_editor',
+        'think',
+        'web_read',
+    }.issubset(default_tool_names)
+
+
 @pytest.fixture
 def mock_state() -> State:
     state = Mock(spec=State)
@@ -106,61 +116,6 @@ def test_step_with_pending_actions(agent):
     assert len(agent.pending_actions) == 0
 
 
-def test_codeact_get_tools_default():
-    tools = codeact_get_tools(
-        enable_jupyter=True,
-        enable_llm_editor=True,
-        enable_browsing=True,
-    )
-    assert len(tools) > 0
-
-    # Check required tools are present
-    tool_names = [tool['function']['name'] for tool in tools]
-    assert 'execute_bash' in tool_names
-    assert 'execute_ipython_cell' in tool_names
-    assert 'edit_file' in tool_names
-    assert 'web_read' in tool_names
-
-
-def test_readonly_get_tools_default():
-    tools = readonly_get_tools()
-    assert len(tools) > 0
-
-    # Check required tools are present
-    tool_names = [tool['function']['name'] for tool in tools]
-    assert 'execute_bash' not in tool_names
-    assert 'execute_ipython_cell' not in tool_names
-    assert 'edit_file' not in tool_names
-    assert 'web_read' in tool_names
-    assert 'grep' in tool_names
-    assert 'glob' in tool_names
-    assert 'think' in tool_names
-
-
-def test_codeact_get_tools_with_options():
-    # Test with all options enabled
-    tools = codeact_get_tools(
-        enable_browsing=True,
-        enable_jupyter=True,
-        enable_llm_editor=True,
-    )
-    tool_names = [tool['function']['name'] for tool in tools]
-    assert 'browser' in tool_names
-    assert 'execute_ipython_cell' in tool_names
-    assert 'edit_file' in tool_names
-
-    # Test with all options disabled
-    tools = codeact_get_tools(
-        enable_browsing=False,
-        enable_jupyter=False,
-        enable_llm_editor=False,
-    )
-    tool_names = [tool['function']['name'] for tool in tools]
-    assert 'browser' not in tool_names
-    assert 'execute_ipython_cell' not in tool_names
-    assert 'edit_file' not in tool_names
-
-
 def test_cmd_run_tool():
     CmdRunTool = create_cmd_run_tool()
     assert CmdRunTool['type'] == 'function'


Reviews:
user: login: enyst
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-19T16:59:40Z
user: login: MischaPanch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T17:12:34Z
user: login: MischaPanch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T17:13:54Z
user: login: enyst
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-19T17:34:33Z
user: login: enyst
body: Thank you!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-20T00:14:33Z

Review comments:
diff_hunk: @@ -221,9 +221,22 @@ enable_browsing = true
 # Whether the LLM draft editor is enabled
 enable_llm_editor = false
 
+# Whether the standard editor tool (str_replace_editor) is enabled
+# Only has an effect if enable_llm_editor is False
+enable_editor = true
+
 # Whether the IPython tool is enabled
 enable_jupyter = true
 
+# Whether the command tool is enabled
+enable_cmd = true
+
+# Whether the think tool is enabled
+enable_think = true
+
+# Whether the finish tool is enabled
+enable_finish = true
path: config.template.toml
user: login: enyst
body: I'm sorry, I think we somehow misunderstood each other. I prefer to introduce them like MCPs, in a list of enumerated tools, in agent config. That will work towards a similar interface to use, by the agent, for local tools and MCP tools alike. We can keep the _existing_ `enable_*`, but I would wary of introducing _more_ `enable_*` settings that we will deprecate anyway later.

I don't feel very strongly about it, but just to clarify, this is how I thought about this. Do you see downsides of that?
created_at: 2025-04-19T16:59:40Z
author_association: COLLABORATOR
diff_hunk: @@ -221,9 +221,22 @@ enable_browsing = true
 # Whether the LLM draft editor is enabled
 enable_llm_editor = false
 
+# Whether the standard editor tool (str_replace_editor) is enabled
+# Only has an effect if enable_llm_editor is False
+enable_editor = true
+
 # Whether the IPython tool is enabled
 enable_jupyter = true
 
+# Whether the command tool is enabled
+enable_cmd = true
+
+# Whether the think tool is enabled
+enable_think = true
+
+# Whether the finish tool is enabled
+enable_finish = true
path: config.template.toml
user: login: MischaPanch
body: Yes, I thought about it and I thought a clean implementation would need a few new concepts. I generally dislike string interfaces, so I would have introduced something like a tool registry as an enum. This should be used consistently in the code.

Then the user should have easy access to the registry to understand which tool names are available and can be configured. Otherwise this info would need to be copy pasted to the config template and it would run out of sync almost immediately.

One would need to deal with invalid names that were provided and so on.

This becomes even more convoluted when MCP tools are allowed to be configured. What to do if there are name collisions with default tools?

I think in the near future one should have a proper solution for configuration of default and MCP tools, then one can get rid of all enable_ flags. But I think this solution will not just be a flat list in the config, but at the very least also a registry. Maybe a separate nested config setting that allows selecting tools for everything, including MCP servers.

For now, I wanted to get the issue done without a larger overhaul than the one I already did here
created_at: 2025-04-19T17:12:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,9 +221,22 @@ enable_browsing = true
 # Whether the LLM draft editor is enabled
 enable_llm_editor = false
 
+# Whether the standard editor tool (str_replace_editor) is enabled
+# Only has an effect if enable_llm_editor is False
+enable_editor = true
+
 # Whether the IPython tool is enabled
 enable_jupyter = true
 
+# Whether the command tool is enabled
+enable_cmd = true
+
+# Whether the think tool is enabled
+enable_think = true
+
+# Whether the finish tool is enabled
+enable_finish = true
path: config.template.toml
user: login: MischaPanch
body: The current implementation has the benefit that the toml config exactly mirrors the agent config, so there are no string interfaces involving tool names
created_at: 2025-04-19T17:13:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,9 +221,22 @@ enable_browsing = true
 # Whether the LLM draft editor is enabled
 enable_llm_editor = false
 
+# Whether the standard editor tool (str_replace_editor) is enabled
+# Only has an effect if enable_llm_editor is False
+enable_editor = true
+
 # Whether the IPython tool is enabled
 enable_jupyter = true
 
+# Whether the command tool is enabled
+enable_cmd = true
+
+# Whether the think tool is enabled
+enable_think = true
+
+# Whether the finish tool is enabled
+enable_finish = true
path: config.template.toml
user: login: enyst
body: OK! I'm not sure we can fix human error 😅 - after all, `enable_toolname` needs toolname, but we can go with this IMO, and give it some thought later, as you said.
created_at: 2025-04-19T17:34:33Z
author_association: COLLABORATOR

PR comments:
user: login: MischaPanch
created_at: 2025-04-19T16:26:45Z
body: @enyst pls review, I closed he last PR
author_association: CONTRIBUTOR
user: login: MischaPanch
created_at: 2025-04-19T17:15:16Z
body: I have a minor error in the new unit test. Ran out of battery, will fix later today. But feel free to push to this branch if you want to fix it before that :)
author_association: CONTRIBUTOR