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 InputGuardrailTripwireTriggered before 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 raises OutputGuardrailTripwireTriggered so a bad answer never reaches the user. Each guardrail returns GuardrailFunctionOutput(output_info, tripwire_triggered).
  • Sessions — automatic conversation memory. A Session stores the conversation items so a second Runner.run on the same session automatically sees the prior turns — no manual history-threading. Build a list-backed InMemorySession and a real SQLiteSession on stdlib sqlite3.

What you build

PieceWhat it does
GuardrailFunctionOutput(output_info, tripwire_triggered) — the boolean decision + logging info
InputGuardrail / OutputGuardrail + @input_guardrail / @output_guardrailwrap a (agent, data) -> GuardrailFunctionOutput function
*TripwireTriggered exceptionscarry the guardrail_result so callers can log the reason
InMemorySession, SQLiteSessionget_items / add_items / pop_item / clear_session
Runner.run_sync(agent, input, session=None)input guardrails → loop → output guardrails → persist to session

File map

FileRole
lab.pyyour implementation (fill the # TODOs)
solution.pyreference + a worked example (python solution.py): a homework-blocking input guardrail, a PII output guardrail, and a session that remembers across runs
test_lab.py22 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.txtpytest 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 InputGuardrailTripwireTriggered and the model is never called (assert the policy didn't run).
  • A clean output passes; a tripped output guardrail raises OutputGuardrailTripwireTriggered; the exception carries the guardrail_result.
  • With multiple input guardrails, the first tripwire halts the run.
  • A fresh session is empty; add_items/get_items/pop_item/clear_session behave; SQLiteSession persists to real sqlite and two session ids sharing one file are isolated.
  • A second Runner.run on 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 lab and solution.

How this maps to the real stack

  • Guardrails are real. Agent(input_guardrails=[...], output_guardrails=[...]), the @input_guardrail / @output_guardrail decorators, GuardrailFunctionOutput(output_info, tripwire_triggered), and the InputGuardrailTripwireTriggered / OutputGuardrailTripwireTriggered exceptions 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; pass session= to Runner.run and history is prepended and results appended automatically. The SDK also ships OpenAIConversationsSession and supports custom backends (Redis, Postgres) behind the same get_items / add_items / pop_item / clear_session protocol 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 as output_info — the real "fast model guards the smart model" pattern.
  • Add a max_items / token-budget pruning policy to SQLiteSession.get_items (keep the last N) and observe how it bounds context growth (Phase 04's problem, at the session layer).
  • Swap SQLiteSession for 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 Session giving automatic cross-run conversation memory behind a get/add/pop/clear protocol. Guardrail functions and model policies are injected, so safety and memory are unit-tested deterministically offline."