Skip to content

Conversation

@malhotra5
Copy link
Collaborator

@malhotra5 malhotra5 commented Oct 22, 2025

Summary

This PR implements automatic tool registration as requested in issue #861. Tools now automatically register themselves when their definition modules are imported, eliminating the need for manual registration calls.

Changes Made

Tool Definition Files

  • Modified all tool definition files to include automatic registration:
    • openhands/tools/execute_bash/definition.py - BashTool
    • openhands/tools/file_editor/definition.py - FileEditorTool
    • openhands/tools/task_tracker/definition.py - TaskTrackerTool
    • openhands/tools/browser_use/definition.py - BrowserToolSet
    • openhands/tools/grep/definition.py - GrepTool
    • openhands/tools/glob/definition.py - GlobTool
    • openhands/tools/planning_file_editor/definition.py - PlanningFileEditorTool

Preset Configurations

  • Updated preset files to remove manual registration calls:
    • openhands/tools/preset/default.py - Updated to rely on automatic registration
    • openhands/tools/preset/planning.py - Updated to rely on automatic registration

Testing

  • Added comprehensive unit tests in tests/sdk/tool/test_automatic_registration.py:
    • Tests for each tool's automatic registration
    • Tests for import from __init__.py files
    • Tests for tool resolution after automatic registration
    • 9 test cases covering all scenarios

Before and After

Before (Manual Registration Required)

from openhands.tools.execute_bash import BashTool
from openhands.sdk import register_tool

register_tool('BashTool', BashTool)  # Manual registration required

agent = Agent(tools=[BashTool])

After (Automatic Registration)

from openhands.tools.execute_bash import BashTool  # Both imports and registers tool

agent = Agent(tools=[BashTool])

Implementation Details

Each tool definition file now includes:

  1. Import of register_tool function
  2. Automatic registration call at module level: register_tool("ToolName", ToolClass)

The registration happens when the module is imported, making tool usage much simpler and more intuitive.

Testing

  • ✅ All existing tests pass
  • ✅ New unit tests verify automatic registration works correctly
  • ✅ Integration tests confirm tools work as expected
  • ✅ Pre-commit hooks pass (formatting, linting, type checking)

Fixes

Closes #861

Breaking Changes

None. This change is backward compatible - existing code that manually registers tools will continue to work, but the manual registration is now redundant.

@malhotra5 can click here to continue refining the PR


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.12-nodejs22 Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:0292c6e-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-0292c6e-python \
  ghcr.io/openhands/agent-server:0292c6e-python

All tags pushed for this build

ghcr.io/openhands/agent-server:0292c6e-golang-amd64
ghcr.io/openhands/agent-server:v1.0.0a5_golang_tag_1.21-bookworm_binary-amd64
ghcr.io/openhands/agent-server:0292c6e-golang-arm64
ghcr.io/openhands/agent-server:v1.0.0a5_golang_tag_1.21-bookworm_binary-arm64
ghcr.io/openhands/agent-server:0292c6e-java-amd64
ghcr.io/openhands/agent-server:v1.0.0a5_eclipse-temurin_tag_17-jdk_binary-amd64
ghcr.io/openhands/agent-server:0292c6e-java-arm64
ghcr.io/openhands/agent-server:v1.0.0a5_eclipse-temurin_tag_17-jdk_binary-arm64
ghcr.io/openhands/agent-server:0292c6e-python-amd64
ghcr.io/openhands/agent-server:v1.0.0a5_nikolaik_s_python-nodejs_tag_python3.12-nodejs22_binary-amd64
ghcr.io/openhands/agent-server:0292c6e-python-arm64
ghcr.io/openhands/agent-server:v1.0.0a5_nikolaik_s_python-nodejs_tag_python3.12-nodejs22_binary-arm64
ghcr.io/openhands/agent-server:0292c6e-golang
ghcr.io/openhands/agent-server:0292c6e-java
ghcr.io/openhands/agent-server:0292c6e-python

About Multi-Architecture Support

  • Each variant tag (e.g., 0292c6e-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 0292c6e-python-amd64) are also available if needed

- Add register_tool imports and automatic registration calls to all tool definition files
- Tools now automatically register themselves when their definition modules are imported
- Update preset configurations to remove manual registration calls
- Add comprehensive unit tests to verify automatic registration works correctly
- Simplifies tool usage from manual registration to just importing the tool

Fixes #861

