Stop conditions define when agents should stop execution by inspecting run history after each step. Unlike max_steps which is a hard safety limit, stop conditions represent goal completion, budget constraints, or loop detection.
Why use stop conditions:
- Goal Completion - Stop when agents call specific tools, produce expected outputs, or achieve objectives
- Safety Limits - Prevent runaway costs by setting token, cost, or time budgets
- Loop Detection - Detect and stop stuck agents repeating tools or hitting errors
When to use stop conditions:
Use stop conditions to define success criteria for agent runs. Use max_steps as a safety backstop. Use hooks for event-driven control requiring complex logic.
This guide covers goal completion, safety limits, loop detection, combining conditions, and custom stop condition patterns.
import dreadnode as dn
from dreadnode.agent.stop import tool_use, generation_count
agent = dn.Agent(
name="task-runner",
model="gpt-4o",
tools=[dn.agent.tools.fs.Filesystem(path=".", variant="read")],
stop_conditions=[
tool_use("finish_task"), # Stop when task is complete
generation_count(20), # Or after 20 LLM calls (safety limit)
],
)
Stop Conditions vs Other Mechanisms
| Mechanism | Purpose | When to use |
|---|
stop_conditions | Define success criteria | Goal completion, budget limits |
max_steps | Hard limit on think-act cycles | Prevent runaway agents |
Hooks returning Finish | React to specific events | Complex conditional logic |
Stop conditions check the state of the run. Hooks react to individual events. Use stop conditions for declarative goals; use hooks for event-driven control.
Categories of Stop Conditions
Goal Completion
Stop when the agent achieves its objective:
from dreadnode.agent.stop import tool_use, output, tool_output
# Agent calls a completion tool
tool_use("finish_task")
# Agent's response contains a marker
output("TASK_COMPLETE")
# A tool returns a specific result (e.g., finding a flag)
tool_output(r"flag\{[a-z0-9]+\}", regex=True)
Safety Limits
Prevent runaway costs or infinite loops:
from dreadnode.agent.stop import generation_count, token_usage, estimated_cost, elapsed_time
generation_count(50) # Max LLM inference calls
token_usage(100_000) # Max tokens consumed
estimated_cost(5.0) # Max cost in USD
elapsed_time(max_seconds=600) # Max wall-clock time
Loop Detection
Detect when an agent is stuck:
from dreadnode.agent.stop import no_new_tool_used, tool_error
# Stop if no new tools used for 3 consecutive steps
no_new_tool_used(for_steps=3)
# Stop on tool errors (gracefully handled)
tool_error()
tool_error("risky_operation") # Specific tool only
Combining Conditions
Use | (OR) when any condition should trigger stopping:
from dreadnode.agent.stop import tool_use, generation_count, estimated_cost
# Stop on success OR safety limits
stop = (
tool_use("finish_task") |
tool_use("give_up") |
generation_count(50) |
estimated_cost(10.0)
)
Use & (AND) when multiple conditions must all be true:
from dreadnode.agent.stop import tool_output, output
# Stop when flag is found in both tool output AND agent response
stop = tool_output(r"flag\{.*\}") & output("flag")
Agent Stalling
An agent “stalls” when it produces a response with no tool calls while stop conditions exist but aren’t met. This triggers an AgentStalled event.
The never() condition is specifically designed to force stalling behavior:
from dreadnode.agent.stop import never
from dreadnode.agent.events import AgentStalled
from dreadnode.agent.hooks import retry_with_feedback
agent = dn.Agent(
name="continuous-worker",
model="gpt-4o-mini",
tools=[dn.agent.tools.fs.Filesystem(path=".", variant="read")],
stop_conditions=[never()], # Never satisfied naturally
hooks=[
retry_with_feedback(
AgentStalled,
"Continue working. Use finish_task when done."
)
],
)
This pattern ensures the agent keeps working until it explicitly calls a completion tool. TaskAgent uses this pattern by default.
If an agent has no stop conditions and stops calling tools, it finishes successfully without stalling. Stalling only occurs when conditions exist but aren’t met.
Custom Stop Conditions
Create conditions for domain-specific logic:
from dreadnode.agent.stop import StopCondition
from dreadnode.agent.events import AgentEvent, ToolEnd
from collections.abc import Sequence
def files_analyzed(min_count: int) -> StopCondition:
"""Stop after analyzing a minimum number of files."""
def check(events: Sequence[AgentEvent]) -> bool:
analyze_calls = [
e for e in events
if isinstance(e, ToolEnd) and e.tool_call.name == "analyze_file"
]
return len(analyze_calls) >= min_count
return StopCondition(check, name="files_analyzed")
agent = dn.Agent(
name="file-analyzer",
model="gpt-4o",
tools=[dn.agent.tools.fs.Filesystem(path=".", variant="read")],
stop_conditions=[files_analyzed(10)]
)
The function receives the full event history, giving you access to all tool calls, generations, and timing information.
Common Patterns
TaskAgent-style completion
from dreadnode.agent.stop import tool_use
# Stop when agent explicitly finishes or gives up
stop_conditions=[
tool_use("finish_task") | tool_use("give_up_on_task")
]
CTF/capture-the-flag
from dreadnode.agent.stop import tool_output, generation_count
# Stop when flag is found, or give up after 30 generations
stop_conditions=[
tool_output(r"flag\{.*\}", regex=True) |
generation_count(30)
]
Budget-constrained exploration
from dreadnode.agent.stop import estimated_cost, token_usage
# Stay within budget
stop_conditions=[
estimated_cost(2.0) |
token_usage(50_000)
]
See the SDK reference for complete function signatures and parameters.