diff --git a/openhands-sdk/openhands/sdk/agent/base.py b/openhands-sdk/openhands/sdk/agent/base.py index a364dbadea..9e3a818ef8 100644 --- a/openhands-sdk/openhands/sdk/agent/base.py +++ b/openhands-sdk/openhands/sdk/agent/base.py @@ -384,6 +384,13 @@ def verify( if isinstance(event, ActionEvent) and event.tool_name } + # Add builtin tool names from include_default_tools + # These are runtime names like 'finish', 'think' + for tool_class_name in self.include_default_tools: + tool_class = BUILT_IN_TOOL_CLASSES.get(tool_class_name) + if tool_class is not None: + runtime_names.add(tool_class.name) + # Only require tools that were actually used in history. missing_used_tools = used_tools - runtime_names if missing_used_tools: diff --git a/tests/sdk/conversation/local/test_state_serialization.py b/tests/sdk/conversation/local/test_state_serialization.py index ea92ee23b4..23e6b49922 100644 --- a/tests/sdk/conversation/local/test_state_serialization.py +++ b/tests/sdk/conversation/local/test_state_serialization.py @@ -1165,3 +1165,150 @@ def test_conversation_state_cipher_mismatch(): "API_KEY" ].get_value() assert api_key_value is None + + +def test_agent_verify_builtin_tools_included_in_check(): + """Test that verify() correctly includes builtin tools in its check. + + Builtin tools configured via include_default_tools are now correctly + recognized by their runtime names (e.g., 'finish', 'think') when + checking against event history. + """ + from openhands.sdk.agent import AgentBase + from openhands.sdk.event import ActionEvent + from openhands.sdk.llm import MessageToolCall + from openhands.sdk.tool import Tool + + llm = LLM(model="gpt-4o-mini", api_key=SecretStr("test-key"), usage_id="test-llm") + + # Create persisted agent with TerminalTool + persisted_agent_obj = Agent( + llm=llm, + tools=[Tool(name="TerminalTool")], + include_default_tools=["FinishTool"], + ) + + # Events from the session - only 'finish' was used (a builtin tool) + events_with_finish = [ + ActionEvent( + source="agent", + thought=[], + tool_name="finish", # Runtime name, NOT 'FinishTool' + tool_call_id="call_123", + tool_call=MessageToolCall( + id="call_123", + name="finish", + arguments='{"message": "Done!"}', + origin="completion", + ), + llm_response_id="resp_123", + ), + ] + + # Serialize and deserialize to simulate loading from persistence + serialized = persisted_agent_obj.model_dump_json() + persisted_agent = AgentBase.model_validate_json(serialized) + + # Create a runtime agent with DIFFERENT tools (FileEditorTool instead of + # TerminalTool) but still including FinishTool builtin + runtime_agent = Agent( + llm=llm, + tools=[Tool(name="FileEditorTool")], # Different from persisted! + include_default_tools=["FinishTool"], + ) + + # This should PASS since 'finish' is a builtin tool available via + # include_default_tools. The verify method adds builtin tool runtime names + # to the check. + result = runtime_agent.verify(persisted_agent, events=events_with_finish) + assert result is runtime_agent + + +def test_agent_verify_think_builtin_tool_included(): + """Test that 'think' builtin tool is correctly included in event check.""" + from openhands.sdk.agent import AgentBase + from openhands.sdk.event import ActionEvent + from openhands.sdk.llm import MessageToolCall + from openhands.sdk.tool import Tool + + llm = LLM(model="gpt-4o-mini", api_key=SecretStr("test-key"), usage_id="test-llm") + + persisted_agent_obj = Agent( + llm=llm, + tools=[Tool(name="TerminalTool")], + include_default_tools=["ThinkTool"], + ) + + events_with_think = [ + ActionEvent( + source="agent", + thought=[], + tool_name="think", + tool_call_id="call_123", + tool_call=MessageToolCall( + id="call_123", + name="think", + arguments='{"thought": "Let me think..."}', + origin="completion", + ), + llm_response_id="resp_123", + ), + ] + + serialized = persisted_agent_obj.model_dump_json() + persisted_agent = AgentBase.model_validate_json(serialized) + + runtime_agent = Agent( + llm=llm, + tools=[Tool(name="FileEditorTool")], + include_default_tools=["ThinkTool"], + ) + + result = runtime_agent.verify(persisted_agent, events=events_with_think) + assert result is runtime_agent + + +def test_agent_verify_missing_builtin_tool_fails(): + """Test that verify fails when a used builtin tool is not configured.""" + from openhands.sdk.agent import AgentBase + from openhands.sdk.event import ActionEvent + from openhands.sdk.llm import MessageToolCall + from openhands.sdk.tool import Tool + + llm = LLM(model="gpt-4o-mini", api_key=SecretStr("test-key"), usage_id="test-llm") + + persisted_agent_obj = Agent( + llm=llm, + tools=[Tool(name="TerminalTool")], + include_default_tools=["FinishTool"], # Has FinishTool + ) + + events_with_finish = [ + ActionEvent( + source="agent", + thought=[], + tool_name="finish", + tool_call_id="call_123", + tool_call=MessageToolCall( + id="call_123", + name="finish", + arguments='{"message": "Done!"}', + origin="completion", + ), + llm_response_id="resp_123", + ), + ] + + serialized = persisted_agent_obj.model_dump_json() + persisted_agent = AgentBase.model_validate_json(serialized) + + # Runtime agent does NOT have FinishTool in include_default_tools + runtime_agent = Agent( + llm=llm, + tools=[Tool(name="FileEditorTool")], + include_default_tools=[], # No FinishTool! + ) + + # Should fail because 'finish' was used but FinishTool is not configured + with pytest.raises(ValueError, match="missing from runtime.*finish"): + runtime_agent.verify(persisted_agent, events=events_with_finish)