# Stuck Detector

> Detect and handle stuck agents automatically with timeout mechanisms.

<Note>
  This example is available on GitHub: [examples/01\_standalone\_sdk/20\_stuck\_detector.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/20_stuck_detector.py)
</Note>

The Stuck Detector automatically identifies when an agent enters unproductive patterns such as repeating the same actions, encountering repeated errors, or engaging in monologues. By analyzing the conversation history after the last user message, it detects five types of stuck patterns:

1. **Repeating Action-Observation Cycles**: The same action produces the same observation repeatedly (4+ times)
2. **Repeating Action-Error Cycles**: The same action repeatedly results in errors (3+ times)
3. **Agent Monologue**: The agent sends multiple consecutive messages without user input or meaningful progress (3+ messages)
4. **Alternating Patterns**: Two different action-observation pairs alternate in a ping-pong pattern (6+ cycles)
5. **Context Window Errors**: Repeated context window errors that indicate memory management issues

When enabled (which is the default), the stuck detector monitors the conversation in real-time and can automatically halt execution when stuck patterns are detected, preventing infinite loops and wasted resources.

For more information about the detection algorithms and how pattern matching works, refer to the [StuckDetector source code](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/stuck_detector.py).

```python icon="python" expandable examples/01_standalone_sdk/20_stuck_detector.py theme={null}
import os

from pydantic import SecretStr

from openhands.sdk import (
    LLM,
    Conversation,
    Event,
    LLMConvertibleEvent,
    get_logger,
)
from openhands.tools.preset.default import get_default_agent


logger = get_logger(__name__)

# Configure LLM
api_key = os.getenv("LLM_API_KEY")
assert api_key is not None, "LLM_API_KEY environment variable is not set."
model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929")
base_url = os.getenv("LLM_BASE_URL")
llm = LLM(
    usage_id="agent",
    model=model,
    base_url=base_url,
    api_key=SecretStr(api_key),
)

agent = get_default_agent(llm=llm)

llm_messages = []


def conversation_callback(event: Event):
    if isinstance(event, LLMConvertibleEvent):
        llm_messages.append(event.to_llm_message())


# Create conversation with built-in stuck detection
conversation = Conversation(
    agent=agent,
    callbacks=[conversation_callback],
    workspace=os.getcwd(),
    # This is by default True, shown here for clarity of the example
    stuck_detection=True,
)

# Send a task that will be caught by stuck detection
conversation.send_message(
    "Please execute 'ls' command 5 times, each in its own "
    "action without any thought and then exit at the 6th step."
)

# Run the conversation - stuck detection happens automatically
conversation.run()

assert conversation.stuck_detector is not None
final_stuck_check = conversation.stuck_detector.is_stuck()
print(f"Final stuck status: {final_stuck_check}")

print("=" * 100)
print("Conversation finished. Got the following LLM messages:")
for i, message in enumerate(llm_messages):
    print(f"Message {i}: {str(message)[:200]}")

# Report cost
cost = llm.metrics.accumulated_cost
print(f"EXAMPLE_COST: {cost}")
```

```bash Running the Example theme={null}
export LLM_API_KEY="your-api-key"
cd agent-sdk
uv run python examples/01_standalone_sdk/20_stuck_detector.py
```

## How It Works

In this example, the agent is deliberately given a task designed to trigger stuck detection - executing the same `ls` command 5 times in a row. The stuck detector analyzes the event history and identifies the repetitive pattern:

1. The conversation proceeds normally until the agent starts repeating actions
2. After detecting the pattern (4 identical action-observation pairs), the stuck detector flags the conversation as stuck
3. The conversation can then handle this gracefully, either by stopping execution or taking corrective action

The example demonstrates that stuck detection is enabled by default (`stuck_detection=True`), and you can check the stuck status at any point using `conversation.stuck_detector.is_stuck()`.

## Pattern Detection

The stuck detector compares events based on their semantic content rather than object identity. For example:

* **Actions** are compared by their tool name, action content, and thought (ignoring IDs and metrics)
* **Observations** are compared by their observation content and tool name
* **Errors** are compared by their error messages
* **Messages** are compared by their content and source

This allows the detector to identify truly repetitive behavior while ignoring superficial differences like timestamps or event IDs.

## Next Steps

* **[Conversation Pause and Resume](/sdk/guides/convo-pause-and-resume)** - Manual execution control
* **[Hello World](/sdk/guides/hello-world)** - Learn the basics of the SDK


---

> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.openhands.dev/llms.txt