Skip to content

Conversation

@xingyaoww
Copy link
Collaborator

@xingyaoww xingyaoww commented Nov 11, 2025

Summary

Implements dynamic tool registration for RemoteConversation to eliminate hardcoded server-side tool registration. This allows clients to use any tool set without server modifications, improving flexibility and scalability.

Revised approach: Track module qualnames at registration time in the tool registry and dynamically import modules by qualname on server-side. This eliminates the need for manual mapping and provides a single source of truth.

Fixes #1128

Changes

Client-Side

  • registry.py: Enhanced tool registry to track module qualnames:
    • Added _MODULE_QUALNAMES dict to store tool name → module qualname mapping
    • Updated register_tool() to automatically capture and store module qualname from the factory callable/class/instance
    • Added get_tool_module_qualnames() function to retrieve the mapping
  • remote_conversation.py: Modified RemoteConversation.__init__() to send tool module qualnames (dict) instead of tool names (list) in the conversation creation payload

Server-Side

  • models.py: Updated StartConversationRequest to accept tool_module_qualnames: dict[str, str] field (defaults to empty dict)
  • conversation_service.py: Modified start_conversation() to dynamically import modules by qualname using importlib.import_module(), which triggers tool auto-registration
  • tool_router.py: Removed hardcoded register_planning_tools() call (was already removed in previous commit)
  • Removed tool_utils.py: No longer needed as module qualnames are tracked automatically by the registry

Tests

  • test_registry_qualnames.py (new): Tests for module qualname tracking in the registry
    • Tests tracking qualnames from classes, callables, and auto-imported modules
    • Verifies get_tool_module_qualnames() returns a copy
  • test_conversation_router.py: Updated tests to use tool_module_qualnames dict instead of registered_tools list

Benefits

Single Source of Truth: Registry automatically tracks module info when tools register
No Manual Mapping: Module qualnames are captured automatically from the factory
Flexibility: Clients can use any tool set without server changes
Scalability: Works with custom tools automatically
Loose Coupling: Server doesn't need to know about specific tools in advance
Backward Compatibility: Existing code continues to work (tool_module_qualnames defaults to empty dict)
Better DX: Tools "just work" when registered on the client

Architecture

How It Works

  1. Registration Time: When a tool is registered via register_tool(name, factory):

    • Registry extracts the module qualname from the factory (factory.__module__)
    • Stores it in _MODULE_QUALNAMES[name] = module_qualname
  2. Client Side: When creating a RemoteConversation:

    • Calls get_tool_module_qualnames() to get the {tool_name: module_qualname} mapping
    • Sends it in the conversation creation payload
  3. Server Side: When starting a conversation:

    • Receives the tool_module_qualnames dict
    • For each module qualname, calls importlib.import_module(qualname)
    • Module import triggers tool auto-registration via decorators/side effects

Key Insight

Most tools auto-register when their module is imported (via decorators or module-level code). By tracking module qualnames at registration time, we can:

  • Avoid manual mapping (TOOL_MODULE_MAP)
  • Support any tool without predefined configuration
  • Maintain a single source of truth in the registry

Testing

All tests pass, including:

  • 4 new tests for registry module qualname tracking
  • 2 updated tests for conversation router with tool_module_qualnames
  • All existing conversation router tests (39 total)
uv run pytest tests/sdk/test_registry_qualnames.py -v
uv run pytest tests/agent_server/test_conversation_router.py -v

Security Considerations

  • Tools are registered through standard Python module imports (no eval/exec)
  • Import failures are logged as warnings and gracefully handled (agent fails only if it tries to use unregistered tools)
  • Server only imports modules that were already registered on the client side

Backward Compatibility

  • tool_module_qualnames field in StartConversationRequest is optional and defaults to empty dict
  • Existing code that doesn't send tool module qualnames continues to work
  • Default tools remain registered via register_default_tools() in tool_router.py

Example Usage

from openhands.sdk import RemoteWorkspace, Agent, LLM
from openhands.sdk.conversation import RemoteConversation
from openhands.tools.preset.planning import register_planning_tools

# Register planning tools on client - registry automatically tracks module qualnames
register_planning_tools()

# Create RemoteConversation - planning tools are automatically registered on server
# by importing their modules
conversation = RemoteConversation(
    workspace=workspace,
    agent=agent
)

Related

- Add tool_utils.py with TOOL_MODULE_MAP and register_tools_by_name() for dynamic tool import
- Update StartConversationRequest model to accept registered_tools field
- Update RemoteConversation to send list of registered tools in payload
- Update conversation_service to dynamically register tools from client
- Remove hardcoded register_planning_tools() call from tool_router.py
- Add comprehensive tests for dynamic tool registration
- Maintain backward compatibility for existing code

Fixes #1128

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

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   conversation_service.py29317540%62, 65, 76–77, 80–83, 85, 89, 91, 94–101, 104–105, 108–112, 115–117, 119–122, 124, 131–132, 134–136, 139, 143, 145, 147, 154–158, 166, 181, 185–187, 190, 202–203, 211, 214, 225–229, 231–234, 237–242, 245–248, 250–252, 254–258, 272–276, 279, 281, 284–286, 288, 292, 296, 303–307, 310–311, 317–325, 343, 366, 413, 415, 417–418, 421, 426, 428–429, 433–434, 436–437, 440–442, 445, 451, 456–459, 466–467, 471–475, 477, 482, 486–488, 493, 496, 500–501, 503–505, 507, 509, 522–524, 527, 530, 533–536, 543–544, 548–550, 553–554, 556
   models.py96297%55–56
   tool_router.py9277%20–21
   tool_utils.py16381%44–45, 48
openhands-sdk/openhands/sdk/conversation/impl
   remote_conversation.py3629773%54–60, 67–70, 99, 106, 114, 116–119, 129, 138, 142–143, 148–151, 186, 200, 217, 228, 237–238, 290, 310, 318, 330, 338–341, 344, 349–350, 355–359, 364–368, 373–376, 379, 390–391, 395, 399, 402, 478, 484, 486, 502, 504–505, 516, 533–534, 540, 555, 588, 590, 592–593, 597–598, 607, 615, 620–622, 624, 627, 629–630, 647, 654, 660–661, 675–676, 683–684
TOTAL11965553253% 

Revised approach: Track module qualnames at registration time and dynamically
import modules by qualname on server-side.

Changes:
- Modified tool registry to track module qualnames when tools are registered
  - Added _MODULE_QUALNAMES dict to store tool name -> module qualname mapping
  - Updated register_tool() to capture and store module qualname from factory
  - Added get_tool_module_qualnames() function to retrieve the mapping
- Updated StartConversationRequest model to accept tool_module_qualnames dict
- Updated RemoteConversation to send tool module qualnames in conversation payload
- Updated conversation_service to dynamically import modules by qualname
- Removed hardcoded planning tools registration from tool_router.py
- Removed tool_utils.py and manual TOOL_MODULE_MAP (superseded by registry tracking)
- Added comprehensive tests for dynamic tool registration

This approach provides a single source of truth in the registry and works with
any tool without predefined mappings.

Fixes #1128

Co-authored-by: openhands <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement dynamic tool registration for RemoteConversation

3 participants