Lab 03 — Guardrails + Sessions
Phase 21 · Lab 03 · Phase README · Warmup
The problem
Two more OpenAI Agents SDK primitives, both bolted onto the same Runner loop:
- Guardrails — cheap, fast checks that run around the expensive agent. An input guardrail
runs on the first agent's input; if its tripwire fires, the run aborts with
InputGuardrailTripwireTriggeredbefore the model is ever called — you don't pay for a big model on an obviously bad request. An output guardrail runs on the final output; a tripwire raisesOutputGuardrailTripwireTriggeredso a bad answer never reaches the user. Each guardrail returnsGuardrailFunctionOutput(output_info, tripwire_triggered). - Sessions — automatic conversation memory. A
Sessionstores the conversation items so a secondRunner.runon the same session automatically sees the prior turns — no manual history-threading. Build a list-backedInMemorySessionand a realSQLiteSessionon stdlibsqlite3.
What you build
| Piece | What it does |
|---|---|
GuardrailFunctionOutput | (output_info, tripwire_triggered) — the boolean decision + logging info |
InputGuardrail / OutputGuardrail + @input_guardrail / @output_guardrail | wrap a (agent, data) -> GuardrailFunctionOutput function |
*TripwireTriggered exceptions | carry the guardrail_result so callers can log the reason |
InMemorySession, SQLiteSession | get_items / add_items / pop_item / clear_session |
Runner.run_sync(agent, input, session=None) | input guardrails → loop → output guardrails → persist to session |
File map
| File | Role |
|---|---|
lab.py | your implementation (fill the # TODOs) |
solution.py | reference + a worked example (python solution.py): a homework-blocking input guardrail, a PII output guardrail, and a session that remembers across runs |
test_lab.py | 22 tests: benign/tripped input & output guardrails, halt-before-agent, decorators, session round-trip/pop/clear, sqlite isolation-by-id, memory across runs, no-write-on-block, determinism |
requirements.txt | pytest only |
Run it
pytest test_lab.py -v # your lab.py (red until you implement)
LAB_MODULE=solution pytest test_lab.py -v # the reference (green)
python solution.py # the guardrails + session worked example
Success criteria
-
A benign input passes; a tripped input guardrail raises
InputGuardrailTripwireTriggeredand the model is never called (assert the policy didn't run). -
A clean output passes; a tripped output guardrail raises
OutputGuardrailTripwireTriggered; the exception carries theguardrail_result. - With multiple input guardrails, the first tripwire halts the run.
-
A fresh session is empty;
add_items/get_items/pop_item/clear_sessionbehave;SQLiteSessionpersists to real sqlite and two session ids sharing one file are isolated. -
A second
Runner.runon the same session sees the prior turns (a counting agent reports "turn 2"); without a session it's stateless ("turn 1" every time); a blocked run writes nothing to the session. -
All 22 tests pass under both
labandsolution.
How this maps to the real stack
- Guardrails are real.
Agent(input_guardrails=[...], output_guardrails=[...]), the@input_guardrail/@output_guardraildecorators,GuardrailFunctionOutput(output_info, tripwire_triggered), and theInputGuardrailTripwireTriggered/OutputGuardrailTripwireTriggeredexceptions are all the SDK's real API. The intended pattern is exactly the one here: run a small, fast, cheap model (or a regex/classifier) as the guardrail so it adds negligible cost, and let its tripwire short-circuit the expensive agent. In the real SDK input guardrails run concurrently with the first model call and cancel it on a tripwire; running them first, as here, is the same guarantee (no final output escapes) with simpler mechanics. - Sessions are real.
SQLiteSession(session_id, db_path)is a built-in; passsession=toRunner.runand history is prepended and results appended automatically. The SDK also shipsOpenAIConversationsSessionand supports custom backends (Redis, Postgres) behind the sameget_items/add_items/pop_item/clear_sessionprotocol you implemented. - Where this sits vs Phase 10. Phase 10 built a defense-in-depth injection harness (trust boundary, allow-lists, output exfil scan, HITL). SDK guardrails are the framework-native slice of that: the tripwire pattern is the productized "input guard / output guard" layer. The architectural controls (least-privilege tools, the trust boundary) still live in your code around the SDK.
Limits. Real guardrails often call a model, so they're async and can themselves fail;
output_info is typically a pydantic object; sessions store the full Responses-API item shape and
must handle concurrency/pruning at scale. The mechanism — a boolean tripwire that halts the run,
and a keyed store that makes memory automatic — is the faithful core.
Extensions (your own machine)
- Make a guardrail call a model (a second, tiny
Agent) that classifies the input, and return its structured verdict asoutput_info— the real "fast model guards the smart model" pattern. - Add a
max_items/ token-budget pruning policy toSQLiteSession.get_items(keep the last N) and observe how it bounds context growth (Phase 04's problem, at the session layer). - Swap
SQLiteSessionfor a Redis-backed session behind the same 4-method protocol and confirm the Runner doesn't change.
Interview / resume signal
"Added the OpenAI Agents SDK guardrail and session layers to an agent runner: input guardrails that abort with a tripwire before the model is ever called (so bad requests cost nothing), output guardrails that block unsafe final answers, and a real sqlite-backed
Sessiongiving automatic cross-run conversation memory behind aget/add/pop/clearprotocol. Guardrail functions and model policies are injected, so safety and memory are unit-tested deterministically offline."