Co-authored-by: openhands <[email protected]>
- Move test_automatic_registration.py from tests/sdk/tool/ to tests/cross/
- SDK tests should only import from openhands.sdk, cross tests can import from openhands.tools
- All tests still pass in their new location

Co-authored-by: openhands <[email protected]>
@github-actions
Copy link
Contributor

github-actions bot commented Oct 22, 2025

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/agent
   agent.py1495463%135, 139–140, 147–148, 150–152, 154–156, 172, 187–189, 196–198, 200, 204, 207–208, 210, 217, 243, 248, 279, 283, 288, 299, 302, 324–326, 328, 340–341, 346–347, 359–362, 371–372, 377, 389–390, 395–396, 428, 435–436, 455
openhands-sdk/openhands/sdk/conversation
   response_utils.py15940%25, 27, 33–34, 36, 38–41
openhands-sdk/openhands/sdk/llm/mixins
   fn_call_converter.py34430511%74–76, 337–350, 352–353, 355, 364–365, 367–370, 372–373, 375–376, 378–379, 381–384, 386–387, 389–390, 392, 399, 401, 422–429, 431–435, 439–445, 447–452, 456–464, 466–469, 471, 473–475, 478, 481–483, 485, 489, 491–492, 501, 503–504, 508–512, 516–521, 523, 525, 530, 533, 535–536, 539, 542, 544–548, 554, 576, 581, 591–594, 600–603, 608–614, 616, 618, 622, 625, 627–628, 630–633, 638, 640, 644–645, 649, 655, 657, 660, 668, 670–672, 674–676, 678–680, 686–689, 692–693, 700–705, 708–712, 717, 720, 724, 728, 733–734, 737–739, 742, 747–749, 751–752, 768, 780–782, 786–787, 789–792, 794–795, 797–799, 801, 804, 806, 808–810, 812–813, 816–817, 820–822, 824–827, 830, 834, 841–842, 845–846, 860, 866–868, 871–872, 876–877, 883–884, 887, 899, 902–909, 913–914, 919–920, 925, 931–934, 944–945, 950, 956–957, 962–963, 970, 973–975, 978–979, 981, 987, 992, 995, 999, 1007, 1009–1010, 1013–1015, 1017–1018, 1024–1026, 1028–1029, 1031, 1033, 1037, 1039, 1044, 1046–1047, 1050
openhands-sdk/openhands/sdk/mcp
   tool.py895439%40–41, 51–52, 56–59, 63, 66, 69–72, 84, 108–110, 112, 115–116, 127, 145–146, 149–153, 155–157, 163, 184, 186–187, 191–193, 201–202, 210, 220–222, 227, 234–235, 237, 262–263, 267–269
openhands-sdk/openhands/sdk/tool
   registry.py712269%35–36, 41, 44–45, 48, 50, 59, 61–63, 67, 70, 74, 82–83, 94, 107, 125, 128, 134, 154
   tool.py1343474%221, 226–228, 233, 237, 243, 253, 255, 263, 278, 304, 313, 316–322, 339, 344–347, 349, 356–358, 416, 439–442
openhands-sdk/openhands/sdk/tool/builtins
   finish.py32875%28–31, 39, 45, 68, 93
   think.py381463%30, 33–34, 37, 39–43, 45, 57, 63, 84, 109
openhands-tools/openhands/tools/browser_use
   definition.py1151686%39–40, 42, 46–55, 57–58, 60
openhands-tools/openhands/tools/delegate
   definition.py24579%55, 99, 102, 108, 111
openhands-tools/openhands/tools/execute_bash
   definition.py1016040%54, 57, 60–61, 63, 66–68, 70–72, 74–76, 78, 108, 112–121, 126, 129–131, 134, 136–138, 140, 144–145, 148–150, 152–153, 156–159, 163–165, 170, 174–176, 179–181, 185–186, 188, 253
openhands-tools/openhands/tools/file_editor
   definition.py64985%96, 108, 128, 131, 134, 141, 143, 145, 147
openhands-tools/openhands/tools/glob
   definition.py381657%54–55, 57–58, 63–64, 68–69, 74, 107, 109–111, 114, 117, 124
openhands-tools/openhands/tools/grep
   definition.py421857%60–61, 63–64, 69, 74, 79–80, 85–86, 91, 120, 122–124, 127, 130, 137
openhands-tools/openhands/tools/planning_file_editor
   definition.py241154%72, 76–78, 81–82, 84, 86, 89, 95, 102
openhands-tools/openhands/tools/preset
   planning.py412831%63–64, 66–67, 69–71, 73, 84–86, 88, 94–96, 100–102, 113, 116–118, 120, 137, 142, 158, 160, 171
