Skip to content

Conversation

@xingyaoww
Copy link
Collaborator

@xingyaoww xingyaoww commented Dec 11, 2025

Summary

This PR implements support for custom tools when using a remote agent server, addressing issue #1381.

Changes

Core Implementation

  • Dynamic Tool Registration: Modified conversation_service.py to dynamically import and register custom tools from client's tool_module_qualnames
  • Tool Qualname Transmission: Updated RemoteConversation.create() to send tool module qualnames to the server
  • API Schema Update: Extended StartConversationRequest to include optional tool_module_qualnames field

Example Implementation

Added comprehensive example under examples/02_remote_agent_server/05_custom_tool/:

  • Custom Tool: ReportBugTool - demonstrates structured data collection pattern (common use case for QA workflows)
  • Dockerfile: Multi-stage build that:
    1. Builds agent-server from source (includes dynamic tool registration code)
    2. Copies custom tool modules into Docker image
    3. Sets PYTHONPATH to make custom tools importable
  • Example Script: custom_tool_example.py showing end-to-end usage
  • Build Script: build_custom_image.sh for easy Docker image creation
  • README: Comprehensive documentation with usage examples

Testing

Successfully tested that:

  1. ✅ Custom tool modules are imported server-side
  2. ✅ Tools register correctly via @register_tool decorator
  3. ✅ Agent can resolve and load custom tools
  4. ✅ Conversation creation succeeds with custom tools

Related PRs

Closes

Closes #1381

Co-authored-by: openhands openhands@all-hands.dev


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:a5f5726-python

Run

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

All tags pushed for this build

ghcr.io/openhands/agent-server:a5f5726-golang-amd64
ghcr.io/openhands/agent-server:a5f5726-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:a5f5726-golang-arm64
ghcr.io/openhands/agent-server:a5f5726-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:a5f5726-java-amd64
ghcr.io/openhands/agent-server:a5f5726-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:a5f5726-java-arm64
ghcr.io/openhands/agent-server:a5f5726-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:a5f5726-python-amd64
ghcr.io/openhands/agent-server:a5f5726-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-amd64
ghcr.io/openhands/agent-server:a5f5726-python-arm64
ghcr.io/openhands/agent-server:a5f5726-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-arm64
ghcr.io/openhands/agent-server:a5f5726-golang
ghcr.io/openhands/agent-server:a5f5726-java
ghcr.io/openhands/agent-server:a5f5726-python

About Multi-Architecture Support

  • Each variant tag (e.g., a5f5726-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., a5f5726-python-amd64) are also available if needed

openhands-agent and others added 12 commits November 11, 2025 14:51
- 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 <openhands@all-hands.dev>
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 <openhands@all-hands.dev>
Resolved conflicts in tests/agent_server/test_conversation_router.py by keeping
both sets of tests:
- test_start_conversation_with_tool_module_qualnames
- test_start_conversation_without_tool_module_qualnames
- test_set_conversation_security_analyzer_success
- test_set_conversation_security_analyzer_with_none
- test_security_analyzer_endpoint_with_malformed_analyzer_data

All tests pass successfully.

Co-authored-by: openhands <openhands@all-hands.dev>
SDK tests should not import from openhands.tools. Since this test
uses GlobTool from openhands.tools, it should be in tests/cross
which is for tests that use both SDK and tools packages.

This fixes the CI failure where the sdk-tests job checks for and
rejects any openhands.tools imports in tests/sdk/.

Co-authored-by: openhands <openhands@all-hands.dev>
All tools are now dynamically registered via tool_module_qualnames sent by
the client. The server imports tool modules based on the client's registry,
eliminating the need for pre-registering default tools at server startup.

This change completes the dynamic tool registration implementation by:
- Removing the last hardcoded server-side tool registration
- Ensuring tools are only registered when a client needs them
- Supporting custom and default tools through the same mechanism

Co-authored-by: openhands <openhands@all-hands.dev>
This commit fixes multiple issues that prevented the example from running:

1. VSCode Port Conflict:
   - Added OH_ENABLE_VSCODE=false to server environment to avoid port 8001 conflict
   - The VSCode service was trying to bind to the same port as the API server

2. Missing API Key Authentication:
   - Added api_key parameter to ManagedAPIServer class
   - Set SESSION_API_KEY environment variable when starting the server
   - Pass api_key to Workspace initialization
   - Use 'local-test-key' for local testing

3. LLM Configuration:
   - Changed default model to use litellm_proxy/ prefix
   - Set default base_url to https://llm-proxy.app.all-hands.dev/
   - This allows the example to work with the LiteLLM proxy

4. Simplified Example Flow:
   - Modified example to demonstrate server connectivity without requiring LLM API calls
   - Skip agent.run() to avoid needing valid LLM API key
   - Add try/except for title generation and cost calculation
   - Example now focuses on showing RemoteConversation setup and basic operations

The example now successfully demonstrates:
- Starting a managed API server
- Creating a RemoteConversation
- Sending messages
- Retrieving conversation state and events
- Proper cleanup

Co-authored-by: openhands <openhands@all-hands.dev>
When the agent server restarts and loads persisted conversations from
disk, it now dynamically registers the tools by importing their modules
based on the stored tool_module_qualnames. This ensures tools are
available when resuming conversations, preventing KeyError when the
agent tries to use them.

The fix adds tool registration logic in the __aenter__() method of
ConversationService, which is called during server startup. For each
persisted conversation, it:
1. Reads the tool_module_qualnames from stored metadata
2. Imports each module using importlib.import_module()
3. The import triggers tool auto-registration via decorators
4. Logs successful registrations and warnings for failures

This complements the existing tool registration for new conversations
and ensures tools work correctly in both scenarios.

