The research question
What is the agent allowed to decide, and who checks it? Adding a language model to a script does not remove the need for data contracts, accounting, or evaluation. It introduces another source of uncertain output.
Our architecture will separate five responsibilities: observations, proposal, validation, paper recording, and review. The proposal is untrusted input even when it looks reasonable. It cannot create its own evidence, choose arbitrary tools, or silently change the experiment's rules.
Time: approximately 30 minutes. You need: Python and a text editor. This lab uses fixed strings in place of a model response, so it needs no API key and makes no paid requests.
Step 1 — Define a deliberately small decision
For this experiment, the only allowed decisions are paper and skip. A proposal must refer to an observation we supplied. Its confidence score must be a finite number between zero and one. We retain that score as a model claim, not a measured probability of success.
The output must contain exactly three keys. Unknown fields are rejected rather than interpreted as new instructions. Most importantly, the lab has no function capable of sending an order. Changing a JSON field cannot grant a capability that does not exist.
Step 2 — Put validation between suggestion and action
Save this as lab_05.py. The three test proposals are: a valid paper suggestion, a suggestion referencing nonexistent evidence, and a suggestion containing an unauthorized tool field.
import json
import math
observations = {"obs-001": {"asset": "SYNTHETIC_DEMO", "source": "fixture_v1"}}
def validate(raw):
if len(raw) > 1000:
raise ValueError("response too large")
proposal = json.loads(raw)
if not isinstance(proposal, dict):
raise ValueError("expected an object")
if set(proposal) != {"observation_id", "decision", "confidence"}:
raise ValueError("unexpected or missing fields")
if not isinstance(proposal["observation_id"], str):
raise ValueError("invalid observation id")
if proposal["observation_id"] not in observations:
raise ValueError("unknown observation")
if proposal["decision"] not in ("paper", "skip"):
raise ValueError("decision not allowed")
confidence = proposal["confidence"]
if isinstance(confidence, bool) or not isinstance(confidence, (int, float)):
raise ValueError("confidence must be numeric")
if not 0 <= confidence <= 1 or not math.isfinite(confidence):
raise ValueError("confidence out of range")
return proposal
proposals = [
'{"observation_id":"obs-001","decision":"paper","confidence":0.6}',
'{"observation_id":"invented","decision":"paper","confidence":0.9}',
'{"observation_id":"obs-001","decision":"paper","confidence":0.9,"tool":"send_order"}',
]
accepted = []
for number, raw in enumerate(proposals, start=1):
try:
proposal = validate(raw)
except (ValueError, TypeError) as error:
print(f"proposal={number} rejected: {error}")
else:
accepted.append(proposal)
print(f"proposal={number} accepted: {proposal['decision']}")
assert len(accepted) == 1
print("paper_records=1 external_actions=0")
Expected output:
proposal=1 accepted: paper
proposal=2 rejected: unknown observation
proposal=3 rejected: unexpected or missing fields
paper_records=1 external_actions=0
Step 3 — Test a deceptive input
Replace the first proposal's confidence with true, then with 2, then with NaN, one experiment at a time. They should all be rejected. Python treats booleans as a kind of integer, which is why the validator checks for them explicitly. The standard JSON decoder can also accept non-finite numeric values; our finite-number check rejects them at the boundary.
After testing, restore the original proposals so the expected-output comparison passes. Add each rejected example to a small test note instead of deleting the evidence of how it behaved.
Step 4 — Draw the responsibility boundary
In your own words, describe the pipeline: a collector records observations; a proposer suggests a bounded response; a validator checks structure and evidence identity; a paper recorder appends an accepted record; an evaluator compares outcomes later.
If you add an AI provider in a later project, the same validator belongs after the response is decoded. Input instructions alone are not enforcement. A document or webpage saying “ignore the rules” remains source data, not authorization to change the program. Do not pass arbitrary generated strings to eval, a shell, or a database statement.
Troubleshooting
A JSON syntax error means the response did not satisfy even the first parsing layer. Reject it and retain an appropriately redacted error record; do not repair it by running code found inside the string. If every input passes, confirm that you did not remove the exact-key and observation-ID checks. If a new legitimate field is needed, revise the schema deliberately and version that change.
Evidence and limits
This demonstrates a narrow validation boundary with no external execution authority. It does not demonstrate a secure production agent, accurate predictions, or model calibration. Production integrations also need authentication, scoped permissions, timeouts, request-size limits, audit records, and defenses matched to their real capabilities.
Completion check: Explain why the high-confidence invalid proposal is rejected and why a valid proposal still says nothing about profitability.
Next edition: Make automation observable and retries deliberate.
Primary references
- Python JSON documentation documents decoding behavior, including handling of non-finite numbers.
- Python math documentation documents the finite-number check. The validation policy and architecture are specific choices for this teaching lab.