All tutorials
Track 32·State & Control

Multi-turn conversations with multi_turn=True

Set multi_turn=True and call agent.run() multiple times on the same agent. State held in instance variables persists across turns. The model receives the conversation history on each turn.

intermediate8 min
Video coming soon
Browse this tutorial's folder in tutorials-pygithub.com/OpenSymbolicAI/tutorials-py/tree/main/32-multiturn-cart

Every previous tutorial calls agent.run() once and reads the result. The one new thing: set multi_turn=True in PlanExecuteConfig and call agent.run() multiple times on the same agent instance. The model receives the full conversation history on each turn. State held in instance variables persists because the agent object is reused.

The agent#

A shopping cart whose state lives in self._cart, an instance variable the model never sees.

python
# cart.py
from pydantic import BaseModel
from opensymbolicai.blueprints import PlanExecute
from opensymbolicai.core import primitive
from opensymbolicai.models import PlanExecuteConfig


class Item(BaseModel):
    price: float
    quantity: int


class Cart(BaseModel):
    items: dict[str, Item] = {}


class ShoppingCart(PlanExecute):

    def __init__(self, llm, config=None) -> None:
        super().__init__(llm=llm, config=config)
        self._cart = Cart()

    @primitive(read_only=False)
    def add_item(self, name: str, price: float, quantity: int) -> str:
        """Add an item to the cart by name, unit price, and quantity."""
        self._cart = Cart(items={**self._cart.items,
                                  name: Item(price=price, quantity=quantity)})
        return f"added {name}"

    @primitive(read_only=False)
    def remove_item(self, name: str) -> str:
        """Remove an item from the cart by name."""
        self._cart = Cart(items={k: v for k, v in self._cart.items.items()
                                  if k != name})
        return f"removed {name}"

    @primitive(read_only=True)
    def cart_total(self) -> float:
        """Return the total cost of all items in the cart."""
        return sum(item.price * item.quantity
                   for item in self._cart.items.values())

    @primitive(read_only=True)
    def list_items(self) -> list[str]:
        """Return the names of all items currently in the cart."""
        return list(self._cart.items.keys())

There is no new_cart primitive. The cart is created in __init__ and mutated in place. The model never sees the cart object; it just calls add_item and remove_item by name and price.

Four turns on one agent#

python
# main.py
from opensymbolicai.llm import LLMConfig
from opensymbolicai.models import PlanExecuteConfig

TASKS = [
    "Add to the cart: milk at $2.50, eggs at $3.99, bread at $4.50, "
    "butter at $5.99, yogurt at $1.99, and cheese at $6.50.",
    "Add to the cart: chicken at $8.99, pasta at $2.25, tomatoes at $3.50, "
    "garlic at $1.50, olive oil at $7.99, and onions at $1.25.",
    "Remove eggs and butter from the cart.",
    "What is the total?",
]

llm = LLMConfig(provider="ollama", model="qwen2.5-coder:7b")
config = PlanExecuteConfig(multi_turn=True)
agent = ShoppingCart(llm=llm, config=config)

for task in TASKS:
    result = agent.run(task)
    print(f"Task:   {task}")
    print(f"Plan:   {result.plan}")
    print(f"Result: {result.result}")
    print()
bash
uv run main.py

Output:

text
Task:   Add to the cart: milk at $2.50, eggs at $3.99 ...
Plan:
  r1 = add_item('milk', 2.50, 1)
  r2 = add_item('eggs', 3.99, 1)
  r3 = add_item('bread', 4.50, 1)
  r4 = add_item('butter', 5.99, 1)
  r5 = add_item('yogurt', 1.99, 1)
  r6 = add_item('cheese', 6.50, 1)
  return r1 + ', ' + r2 + ', ' + r3 + ', ' + r4 + ', ' + r5 + ', ' + r6
Result: added milk, added eggs, added bread, added butter, added yogurt, added cheese

Task:   Add to the cart: chicken at $8.99, pasta at $2.25 ...
Plan:
  r1 = add_item('chicken', 8.99, 1)
  ...
Result: added chicken, added pasta, added tomatoes, added garlic, added olive oil, added onions

Task:   Remove eggs and butter from the cart.
Plan:
  r1 = remove_item('eggs')
  r2 = remove_item('butter')
  return r1 + ', ' + r2
Result: removed eggs, removed butter

Task:   What is the total?
Plan:
  total = cart_total()
  return total
Result: 40.97

What to notice#

  • No cart variable in any plan. The model calls add_item('milk', 2.50, 1) and gets back "added milk". It never holds a reference to the cart. State lives in self._cart, which persists because the agent object is reused.
  • Turn 3 knows what was added in turns 1 and 2. multi_turn=True puts the conversation history in the prompt, so the model knows eggs and butter exist to remove.
  • The total reflects all four turns. Turn 4 calls cart_total() with no arguments. The right answer comes back because self._cart accumulated everything from the previous turns.
  • Two separate instances have separate carts. Create a second ShoppingCart and its self._cart starts empty. There is no shared state between instances.