openhands-tools/openhands/tools/task_tracker
   definition.py1348437%55, 58–60, 62–63, 66–67, 69, 85, 90, 92, 94–95, 98, 101–103, 105–106, 109–115, 117–119, 122, 124–127, 129, 132, 135–136, 138–139, 141–142, 144, 173, 175, 177–179, 185, 187–188, 193–194, 198, 207–208, 210–212, 216–217, 219–222, 224, 229, 235–239, 243, 247–248, 250–251, 253, 255–259
TOTAL11709540653% 

- Updated register_default_tools() to use ToolClass.tool_name instead of ToolClass.name
- This fixes AttributeError when importing tools that use ClassVar[str] for tool_name
- All automatic registration tests now pass

Co-authored-by: openhands <[email protected]>
- Add _camel_to_snake utility function to convert CamelCase to snake_case
- Add __init_subclass__ method to ToolBase for automatic tool naming
- Remove hardcoded tool_name attributes from all 7 tool classes
- Update preset files to use .tool_name attributes instead of hardcoded strings
- Add comprehensive test suite for automatic naming functionality
- Update existing tests to expect snake_case tool names
- All tools now automatically get snake_case names (e.g., BashTool -> bash_tool)

This eliminates the need for manual tool_name declarations while maintaining
backward compatibility through the .tool_name class attribute.

Co-authored-by: openhands <[email protected]>
@malhotra5 malhotra5 marked this pull request as draft October 22, 2025 18:20


# Automatically register the tool when this module is imported
register_tool(BrowserToolSet.tool_name, BrowserToolSet)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a check to make sure there's no tool name conflicts in the tool registry

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking of doing this in a separate PR so we can better track the changes

I'll need to look into reseting the tool registry so that unit tests aren't complaining

@blacksmith-sh
Copy link
Contributor

blacksmith-sh bot commented Oct 30, 2025

[Automatic Post]: It has been a while since there was any activity on this PR. @malhotra5, are you still working on it? If so, please go ahead, if not then please request review, close it, or request that someone else follow up.

@blacksmith-sh
Copy link
Contributor

blacksmith-sh bot commented Nov 3, 2025

Found 9 test failures on Blacksmith runners:

Test View Logs
test_automatic_registration/test_tool_can_be_resolved_after_automatic_registration View Logs
test_bash_tool/test_bash_tool_initialization View Logs
test_bash_tool/test_bash_tool_to_openai_tool View Logs
test_bash_tool/test_bash_tool_with_username View Logs
test_bash_tool_auto_detection/test_tool_metadata View Logs
test_file_editor_tool/test_file_editor_tool_initialization View Logs
test_file_editor_tool/test_file_editor_tool_to_openai_tool View Logs
test_glob_tool/test_glob_tool_initialization View Logs
test_grep_tool/test_grep_tool_initialization View Logs


Fix in Cursor

- Changed .tool_name to .name in examples, tests, and tool registration
- Removed invalid name= parameters from tool constructors since name is a ClassVar
- Updated browser tool tests to expect correct auto-generated names
- All ToolDefinition subclasses now consistently use .name attribute

Co-authored-by: openhands <[email protected]>
- Remove name= parameters from mock tool constructors since name is now a ClassVar
- Update test expectations to use auto-generated tool names (e.g., mock_test_tool)
- Fix regex patterns in error message tests to match new tool names
- Remove test for assigning to name attribute since it's now a ClassVar

Co-authored-by: openhands <[email protected]>
- Set name as ClassVar[str] instead of instance variable to properly override automatic naming
- Update test expectations to use correct tool names (finish_tool, think_tool)
- Add proper ClassVar import for type checking

Co-authored-by: openhands <[email protected]>
- Update mock tool call from 'think' to 'think_tool' to match automatic naming
- Ensures test passes with new ClassVar name attribute system

Co-authored-by: openhands <[email protected]>
…override

- Fixed test_llm_completion.py: Added ClassVar name and removed name= parameter
- Fixed test_to_responses_tool.py: Added ClassVar name and removed name= parameter
- Fixed test_to_responses_tool_security.py: Created separate tool classes with ClassVar names
- Fixed MCPToolDefinition: Used property with type: ignore[override] to override ClassVar name
- All 171 tests passing, all type checker issues resolved