Co-authored-by: openhands <openhands@all-hands.dev>
…ion' into implement-custom-tools-remote-agent
This example demonstrates how to use custom tools with a remote agent server
by building a custom Docker image that includes the tool implementation.

Features:
- Custom ReportBugTool for structured bug data collection
- Dockerfile that extends base agent server image with custom tools
- Build script for creating custom images
- Complete SDK example showing the full workflow
- Comprehensive README with usage instructions

Implements: #1381

Co-authored-by: openhands <openhands@all-hands.dev>
- Modified Dockerfile to build agent-server from source instead of using pre-built image
- This ensures conversation_service.py includes the dynamic tool registration code
- Fixed Docker build path for custom_tools directory in .dockerignore
- Updated logging to use DEBUG level instead of INFO for tool registration
- Successfully tested: custom tools now register correctly on remote agent server

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions
Copy link
Contributor

github-actions bot commented Dec 11, 2025

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   conversation_service.py33521037%63, 66, 77–78, 81–84, 86, 90, 92, 95–102, 105–106, 109–113, 116–118, 120–123, 125, 132–133, 135–137, 140, 144, 146, 148, 155, 161, 169–170, 179–182, 191, 200, 205–206, 209, 222–223, 241, 244, 255–259, 261–264, 267–272, 275–278, 280–282, 285, 288–290, 295–298, 306, 311–313, 327–331, 334, 336, 339–341, 343, 347, 351, 358–362, 365–366, 370–374, 377–378, 382–386, 389–390, 396–401, 408–409, 411, 415, 417–418, 423–424, 430–431, 437–439, 457, 481, 506, 508–509, 535, 537, 539–542, 547, 549–550, 554–555, 557–558, 561–563, 566, 572, 577–580, 587–588, 592–596, 598, 603, 607–609, 613–614, 616–618, 620, 622, 635–637, 640, 643, 646–649, 656–657, 661–663, 666–667, 669
   models.py1181488%56–57, 180–181, 183–185, 187, 191–192, 194, 200, 203, 205
   tool_router.py7271%18–19
openhands-sdk/openhands/sdk/conversation/impl
   remote_conversation.py41113267%57–63, 70–73, 102, 109, 117, 119–122, 132, 145–146, 151–154, 189, 203, 220, 231, 240–241, 293, 313, 321, 333, 341–344, 347, 352–355, 357, 362–363, 368–372, 377–381, 386–389, 392, 403–404, 408, 412, 415, 493, 499, 501, 517–518, 523, 525–526, 537, 554–555, 562–564, 567–571, 573–574, 578, 580–588, 590, 594, 609, 627, 644, 646, 648–649, 653–654, 663–664, 673, 681, 686–688, 690, 693, 695–696, 716, 718, 724–725, 740, 747, 753–754, 769, 783–784, 791–792
openhands-sdk/openhands/sdk/tool
   registry.py842471%36–37, 42, 45–46, 49, 51, 60, 62–64, 68, 71, 75, 83–84, 95, 108, 126, 129, 135, 147–148, 166
TOTAL12631564155% 

@xingyaoww
Copy link
Collaborator Author

@OpenHands please review diff from main and try to run the example introduced in this PR and tell me if it is runnable. You may need to install docker and build docker before you start to run it

@openhands-ai
Copy link

openhands-ai bot commented Dec 16, 2025

I'm on it! xingyaoww can track my progress at all-hands.dev

The Dockerfile copies SDK packages from the repository root (openhands-sdk,
openhands-tools, etc.), so the build context must be the repository root,
not the example directory.

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link

openhands-ai bot commented Dec 16, 2025

Summary

I reviewed the diff from main and tested the example introduced in PR #1383. Here are my findings:

Is the Example Runnable?

Yes, the example is runnable with one bug fix that I made and pushed.

Bug Found and Fixed

The original build_custom_image.sh script had a bug - it used the example directory ($SCRIPT_DIR) as the Docker build context, but the Dockerfile expects to be built from the repository root because it copies SDK packages like openhands-sdk, openhands-tools, etc.

Fix applied:

  • Added REPO_ROOT variable pointing to the repository root (3 levels up from the example directory)
  • Changed the build context from $SCRIPT_DIR to $REPO_ROOT
  • Added a log message showing the build context

Test Results

After the fix, I successfully:

  1. Built the Docker image - The custom agent server image built successfully in ~94 seconds
  2. Started the container - The agent server started and became healthy
  3. Custom tools were registered - The server logged: "Dynamically registered 4 tools for conversation: ['ReportBugTool', 'file_editor', 'task_tracker', 'terminal']"
  4. Conversation was created - The conversation was created successfully with the custom tool
  5. LLM call failed - Due to an external LLM API authentication error (not a code issue)

The LLM authentication error ("Invalid proxy server token passed") is an external dependency issue with the LLM proxy server, not a problem with the example code itself. The infrastructure and custom tool registration work correctly.

Commit Pushed

I pushed the fix to the PR branch:

fix: Use repository root as Docker build context for custom tool example

The Dockerfile copies SDK packages from the repository root (openhands-sdk,
openhands-tools, etc.), so the build context must be the repository root,
not the example directory.

The PR is now updated at: #1383

View full conversation

@openhands-ai
Copy link

openhands-ai bot commented Dec 16, 2025

Looks like there are a few issues preventing this PR from being merged!

  • GitHub Actions are failing:
    • [Optional] Docs example

If you'd like me to help, just leave a comment, like

@OpenHands please fix the failing actions on PR #1383 at branch `implement-custom-tools-remote-agent`

Feel free to include any additional details that might help me get this PR into a better state.

You can manage your notification settings

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.

Document and implement custom tool support for remote agent server

3 participants