Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f2008ca
Refactor ToolDefinition architecture to use subclass pattern for all …
openhands-agent Oct 30, 2025
38625a6
Merge ToolDefinition into ToolBase and fix all tests
openhands-agent Oct 30, 2025
eb569b9
Fix example file to use ToolBase instead of ToolDefinition
openhands-agent Oct 31, 2025
5de1644
Fix polymorphic tool serialization with explicit type helper
openhands-agent Oct 31, 2025
097b50d
Merge branch 'main' into openhands/simplify-tool-definition-architecture
simonrosenberg Nov 1, 2025
085395c
Merge branch 'main' into openhands/simplify-tool-definition-architecture
xingyaoww Nov 3, 2025
75a7484
Merge branch 'main' into openhands/simplify-tool-definition-architecture
xingyaoww Nov 3, 2025
0f17f17
Prevent direct ToolBase instantiation and enforce subclass pattern
openhands-agent Nov 3, 2025
43b074c
Merge ToolBase and ToolDefinition into single ToolDefinition class
openhands-agent Nov 3, 2025
414a03f
Merge branch 'main' into openhands/simplify-tool-definition-architecture
xingyaoww Nov 3, 2025
1894612
revert
xingyaoww Nov 3, 2025
874b2db
revert polymorphic hacks
xingyaoww Nov 3, 2025
95a7a1f
revert hacks
xingyaoww Nov 3, 2025
b99401f
Remove backward compatibility from browser_use tools
openhands-agent Nov 3, 2025
8290cda
Refactor browser actions to use inheritance hierarchy instead of unio…
openhands-agent Nov 3, 2025
3bbfe6d
Remove backward compatibility singleton comments
openhands-agent Nov 3, 2025
3ef13a4
clean up definitions
xingyaoww Nov 3, 2025
d3667ab
fix
xingyaoww Nov 3, 2025
3d362b7
try fix linter
xingyaoww Nov 3, 2025
1d3e6b4
revert test
xingyaoww Nov 3, 2025
0febb54
Merge branch 'main' into openhands/simplify-tool-definition-architecture
xingyaoww Nov 3, 2025
4acf30d
Fix tests for tool definition refactoring
openhands-agent Nov 3, 2025
3522c71
Merge main into openhands/simplify-tool-definition-architecture
openhands-agent Nov 3, 2025
6172da0
Fix FinishTool.name reference after refactoring
openhands-agent Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions examples/01_standalone_sdk/02_custom_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
LLMConvertibleEvent,
Observation,
TextContent,
ToolDefinition,
ToolBase,
get_logger,
)
from openhands.sdk.tool import (
Expand Down Expand Up @@ -131,14 +131,14 @@ def __call__(self, action: GrepAction, conversation=None) -> GrepObservation: #
cwd = os.getcwd()


def _make_bash_and_grep_tools(conv_state) -> list[ToolDefinition]:
def _make_bash_and_grep_tools(conv_state) -> list[ToolBase]:
"""Create execute_bash and custom grep tools sharing one executor."""

bash_executor = BashExecutor(working_dir=conv_state.workspace.working_dir)
bash_tool = execute_bash_tool.set_executor(executor=bash_executor)

grep_executor = GrepExecutor(bash_executor)
grep_tool = ToolDefinition(
grep_tool = ToolBase(
name="grep",
description=_GREP_DESCRIPTION,
action_type=GrepAction,
Expand Down
2 changes: 0 additions & 2 deletions openhands-sdk/openhands/sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
Observation,
Tool,
ToolBase,
ToolDefinition,
list_registered_tools,
register_tool,
resolve_tool,
Expand Down Expand Up @@ -66,7 +65,6 @@
"ThinkingBlock",
"RedactedThinkingBlock",
"Tool",
"ToolDefinition",
"ToolBase",
"AgentBase",
"Agent",
Expand Down
3 changes: 1 addition & 2 deletions openhands-sdk/openhands/sdk/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer
from openhands.sdk.tool import (
Action,
FinishTool,
Observation,
)
from openhands.sdk.tool.builtins import FinishAction, ThinkAction
Expand Down Expand Up @@ -418,6 +417,6 @@ def _execute_action_event(
on_event(obs_event)

# Set conversation state
if tool.name == FinishTool.name:
if tool.name == "finish":
state.agent_status = AgentExecutionStatus.FINISHED
return obs_event
16 changes: 9 additions & 7 deletions openhands-sdk/openhands/sdk/agent/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from openhands.sdk.logger import get_logger
from openhands.sdk.mcp import create_mcp_tools
from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer
from openhands.sdk.tool import BUILT_IN_TOOLS, Tool, ToolDefinition, resolve_tool
from openhands.sdk.tool import BUILT_IN_TOOLS, Tool, ToolBase, resolve_tool
from openhands.sdk.utils.models import DiscriminatedUnionMixin
from openhands.sdk.utils.pydantic_diff import pretty_pydantic_diff

Expand Down Expand Up @@ -142,7 +142,7 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
)

# Runtime materialized tools; private and non-serializable
_tools: dict[str, ToolDefinition] = PrivateAttr(default_factory=dict)
_tools: dict[str, ToolBase] = PrivateAttr(default_factory=dict)

@property
def prompt_dir(self) -> str:
Expand Down Expand Up @@ -199,7 +199,7 @@ def _initialize(self, state: "ConversationState"):
logger.warning("Agent already initialized; skipping re-initialization.")
return

tools: list[ToolDefinition] = []
tools: list[ToolBase] = []
for tool_spec in self.tools:
tools.extend(resolve_tool(tool_spec, state))

Expand All @@ -220,13 +220,15 @@ def _initialize(self, state: "ConversationState"):
)

# Always include built-in tools; not subject to filtering
tools.extend(BUILT_IN_TOOLS)
# Instantiate built-in tools using their .create() method
for tool_class in BUILT_IN_TOOLS:
tools.extend(tool_class.create(state))

# Check tool types
for tool in tools:
if not isinstance(tool, ToolDefinition):
if not isinstance(tool, ToolBase):
raise ValueError(
f"Tool {tool} is not an instance of 'ToolDefinition'. "
f"Tool {tool} is not an instance of 'ToolBase'. "
f"Got type: {type(tool)}"
)

Expand Down Expand Up @@ -405,7 +407,7 @@ def _walk(obj: object) -> Iterable[LLM]:
yield from _walk(self)

@property
def tools_map(self) -> dict[str, ToolDefinition]:
def tools_map(self) -> dict[str, ToolBase]:
"""Get the initialized tools map.
Raises:
RuntimeError: If the agent has not been initialized.
Expand Down
4 changes: 2 additions & 2 deletions openhands-sdk/openhands/sdk/mcp/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
Action,
Observation,
ToolAnnotations,
ToolDefinition,
ToolBase,
ToolExecutor,
)
from openhands.sdk.tool.schema import Schema
Expand Down Expand Up @@ -114,7 +114,7 @@ def _create_mcp_action_type(action_type: mcp.types.Tool) -> type[Schema]:
return mcp_action_type


class MCPToolDefinition(ToolDefinition[MCPToolAction, MCPToolObservation]):
class MCPToolDefinition(ToolBase[MCPToolAction, MCPToolObservation]):
"""MCP Tool that wraps an MCP client and provides tool functionality."""

mcp_tool: mcp.types.Tool = Field(description="The MCP tool definition.")
Expand Down
2 changes: 0 additions & 2 deletions openhands-sdk/openhands/sdk/tool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@
ExecutableTool,
ToolAnnotations,
ToolBase,
ToolDefinition,
ToolExecutor,
)


__all__ = [
"Tool",
"ToolDefinition",
"ToolBase",
"ToolAnnotations",
"ToolExecutor",
Expand Down
58 changes: 42 additions & 16 deletions openhands-sdk/openhands/sdk/tool/builtins/finish.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections.abc import Sequence
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Self

from pydantic import Field
from rich.text import Text
Expand All @@ -9,13 +9,14 @@
Action,
Observation,
ToolAnnotations,
ToolDefinition,
ToolBase,
ToolExecutor,
)


if TYPE_CHECKING:
from openhands.sdk.conversation.base import BaseConversation
from openhands.sdk.conversation.state import ConversationState


class FinishAction(Action):
Expand Down Expand Up @@ -67,17 +68,42 @@ def __call__(
return FinishObservation(message=action.message)


FinishTool = ToolDefinition(
name="finish",
action_type=FinishAction,
observation_type=FinishObservation,
description=TOOL_DESCRIPTION,
executor=FinishExecutor(),
annotations=ToolAnnotations(
title="finish",
readOnlyHint=True,
destructiveHint=False,
idempotentHint=True,
openWorldHint=False,
),
)
class FinishTool(ToolBase[FinishAction, FinishObservation]):
"""Tool for signaling the completion of a task or conversation."""

@classmethod
def create(
cls,
conv_state: "ConversationState | None" = None, # noqa: ARG003
**params,
) -> Sequence[Self]:
"""Create FinishTool instance.

Args:
conv_state: Optional conversation state (not used by FinishTool).
**params: Additional parameters (none supported).

Returns:
A sequence containing a single FinishTool instance.

Raises:
ValueError: If any parameters are provided.
"""
if params:
raise ValueError("FinishTool doesn't accept parameters")
return [
cls(
name="finish",
action_type=FinishAction,
observation_type=FinishObservation,
description=TOOL_DESCRIPTION,
executor=FinishExecutor(),
annotations=ToolAnnotations(
title="finish",
readOnlyHint=True,
destructiveHint=False,
idempotentHint=True,
openWorldHint=False,
),
)
]
56 changes: 41 additions & 15 deletions openhands-sdk/openhands/sdk/tool/builtins/think.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections.abc import Sequence
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Self

from pydantic import Field
from rich.text import Text
Expand All @@ -9,13 +9,14 @@
Action,
Observation,
ToolAnnotations,
ToolDefinition,
ToolBase,
ToolExecutor,
)


if TYPE_CHECKING:
from openhands.sdk.conversation.base import BaseConversation
from openhands.sdk.conversation.state import ConversationState


class ThinkAction(Action):
Expand Down Expand Up @@ -83,16 +84,41 @@ def __call__(
return ThinkObservation()


ThinkTool = ToolDefinition(
name="think",
description=THINK_DESCRIPTION,
action_type=ThinkAction,
observation_type=ThinkObservation,
executor=ThinkExecutor(),
annotations=ToolAnnotations(
readOnlyHint=True,
destructiveHint=False,
idempotentHint=True,
openWorldHint=False,
),
)
class ThinkTool(ToolBase[ThinkAction, ThinkObservation]):
"""Tool for logging thoughts without making changes."""

@classmethod
def create(
cls,
conv_state: "ConversationState | None" = None, # noqa: ARG003
**params,
) -> Sequence[Self]:
"""Create ThinkTool instance.

Args:
conv_state: Optional conversation state (not used by ThinkTool).
**params: Additional parameters (none supported).

Returns:
A sequence containing a single ThinkTool instance.

Raises:
ValueError: If any parameters are provided.
"""
if params:
raise ValueError("ThinkTool doesn't accept parameters")
return [
cls(
name="think",
description=THINK_DESCRIPTION,
action_type=ThinkAction,
observation_type=ThinkObservation,
executor=ThinkExecutor(),
annotations=ToolAnnotations(
readOnlyHint=True,
destructiveHint=False,
idempotentHint=True,
openWorldHint=False,
),
)
]
Loading
Loading