Co-authored-by: openhands <[email protected]>
- Updated BashTool tests to use 'bash_tool' instead of 'execute_bash'
- Fixed FileEditorTool tests to expect 'file_editor_tool' instead of 'str_replace_editor'
- Corrected hello world test tool registration and mock LLM responses
- All core test suites now passing: bash tool (5/5), file editor (8/8),
  hello world (3/3), automatic registration (9/9), bash auto detection (7/7)

Co-authored-by: openhands <[email protected]>
The test imports from openhands.tools which violates the SDK test isolation rule.
SDK tests should only import from openhands.sdk, while cross tests can import
from both openhands.sdk and openhands.tools.

This fixes the CI failure about openhands.tools imports in tests/sdk/.

Co-authored-by: openhands <[email protected]>
- Fixed agent.py to check for 'finish_tool' instead of 'finish' when setting agent status to FINISHED
- Fixed response_utils.py to check for 'finish_tool' instead of 'finish' when extracting final response
- Updated test_agent_final_response.py to use correct tool name 'finish_tool' in test cases
- Updated test_confirmation_mode.py to use correct tool names 'finish_tool' and 'think_tool'

This resolves the issue where agents would get stuck in loops because they couldn't recognize finish tool calls due to name mismatch. The FinishTool class name gets converted to 'finish_tool' via _camel_to_snake() function, so all references should use this consistent naming.

Fixes failing tests:
- test_single_finish_action_skips_confirmation_entirely
- test_think_and_finish_action_skips_confirmation_entirely

Co-authored-by: openhands <[email protected]>
Update test expectations to match actual tool names:
- GlobTool.name: 'glob' -> 'glob_tool'
- GrepTool.name: 'grep' -> 'grep_tool'

This aligns with the _camel_to_snake() naming convention used
in tool registration system.

Co-authored-by: openhands <[email protected]>
@malhotra5 malhotra5 marked this pull request as ready for review November 4, 2025 17:08
@malhotra5
Copy link
Collaborator Author

Verified that this works in the CLI

@malhotra5 malhotra5 requested review from enyst and xingyaoww November 4, 2025 17:30
mcp_tool: mcp.types.Tool = Field(description="The MCP tool definition.")

@property
def name(self) -> str: # type: ignore[override]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we don't do "# type: ignore[override]"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this one is a bit hacky. Essentially every tool name is on a class level. However, for MCPTool the name of the tool is instantiated later based on the instance level details. Without the type ignore we get type error

Instance variable "name" overrides class variable of same name in class "ToolDefinition"PylancereportIncompatibleVariableOverride

Copy link
Collaborator Author

@malhotra5 malhotra5 Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we introduce a separate field for mcp tool name and get rid of this hacky override?

@property
def mcp_tool_name(self):
    return self.mcp_tool.name

Copy link
Collaborator

@xingyaoww xingyaoww left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final few things left:

  1. we should add a check at tool registry when we try to register the same tool name multiple time (either throw an exception, or log a warning message)
  2. do string search for explicit "execute_bash" mention in prompts and update it with the updated tool name
  3. (optional), remove "tool" suffix from automatic tool names

@malhotra5
Copy link
Collaborator Author

we should add a check at tool registry when we try to register the same tool name multiple time (either throw an exception, or log a warning message)

I'll throw a warning log for now, but wanted to do this in a follow up PR to isolate the changes, particularly with the test fixtures etc (because running all the unit tests will be registering the same tools)

malhotra5 and others added 10 commits November 4, 2025 13:07
- Changed agent_status to execution_status to match ConversationState field
- Fixed import issue with AgentExecutionStatus

Co-authored-by: openhands <[email protected]>
Updated all test files to use the new tool naming convention:
- think_tool → think
- execute_bash → bash
- str_replace_editor → file_editor
- mock_test_tool → mock_test
- mock_immutable_tool → mock_immutable
- __simple_hello_tool → __simple_hello

All SDK tests now pass with the new automatic tool naming logic.

Co-authored-by: openhands <[email protected]>
Updated all tool tests to expect new naming convention where '_tool' suffix is removed:
- BashTool: 'bash_tool' -> 'bash'
- FileEditorTool: 'file_editor_tool' -> 'file_editor'
- GlobTool: 'glob_tool' -> 'glob'
- GrepTool: 'grep_tool' -> 'grep'
- PlanningFileEditorTool: 'planning_file_editor_tool' -> 'planning_file_editor'

Updated test assertions and expected values to match the new automatic naming logic.

Co-authored-by: openhands <[email protected]>
Copy link
Collaborator

@xingyaoww xingyaoww left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

