Analyze a plan's structure
Read the primitive calls and read_only flags with agent.analyze_plan. Find out which mutating primitives a plan would touch before you run it.
Before you start
Track 12 ran a plan you already had. Before you run one, you might want to know
what it would do. agent.analyze_plan(plan) tells you. It parses the plan and
reports every primitive call in it: the method name, whether that method is
read-only, and the arguments. Nothing runs and the model is not called. It is
pure inspection of the plan text.
The question it answers: which mutating primitives does this plan touch?
1. A bank account, with reads and writes#
A bank account is a good example because it has a real read/write split.
deposit and withdraw change the balance, so they are declared
read_only=False. can_afford only looks, so it is read_only=True.
# account.py
from opensymbolicai.blueprints import PlanExecute
from opensymbolicai.core import primitive
class Account(PlanExecute):
@primitive(read_only=False)
def deposit(self, balance: float, amount: float) -> float:
"""Add money to the balance."""
return balance + amount
@primitive(read_only=False)
def withdraw(self, balance: float, amount: float) -> float:
"""Take money out of the balance."""
return balance - amount
@primitive(read_only=True)
def can_afford(self, balance: float, price: float) -> bool:
"""Check whether the balance covers a price."""
return balance >= price2. Analyze two plans#
Write two plans by hand: one that only checks the balance, one that changes it. Analyze each.
# main.py
READ_ONLY_PLAN = """ok = can_afford(100, 30)
big = can_afford(100, 250)"""
MUTATING_PLAN = """balance = deposit(100, 50)
balance = withdraw(balance, 30)
ok = can_afford(balance, 200)"""
analysis = agent.analyze_plan(READ_ONLY_PLAN)
for call in analysis.calls: # each is a PrimitiveCall
flag = "read-only" if call.read_only else "mutating"
print(f"{call.method_name}: {flag} args={call.args}")
print("has_mutations:", analysis.has_mutations)uv run main.pyOutput:
--- read-only plan ---
can_afford: read-only args={'arg0': 100, 'arg1': 30}
can_afford: read-only args={'arg0': 100, 'arg1': 250}
has_mutations: False
mutating primitives: []
--- plan that moves money ---
deposit: mutating args={'arg0': 100, 'arg1': 50}
withdraw: mutating args={'arg0': 'balance', 'arg1': 30}
can_afford: read-only args={'arg0': 'balance', 'arg1': 200}
has_mutations: True
mutating primitives: ['deposit', 'withdraw']What you're looking at#
analyze_plan returns a PlanAnalysis. Its .calls are the primitive calls
in order, each a PrimitiveCall with:
method_name: the name of the primitive calledread_only: whether that primitive is read-only (from its decorator)args: the arguments it was given
Two convenience properties sit on top:
has_mutations:Truewhen any call is not read-onlymethod_names: just the names in order
The first plan only calls can_afford, a read-only method, so has_mutations
is False. The second calls deposit and withdraw before checking, so
has_mutations is True and the mutating primitives are right there.
Use analyze_plan to build a gate: check whether a plan has mutations before
deciding whether to run it.