OpenAI Agents SDK
Guardrail helpers using the Agents SDK's own input_guardrail/output_guardrail hooks.
The OpenAI Agents SDK ships
purpose-built input_guardrail/output_guardrail hooks: input guardrails
validate a message before it reaches the agent, output guardrails validate
the agent's response before it reaches the user, and a tripwire raises a
stoppable exception. tawaguard.agents_sdk wraps TawaGuard.analyze() as
guardrails shaped exactly like that, so the Agents SDK runs the check itself
— no manual block check before or after the agent call.
Install
pip install "tawaguard-sdk[agents]"openai-agents is an optional extra — not a dependency of the base
tawaguard-sdk package, so nothing pulls it in unless you import
tawaguard.agents_sdk.
Add the guardrails
from agents import Agent, InputGuardrailTripwireTriggered, Runner
from tawaguard import AsyncTawaGuard
from tawaguard.agents_sdk import tawa_input_guardrail, tawa_output_guardrail
tawa = await AsyncTawaGuard.create("YOUR_API_KEY")
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
input_guardrails=[tawa_input_guardrail(tawa)],
output_guardrails=[tawa_output_guardrail(tawa)],
)
try:
result = await Runner.run(agent, user_message)
except InputGuardrailTripwireTriggered as err:
scan = err.guardrail_result.output.output_info # the ScanResult that tripped it
print(f"Blocked ({scan.action}): {scan.rationale}")Both TawaGuard (sync) and AsyncTawaGuard are accepted — the sync client's
blocking analyze() call runs off-thread via asyncio.to_thread so it
can't stall the Agents SDK's event loop.
How tripping works
You don't raise anything yourself. The guardrail function returns a
GuardrailFunctionOutput with tripwire_triggered set from the scan
result, and the Agents SDK raises
InputGuardrailTripwireTriggered/OutputGuardrailTripwireTriggered itself
when that's True — halting the agent's execution immediately. The
ScanResult that triggered it is always attached at
err.guardrail_result.output.output_info, so you can inspect action,
rationale, confidence, etc. from the except block.
The default blocking policy (default_should_block): always block on
block/hard_block/soft_block, never on allow/ood, and defer to the
server's own suggestion for the genuinely undecided actions
(ambiguous/review/sandbox) — the same policy the
Vercel AI SDK integration uses.
The gateway already collapses review to allow or block per your app's
setting, so against a current gateway the scan's action is allow or
block and the default policy follows it directly. The undecided branch only
applies to a gateway that predates the review flag.
Checking output too
tawa_output_guardrail scans the agent's final output the same way,
raising OutputGuardrailTripwireTriggered instead:
except OutputGuardrailTripwireTriggered as err:
scan = err.guardrail_result.output.output_info
print(f"Output blocked ({scan.action}): {scan.rationale}")It accepts either plain text or a structured Pydantic output type — a
structured output is turned into text via model_dump_json() before being
scanned.
Options
Both tawa_input_guardrail() and tawa_output_guardrail() accept:
| Option | Type | Default | Description |
|---|---|---|---|
should_block | Callable[[ScanResult], bool] | default_should_block | Decide whether a scan result should trip the guardrail |
extract_text | Callable[..., str] | scans the whole input/output | Build the text handed to analyze() |
on_review | Callable[[ScanResult], None | Awaitable[None]] | None | Called once per scan whenever result.needs_review is True, whether or not the guardrail trips, before the tripwire decision. May be a plain or a coroutine function. Errors raised inside it go to on_error, or are dropped when on_error is not set. See The review flag |
on_error | Callable[[BaseException], None] | None | Receives an exception raised by on_review |
cuid / tenant_id / policy_overrides / language | — | None | Forwarded straight to analyze() per call |
name | str | None | "tawa_input_guardrail" / "tawa_output_guardrail" | Guardrail name, used for tracing |
tawa_input_guardrail additionally accepts run_in_parallel (default
True), matching the Agents SDK's own InputGuardrail option — whether the
guardrail runs concurrently with the agent or before it starts.
Empty input/output (e.g. a tool-only turn with no text content) skips the
scan entirely rather than sending an empty string — output_info is
None and the tripwire never fires in that case.
Policy Studio integration
default_should_block only looks at the scan's raw action/suggestion
— it never reads result.policy, the verdict from your org's
Policy Studio rules. That verdict is already
computed server-side on every scan; by default it's simply not checked.
There's no dedicated option for this yet (unlike the
Vercel AI SDK integration's
policy option) — should_block is already a plain callback, so pass your
own that also checks result.policy:
from tawaguard.agents_sdk import default_should_block, tawa_input_guardrail
def policy_aware_should_block(result):
if result.policy and "block" in result.policy.directives:
return True
return default_should_block(result)
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
input_guardrails=[
tawa_input_guardrail(tawa, should_block=policy_aware_should_block)
],
)Activating a template in the dashboard's Policy Studio only changes what
result.policy contains on future scans — it never blocks anything by
itself. See Enforcing a policy for why,
and the equivalent pattern for the raw SDK and the Vercel AI SDK.