@xingyaoww xingyaoww added integration-test Runs the integration tests and comments the results test-examples Run all applicable "examples/" files. Expensive operation. labels Nov 4, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Nov 4, 2025

Hi! I started running the integration tests on your PR. You will receive a comment with the results shortly.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 4, 2025

🔄 Running Examples with openhands/claude-haiku-4-5-20251001

Last updated: 2025-11-04 19:09:20 UTC

Example Status Duration Cost
01_standalone_sdk/01_hello_world.py ✅ PASS 22s $0.02
01_standalone_sdk/02_custom_tools.py ✅ PASS 27s $0.03
01_standalone_sdk/03_activate_skill.py ✅ PASS 12s $0.01
01_standalone_sdk/05_use_llm_registry.py ✅ PASS 10s $0.01
01_standalone_sdk/07_mcp_integration.py ✅ PASS 57s $0.04
01_standalone_sdk/09_pause_example.py ✅ PASS 13s $0.01
01_standalone_sdk/10_persistence.py ✅ PASS 39s $0.03
01_standalone_sdk/11_async.py ✅ PASS 35s $0.03
01_standalone_sdk/12_custom_secrets.py ✅ PASS 14s $0.01
01_standalone_sdk/13_get_llm_metrics.py ✅ PASS 33s $0.01
01_standalone_sdk/14_context_condenser.py ✅ PASS 162s $0.30
01_standalone_sdk/17_image_input.py ✅ PASS 19s $0.02
01_standalone_sdk/18_send_message_while_processing.py ✅ PASS 22s $0.01
01_standalone_sdk/19_llm_routing.py ✅ PASS 17s $0.01
01_standalone_sdk/20_stuck_detector.py ✅ PASS 19s $0.01
01_standalone_sdk/21_generate_extraneous_conversation_costs.py ✅ PASS 14s $0.01
01_standalone_sdk/22_anthropic_thinking.py ✅ PASS 20s $0.02
01_standalone_sdk/23_responses_reasoning.py ✅ PASS 73s $0.01
01_standalone_sdk/24_planning_agent_workflow.py ✅ PASS 225s $0.25
02_remote_agent_server/01_convo_with_local_agent_server.py ✅ PASS 71s $0.00
02_remote_agent_server/02_convo_with_docker_sandboxed_server.py ✅ PASS 51s $0.00

✅ All tests passed!

Total: 21 | Passed: 21 | Failed: 0 | Total Cost: $0.85

View full workflow run

@github-actions
Copy link
Contributor

github-actions bot commented Nov 4, 2025

🧪 Integration Tests Results

Overall Success Rate: 100.0%
Total Cost: $0.40
Models Tested: 3
Timestamp: 2025-11-04 18:55:48 UTC

📁 Detailed Logs & Artifacts

Click the links below to access detailed agent/LLM logs showing the complete reasoning process for each model. On the GitHub Actions page, scroll down to the 'Artifacts' section to download the logs.

📊 Summary

Model Success Rate Tests Passed Total Tests Cost
litellm_proxy_claude_sonnet_4_5_20250929 100.0% 7/7 7 $0.33
litellm_proxy_deepseek_deepseek_chat 100.0% 7/7 7 $0.02
litellm_proxy_gpt_5_mini_2025_08_07 100.0% 7/7 7 $0.05

📋 Detailed Results

litellm_proxy_claude_sonnet_4_5_20250929

  • Success Rate: 100.0% (7/7)
  • Total Cost: $0.33
  • Run Suffix: litellm_proxy_claude_sonnet_4_5_20250929_adeae66_sonnet_run_N7_20251104_185310

litellm_proxy_deepseek_deepseek_chat

  • Success Rate: 100.0% (7/7)
  • Total Cost: $0.02
  • Run Suffix: litellm_proxy_deepseek_deepseek_chat_adeae66_deepseek_run_N7_20251104_185309

litellm_proxy_gpt_5_mini_2025_08_07

  • Success Rate: 100.0% (7/7)
  • Total Cost: $0.05
  • Run Suffix: litellm_proxy_gpt_5_mini_2025_08_07_adeae66_gpt5_mini_run_N7_20251104_185315

@xingyaoww xingyaoww merged commit d5995c3 into main Nov 4, 2025
21 checks passed
@xingyaoww xingyaoww deleted the openhands/simplify-tool-registration branch November 4, 2025 19:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-test Runs the integration tests and comments the results test-examples Run all applicable "examples/" files. Expensive operation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Simplify tool registration

5 participants