All tutorials
Track 30·Reliability

max_iterations and the no-progress circuit breaker

GoalSeekingConfig(max_iterations=N) stops an agent that hasn't converged. result.status is ACHIEVED or MAX_ITERATIONS. result.iteration_count tells you how many iterations ran.

intermediate7 min
Video coming soon
Browse this tutorial's folder in tutorials-pygithub.com/OpenSymbolicAI/tutorials-py/tree/main/30-no-progress

The one new thing: GoalSeekingConfig(max_iterations=N) is a hard stop on a GoalSeeking agent. When the limit is reached, result.status is MAX_ITERATIONS instead of ACHIEVED. No exception is raised; it is a clean stop you check after seek() returns.

The agent#

A number guesser that binary-searches a secret number (742) in 1–1000. It needs about 9–10 iterations to converge.

python
# guesser.py
from opensymbolicai.blueprints import GoalSeeking
from opensymbolicai.core import decomposition, evaluator, primitive
from opensymbolicai.models import GoalContext, GoalEvaluation, GoalSeekingConfig

SECRET = 742
GOAL = "Guess the secret number between 1 and 1000."


class HintContext(GoalContext):
    low: int = 1
    high: int = 1000
    last_hint: str = "no guess yet"


class Guesser(GoalSeeking):

    def create_context(self, goal: str) -> HintContext:
        return HintContext(goal=goal)

    @primitive(read_only=True)
    def midpoint(self, low: int, high: int) -> int:
        """Return the midpoint of [low, high]."""
        return (low + high) // 2

    @primitive(read_only=True)
    def guess(self, n: int) -> str:
        """Guess n. Returns a hint like 'hot -- go lower' or 'correct'."""
        diff = SECRET - n
        distance = abs(diff)
        direction = "go higher" if diff > 0 else "go lower"
        if distance == 0:  return "correct"
        if distance < 25:  return f"burning -- {direction}"
        if distance < 100: return f"hot -- {direction}"
        if distance < 200: return f"warm -- {direction}"
        if distance < 400: return f"cold -- {direction}"
        return f"freezing -- {direction}"

    @evaluator
    def _check(self, goal: str, context: HintContext) -> GoalEvaluation:
        return GoalEvaluation(goal_achieved=context.last_hint == "correct")

The only change between two runs: max_iterations#

python
# main.py
from opensymbolicai.llm import LLMConfig
from opensymbolicai.models import GoalSeekingConfig, GoalStatus

llm = LLMConfig(provider="ollama", model="qwen2.5-coder:7b")

for max_iter in [3, 15]:
    config = GoalSeekingConfig(max_iterations=max_iter)
    agent = Guesser(llm=llm, config=config)
    result = agent.seek(GOAL)

    if result.status == GoalStatus.ACHIEVED:
        print(f"max_iterations={max_iter}: ACHIEVED in {result.iteration_count} iteration(s)")
    else:
        print(f"max_iterations={max_iter}: MAX_ITERATIONS after {result.iteration_count} iteration(s)")
bash
uv run main.py

Output (with per-iteration detail from the full example):

text
Run: max_iterations=3
    iteration 0: [501, 1000]  hint='cold -- go higher'
    iteration 1: [501, 749]  hint='burning -- go lower'
    iteration 2: [626, 749]  hint='warm -- go higher'
  Status: MAX_ITERATIONS -- circuit breaker fired after 3 iteration(s)

Run: max_iterations=15
    iteration 0: [1, 1000]  hint='no guess yet'
    iteration 1: [501, 1000]  hint='cold -- go higher'
    ...
    iteration 8: [735, 749]  hint='correct'
  Status: ACHIEVED in 9 iteration(s)

Binary search on 1–1000 needs around 9 iterations. With max_iterations=3 the search range is still [626, 749] when the limit fires. With max_iterations=15 there is enough headroom and the agent converges.

What to notice#

  • No exception on limit. The circuit breaker does not crash your program. Check result.status after seek() and branch on GoalStatus.ACHIEVED vs GoalStatus.MAX_ITERATIONS.
  • result.iteration_count is exact. With max_iterations=3 and the limit firing, iteration_count is 3, not an estimate.
  • The limit fires cleanly. The iteration that would have been the 4th never starts. There is no partial work to clean up.
  • Pick a limit from data, not instinct. Run the agent without a limit first, record how many iterations it typically needs, then set max_iterations to a small multiple of that. Too tight and you cut off convergence; too loose and runaway iterations cost tokens.