Context that changes while it runs
The context in lessons 15 and 16 was set once. Real context moves: a shift ends, a customer gets verified, a spending limit is reached. This lesson changes it between calls.
To watch that clearly the agent needs to make exactly the calls the lesson wants, in order. The agent from lesson 2 takes a script for this.
from pretend_agent_governance import PretendAgent
agent = PretendAgent(script=[
("issue_refund", {"order_id": "A17", "amount": 40}),
("issue_refund", {"order_id": "B22", "amount": 40}),
])
print(agent.decide("anything at all"))
print(agent.decide("also ignored"))With a script the message is ignored and the calls come back in order. The router is still there for the lessons that want a decision; this is for the lessons that want a fixture.
A shift that ends
policy = PolicyEngine()
policy.add_conditional_permission("support", ConditionalPermission(
tool_name="issue_refund", conditions=[Condition("shift", "eq", "open")]))
policy.set_agent_context("support", {"shift": "open"})
kernel = AgentKernel(policy_engine=policy)
for shift in ["open", "closed"]:
policy.update_agent_context("support", {"shift": shift})
verdict = kernel.intercept_tool_execution("support", "issue_refund", {"amount": 40})
print(shift, "->", "allowed" if verdict is None else "blocked")Same agent, same policy object, same tool, different answers, because something outside the call changed between them. Nothing was rebuilt.
update_agent_context merges, so keys you do not mention keep their values. set_agent_context replaces the whole dictionary, which quietly drops anything another part of your program put there.
A budget that runs out
The same mechanism does spending limits, by keeping a running total in the context and comparing the call against it.
policy.set_agent_context("support", {"remaining": 100})
policy.add_conditional_permission("support", ConditionalPermission(
tool_name="issue_refund", conditions=[Condition("remaining", "gt", 0)]))
for amount in [100, 60]:
verdict = kernel.intercept_tool_execution("support", "issue_refund", {"amount": amount})
print(amount, "allowed" if verdict is None else "blocked")
if verdict is None:
policy.update_agent_context("support", {"remaining": policy.agent_contexts["support"]["remaining"] - amount})The second refund is refused because the first one spent the budget. The counting is yours: the engine compares, and your code is what remembers.
Note what the rule actually says. A condition compares against a fixed value, so this asks whether any budget is left rather than whether this particular refund fits inside it. A call for 500 against a remaining 40 would still pass. Checking the call against the balance needs the two numbers in one comparison, which is what a custom rule from lesson 8 is for.
update_agent_context is a mutating method, so a frozen engine refuses it, and a policy that needs to move context at runtime cannot be frozen. Keep the rules frozen and the counters somewhere else if you need both.- Make the budget condition
gteand see whether a refund of exactly the remaining amount goes through. - Freeze the engine before the loop and read the error.
- Swap
update_agent_contextforset_agent_contextand watch the shift key disappear.
You understood something today that you didn't yesterday.