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.
Before you start
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.
# 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#
# 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)")uv run main.pyOutput (with per-iteration detail from the full example):
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.statusafterseek()and branch onGoalStatus.ACHIEVEDvsGoalStatus.MAX_ITERATIONS. result.iteration_countis exact. Withmax_iterations=3and the limit firing,iteration_countis 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_iterationsto a small multiple of that. Too tight and you cut off convergence; too loose and runaway iterations cost tokens.