All tutorials
Track 18·State & Control

The loop guard and max_loop_iterations

Every loop in a DesignExecute plan has a built-in iteration counter. DesignExecuteConfig(max_loop_iterations=N) sets the cap. Trip it to see the error; raise it to let the task finish.

intermediate8 min
Video coming soon
Browse this tutorial's folder in tutorials-pygithub.com/OpenSymbolicAI/tutorials-py/tree/main/18-loop-guard

Track 17 switched the base class to DesignExecute to allow loops. This tutorial shows what keeps those loops from running forever: an AST-injected iteration counter that stops the loop if it exceeds max_loop_iterations.

The plan#

The sum-of-squares plan from Track 17 loops exactly 100 times. Pass it directly to execute() so the result is deterministic regardless of what model you use.

python
PLAN = """\
total = 0
for i in range(1, 101):
    sq = square(i)
    total = add(total, sq)
return format_result("Sum of squares 1 to 100", total)
"""

Setting the limit#

python
from opensymbolicai.models import DesignExecuteConfig

config = DesignExecuteConfig(max_loop_iterations=N)
agent = Accumulator(llm=llm, config=config)
result = agent.execute(PLAN)

Tripping the guard#

With max_loop_iterations=50, the loop body runs 50 times and fails on iteration 51:

python
config = DesignExecuteConfig(max_loop_iterations=50)
agent = Accumulator(llm=llm, config=config)
result = agent.execute(PLAN)

last_step = result.trace.steps[-1]
print(last_step.error)
text
Loop exceeded maximum iterations (50)

Letting it finish#

With max_loop_iterations=100, all 100 iterations fit:

python
config = DesignExecuteConfig(max_loop_iterations=100)
agent = Accumulator(llm=llm, config=config)
result = agent.execute(PLAN)

if result.trace.all_succeeded:
    print(result.get_value())
    print(f"primitive calls: {len(result.trace.steps)}")
text
Sum of squares 1 to 100: 338350
primitive calls: 201

Full script#

python
# main.py
from accumulator import Accumulator
from opensymbolicai.llm import LLMConfig
from opensymbolicai.models import DesignExecuteConfig

PLAN = """\
total = 0
for i in range(1, 101):
    sq = square(i)
    total = add(total, sq)
return format_result("Sum of squares 1 to 100", total)
"""


def run(label: str, max_loop_iterations: int) -> None:
    llm = LLMConfig(provider="ollama", model="qwen2.5-coder:7b")
    config = DesignExecuteConfig(max_loop_iterations=max_loop_iterations)
    agent = Accumulator(llm=llm, config=config)
    result = agent.execute(PLAN)
    last_step = result.trace.steps[-1]

    print(f"--- {label} (max_loop_iterations={max_loop_iterations}) ---")
    if result.trace.all_succeeded:
        print("result:", result.get_value())
        print(f"primitive calls: {len(result.trace.steps)}")
    else:
        print("error:", last_step.error)
    print()


run("limit too small", max_loop_iterations=50)
run("limit sufficient", max_loop_iterations=100)
bash
uv run main.py

The default is 100. Pick a limit that fits the largest range your tasks will ever iterate.