Tool-Using Agent Runtime with Budgets, Checkpoints and Approval
Build the runtime loop for a tool-using agent. A policy (the model) maps history to a tool call or a final answer. The runtime must bound work, survive a restart mid-run, and never run a side-effecting tool without human approval.
1Agent loop
start(goal) repeatedly asks the policy for the next action, executes tools, and appends results to history until a final answer. Tool exceptions and unknown tools become observations for the model, never crashes.
2Step budget
Stop with status budget_exhausted once max_steps policy calls have been made, including across resumes.
3Checkpoint / resume
checkpoint() serialises all run state to JSON; Agent.resume(blob, policy, tools) rebuilds an agent that continues exactly where the original stopped.
4Human approval
Tools flagged side_effect pause the run with status awaiting_approval and a pending call. decide(approved, note) runs it once with an idempotency key run_id:step, or reports the denial to the model as an observation.
Treat the run as a state machine whose whole state is plain JSON: history, step count, status, and the pending call. Pausing for approval is then just a status plus a checkpoint, and resume is deserialisation. Pass an idempotency key derived from run id and step to side-effecting tools so a crash between executing and checkpointing cannot double-send.
Try it first.
tests.py · 12 passing
sent: list[tuple[str, str]] = []
def search(args: dict, key: str) -> str:
if not args.get("q"):
raise ValueError("empty query")
return f"3 results for {args['q']}"
def send_email(args: dict, key: str) -> str:
sent.append((args["to"], key))
return "sent"
tools = [Tool("search", search), Tool("send_email", send_email, side_effect=True)]
def policy(history: list[dict]) -> dict:
last = history[-1]
if last["role"] == "user":
return {"tool": "search", "args": {"q": "refund policy"}}
if last.get("tool") == "search" and "result" in last:
return {"tool": "send_email", "args": {"to": "ops@example.com"}}
if last.get("tool") == "send_email":
return {"final": "emailed" if "result" in last else "cancelled"}
return {"final": "confused"}
# stage 1: plain loop with a pure tool
a = Agent(lambda h: {"final": "hi"} if len(h) > 2 else {"tool": "search", "args": {"q": "x"}}, tools)
assert a.start("go") == "done" and a.state["answer"] == "hi" and a.state["steps"] == 2
# tool errors and unknown tools become observations
b = Agent(lambda h: {"tool": "nope"} if len(h) == 1 else ({"tool": "search", "args": {}} if len(h) == 3 else {"final": h[-1]["error"]}), tools)
assert b.start("go") == "done" and b.state["answer"] == "ValueError: empty query"
assert b.state["history"][2]["error"] == "unknown tool nope"
# stage 2: step budget
c = Agent(lambda h: {"tool": "search", "args": {"q": "again"}}, tools, max_steps=3)
assert c.start("loop forever") == "budget_exhausted" and c.state["steps"] == 3
# stage 3 + 4: pause on side effect, checkpoint, resume in a "new process", approve
d = Agent(policy, tools)
assert d.start("email ops the refund policy") == "awaiting_approval"
assert sent == [] and d.state["pending"]["key"] == "run-1:2"
blob = d.checkpoint()
d2 = Agent.resume(blob, policy, tools)
assert d2.checkpoint() == blob
assert d2.decide(True) == "done" and d2.state["answer"] == "emailed"
assert sent == [("ops@example.com", "run-1:2")]
try:
d2.decide(True); assert False
except RuntimeError:
pass
# denial goes back to the model as an observation; nothing is sent
e = Agent.resume(blob, policy, tools)
assert e.decide(False, "not today") == "done" and e.state["answer"] == "cancelled"
assert len(sent) == 1
# a resumed run keeps its step budget
f = Agent.resume(blob, policy, tools, max_steps=2)
assert f.decide(True) == "budget_exhausted" and len(sent) == 2What changes in production? Store checkpoints after every step in a durable store with a version for optimistic concurrency, bound tokens and wall-clock time as well as steps, compact long histories by summarising old turns, and route approvals through a queue with expiry so a stale approval cannot run against a changed world.