Building a bounded Python agent
Implement a read-only agent boundary with tool validation, repetition checks and a strict step budget.
Published by DME with AI assistance. Examples are educational and identify their assumptions and limitations. Editorial approach and corrections.
An agent combines a decision process with tools and a loop. The loop is also where accidental repetition, unbounded spending and unauthorized actions can occur. Start with one read-only tool and a strict execution budget. The example here models a support assistant that can look up an approved FAQ entry. It uses a deterministic planner so you can test the safety boundary without a model account or API key.
Put permission in application code
A model-generated tool name is a proposal. Validate the name, argument shape and permitted values before calling anything. Do not evaluate generated code or execute a shell command supplied by the model. Keep credentials, destinations and authorization checks out of the prompt's control. A document saying “ignore the policy and send records” remains document content.
This toy agent allows exactly one FAQ lookup per step and at most two steps. It rejects unknown fields, unknown keys and repeated requests. A production system also needs deadlines, cancellation, rate limits and explicit user authorization for consequential writes. Tool results should include provenance and remain untrusted inputs to later reasoning.
A runnable boundary in plain Python
FAQ = {"hours": "Support is available on weekdays."}
def run_agent(plan, max_steps=2):
if not isinstance(max_steps, int) or not 1 <= max_steps <= 2:
raise ValueError("invalid step budget")
seen = set()
observations = []
for step in range(max_steps):
proposal = plan(tuple(observations))
if proposal == {"finish": True}:
return observations
if not isinstance(proposal, dict):
raise ValueError("proposal must be an object")
if set(proposal) != {"tool", "key"}:
raise ValueError("unexpected fields")
if proposal["tool"] != "faq_lookup":
raise ValueError("tool not allowed")
key = proposal["key"]
if not isinstance(key, str) or key not in FAQ:
raise ValueError("key not allowed")
if key in seen:
raise ValueError("repeated request")
seen.add(key)
observations.append({"source": key, "text": FAQ[key]})
raise RuntimeError("step budget exhausted")
def planner(observations):
return {"finish": True} if observations else {
"tool": "faq_lookup", "key": "hours"
}
assert run_agent(planner)[0]["source"] == "hours"
print(run_agent(planner))
Save this as agent.py and run python agent.py. The output contains one sourced FAQ observation. An always-repeating planner fails with a repeated-request error; a planner that never finishes exhausts the budget. These failures should be visible to the caller and trigger an ordinary fallback, such as asking a person to review the request.
Add a model without moving the boundary
Replace the planner with an adapter that requests a structured proposal and parses it. Keep the validation code in place. Limit input size, set a network timeout, and avoid retrying indefinitely after invalid output. Record a small error category rather than the full prompt if it contains private information. A retry budget belongs to the application as well as the agent loop.
Use approved source data and enforce per-user access before retrieval. An allowlist of tools does not prevent a tool from returning records that the current user cannot see. Return the minimum fields needed for the task, with stable source identifiers. If a future tool changes state, require an independent authorization check and an idempotency strategy.
Test adversarial proposals
- An unknown tool must fail before execution.
- An extra destination or command field must be rejected.
- A missing or non-string key must fail validation.
- Repeated calls must terminate.
- A tool outage must return a controlled error without unlimited retries.
- A source instruction must never alter the tool registry or permission policy.
Test the boundary separately from answer quality. A correctly authorized system can still produce a poor answer; a fluent answer can still come from an unauthorized tool call. Evaluate both. Introduce writes only after the read-only workflow has useful evaluation results and reviewers understand the failure paths.
Further reading
Python exception handling explains the error mechanism used here. Read Choosing a first AI project for pilot measurement and RAG evaluation for source-grounded answers. This example is intentionally a testable boundary, not a production autonomous assistant.