All tutorials
Track 17·State & Control

When you need DesignExecute

PlanExecute only allows assignment statements. When a task needs a loop, switch the base class to DesignExecute. Everything else stays the same.

intermediate8 min
Video coming soon
Browse this tutorial's folder in tutorials-pygithub.com/OpenSymbolicAI/tutorials-py/tree/main/17-design-execute

Before you start

Track 1 used PlanExecute. Its generated plans are pure assignment statements: no for, no while, no if. That covers a lot of tasks. When a task requires iteration, DesignExecute is the right base class instead.

The task#

Sum the squares of all integers from 1 to 100. The answer is 338,350.

No model will write 100 inline square() calls. The plan must loop. PlanExecute rejects any plan that contains for or while:

text
PlanExecute rejects the plan: For statements are not allowed in plans

DesignExecute allows it.

The agent#

Three primitives: square, add, and format_result. The only change from a PlanExecute agent is the import and the base class.

python
# accumulator.py
from opensymbolicai.blueprints import DesignExecute
from opensymbolicai.core import primitive


class Accumulator(DesignExecute):
    """Iterative math agent: accumulate values over a range."""

    @primitive(read_only=True)
    def square(self, n: int) -> int:
        """Return n squared."""
        return n * n

    @primitive(read_only=True)
    def add(self, a: int, b: int) -> int:
        """Add two integers."""
        return a + b

    @primitive(read_only=True)
    def format_result(self, label: str, value: int) -> str:
        """Format a labeled result line."""
        return f"{label}: {value}"

Run it#

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

TASK = "What is the sum of the squares of all integers from 1 to 100?"

llm = LLMConfig(provider="ollama", model="qwen2.5-coder:7b")
agent = Accumulator(llm=llm)
result = agent.run(TASK)

print("--- plan ---")
print(result.plan)
print()
print("--- result ---")
print(result.result)
print(f"({len(result.trace.steps)} primitive calls)")
bash
uv run main.py

Output:

text
--- plan ---
total = 0
for i in range(1, 101):
    square_value = square(i)
    total = add(total, square_value)

result_line = format_result("Sum of squares from 1 to 100", total)
return result_line

--- result ---
Sum of squares from 1 to 100: 338350
(201 primitive calls)

201 calls: one square and one add per iteration, plus one format_result at the end.

What DesignExecute permits#

DesignExecute allows for, while, if/elif/else, try/except, and raise in model-generated plans. Everything else that PlanExecute blocks remains blocked: no imports, no function definitions, no exec or eval.

Proving PlanExecute rejects the plan#

Feed the generated plan into a PlanExecute agent's validate_plan:

python
try:
    pe_agent.validate_plan(result.plan)
except ValueError as e:
    print(f"PlanExecute rejects the plan: {e}")
text
PlanExecute rejects the plan: For statements are not allowed in plans

Validation happens before any primitive runs, so nothing executes.