mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 05:37:20 +08:00
feat: Add configurable stuck/loop detection (#11799)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: chuckbutkus <chuck@all-hands.dev>
This commit is contained in:
@@ -895,7 +895,7 @@ class AgentController:
|
|||||||
|
|
||||||
# Synchronize spend across all llm services with the budget flag
|
# Synchronize spend across all llm services with the budget flag
|
||||||
self.state_tracker.sync_budget_flag_with_metrics()
|
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(
|
await self._react_to_exception(
|
||||||
AgentStuckInLoopError('Agent got stuck in a loop')
|
AgentStuckInLoopError('Agent got stuck in a loop')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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_API_KEY='your_api_key_here'
|
||||||
export LLM_MODEL='gpt-4'
|
export LLM_MODEL='gpt-4'
|
||||||
export AGENT_MEMORY_ENABLED='true'
|
export AGENT_MEMORY_ENABLED='true'
|
||||||
|
export AGENT_ENABLE_STUCK_DETECTION='false' # Disable loop detection
|
||||||
export SANDBOX_TIMEOUT='300'
|
export SANDBOX_TIMEOUT='300'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ class AgentConfig(BaseModel):
|
|||||||
"""Whether to enable SoM (Set of Marks) visual browsing."""
|
"""Whether to enable SoM (Set of Marks) visual browsing."""
|
||||||
enable_plan_mode: bool = Field(default=True)
|
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."""
|
"""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(
|
condenser: CondenserConfig = Field(
|
||||||
# The default condenser is set to the conversation window condenser -- if
|
# The default condenser is set to the conversation window condenser -- if
|
||||||
# we use NoOp and the conversation hits the LLM context length limit,
|
# we use NoOp and the conversation hits the LLM context length limit,
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ def mock_agent_with_stats():
|
|||||||
)
|
)
|
||||||
agent_config.disabled_microagents = []
|
agent_config.disabled_microagents = []
|
||||||
agent_config.enable_mcp = True
|
agent_config.enable_mcp = True
|
||||||
|
agent_config.enable_stuck_detection = True
|
||||||
llm_registry.service_to_llm.clear()
|
llm_registry.service_to_llm.clear()
|
||||||
mock_llm = llm_registry.get_llm('agent_llm', llm_config)
|
mock_llm = llm_registry.get_llm('agent_llm', llm_config)
|
||||||
agent.llm = mock_llm
|
agent.llm = mock_llm
|
||||||
|
|||||||
@@ -372,3 +372,39 @@ class TestAgentControllerLoopRecovery:
|
|||||||
assert mock_controller.state.end_id == 5, (
|
assert mock_controller.state.end_id == 5, (
|
||||||
f'Expected end_id to be 5, got {mock_controller.state.end_id}'
|
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)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ def mock_agent_with_stats():
|
|||||||
)
|
)
|
||||||
agent_config.disabled_microagents = []
|
agent_config.disabled_microagents = []
|
||||||
agent_config.enable_mcp = True
|
agent_config.enable_mcp = True
|
||||||
|
agent_config.enable_stuck_detection = True
|
||||||
llm_registry.service_to_llm.clear()
|
llm_registry.service_to_llm.clear()
|
||||||
mock_llm = llm_registry.get_llm('agent_llm', llm_config)
|
mock_llm = llm_registry.get_llm('agent_llm', llm_config)
|
||||||
agent.llm = mock_llm
|
agent.llm = mock_llm
|
||||||
|
|||||||
Reference in New Issue
Block a user