diff --git a/openhands/controller/agent_controller.py b/openhands/controller/agent_controller.py index ef3d162d9d..3f2ad87674 100644 --- a/openhands/controller/agent_controller.py +++ b/openhands/controller/agent_controller.py @@ -895,7 +895,7 @@ class AgentController: # Synchronize spend across all llm services with the budget flag self.state_tracker.sync_budget_flag_with_metrics() - if self._is_stuck(): + if self.agent.config.enable_stuck_detection and self._is_stuck(): await self._react_to_exception( AgentStuckInLoopError('Agent got stuck in a loop') ) diff --git a/openhands/core/config/README.md b/openhands/core/config/README.md index c612a08244..b16fac1fb5 100644 --- a/openhands/core/config/README.md +++ b/openhands/core/config/README.md @@ -32,6 +32,7 @@ The `load_from_env` function in the config package is responsible for loading co export LLM_API_KEY='your_api_key_here' export LLM_MODEL='gpt-4' export AGENT_MEMORY_ENABLED='true' +export AGENT_ENABLE_STUCK_DETECTION='false' # Disable loop detection export SANDBOX_TIMEOUT='300' ``` diff --git a/openhands/core/config/agent_config.py b/openhands/core/config/agent_config.py index 3c506c9382..b9b5873e9e 100644 --- a/openhands/core/config/agent_config.py +++ b/openhands/core/config/agent_config.py @@ -51,6 +51,8 @@ class AgentConfig(BaseModel): """Whether to enable SoM (Set of Marks) visual browsing.""" enable_plan_mode: bool = Field(default=True) """Whether to enable plan mode, which uses the long horizon system message and add the new tool - task_tracker - for planning, tracking and executing complex tasks.""" + enable_stuck_detection: bool = Field(default=True) + """Whether to enable stuck/loop detection. When disabled, the agent will not automatically detect and recover from loops.""" condenser: CondenserConfig = Field( # The default condenser is set to the conversation window condenser -- if # we use NoOp and the conversation hits the LLM context length limit, diff --git a/tests/unit/controller/test_agent_controller.py b/tests/unit/controller/test_agent_controller.py index 2aa5192c7c..da12ee8f9e 100644 --- a/tests/unit/controller/test_agent_controller.py +++ b/tests/unit/controller/test_agent_controller.py @@ -94,6 +94,7 @@ def mock_agent_with_stats(): ) agent_config.disabled_microagents = [] agent_config.enable_mcp = True + agent_config.enable_stuck_detection = True llm_registry.service_to_llm.clear() mock_llm = llm_registry.get_llm('agent_llm', llm_config) agent.llm = mock_llm diff --git a/tests/unit/controller/test_agent_controller_loop_recovery.py b/tests/unit/controller/test_agent_controller_loop_recovery.py index 36c40f0c42..562825cc6b 100644 --- a/tests/unit/controller/test_agent_controller_loop_recovery.py +++ b/tests/unit/controller/test_agent_controller_loop_recovery.py @@ -372,3 +372,39 @@ class TestAgentControllerLoopRecovery: assert mock_controller.state.end_id == 5, ( f'Expected end_id to be 5, got {mock_controller.state.end_id}' ) + + def test_stuck_detection_config_option_exists(self): + """Test that the enable_stuck_detection config option exists and defaults to True.""" + from openhands.core.config.agent_config import AgentConfig + + # Create a default config + config = AgentConfig() + + # Verify the attribute exists and defaults to True + assert hasattr(config, 'enable_stuck_detection') + assert config.enable_stuck_detection is True + + # Verify we can create a config with it disabled + config_disabled = AgentConfig(enable_stuck_detection=False) + assert config_disabled.enable_stuck_detection is False + + def test_stuck_detection_config_from_env(self): + """Test that enable_stuck_detection can be set via environment variable.""" + import os + + from openhands.core.config.agent_config import AgentConfig + + # Test with enabled (default) + os.environ.pop('AGENT_ENABLE_STUCK_DETECTION', None) + config = AgentConfig() + assert config.enable_stuck_detection is True + + # Test with explicitly disabled + os.environ['AGENT_ENABLE_STUCK_DETECTION'] = 'false' + # Need to reload for env var to take effect in real usage + # For this test, we just verify the config accepts the parameter + config_disabled = AgentConfig(enable_stuck_detection=False) + assert config_disabled.enable_stuck_detection is False + + # Cleanup + os.environ.pop('AGENT_ENABLE_STUCK_DETECTION', None) diff --git a/tests/unit/controller/test_agent_controller_posthog.py b/tests/unit/controller/test_agent_controller_posthog.py index 998a8f5fc1..630c18e3aa 100644 --- a/tests/unit/controller/test_agent_controller_posthog.py +++ b/tests/unit/controller/test_agent_controller_posthog.py @@ -57,6 +57,7 @@ def mock_agent_with_stats(): ) agent_config.disabled_microagents = [] agent_config.enable_mcp = True + agent_config.enable_stuck_detection = True llm_registry.service_to_llm.clear() mock_llm = llm_registry.get_llm('agent_llm', llm_config) agent.llm = mock_llm