OpenAI Agents
Protect OpenAI Agents SDK runs with OGuardAI so PII is tokenized before the agent and restored in the output
The OGuardAI Python SDK ships an optional OpenAI Agents SDK integration that wraps an agent run so PII
in the user input is tokenized before the agent runs and restored in the final output afterward. The
agent, its tools, and the Agents SDK tracer only ever see the tokenized safe_text; the sealed
session never enters the run. The integration is a thin seam over the SDK guarded_call primitive.
Install
The integration is an optional extra, so openai-agents is only pulled in when you use it. Importing
guardai_sdk never requires it: the framework is imported lazily, and only when you let the adapter
supply the default runner.
pip install "oguardai-sdk[openai-agents]"Two entry points
Both entry points give the same guarantee, and both are boundary-safe: the transform runs before the agent and the rehydrate runs after it, so neither the raw input nor the restored output is ever inside the agent run. Nothing an Agents SDK trace, span, tool call, or handoff observes is a raw value.
guarded_run(agent, client, user_input, ...)protects a single run. Use it for a one-shot call.GuardedRunner(agent, client, ...)binds an agent, a client, and the protection options once, so everyrunner.run(user_input)is protected the same way. Use it when you run the same agent repeatedly.
Unlike the LangChain integration, this adapter has no in-framework composable variant (there is no node you drop inside a larger agent graph whose own boundary a tracer could observe). Both entry points keep transform and rehydrate outside the run by construction, so there is no tracer caveat to weigh here. This matters because the Agents SDK emits tracing spans for every run by default: because the run only ever sees tokens, those spans never carry a raw or restored value.
Python
from agents import Agent
from guardai_sdk import OGuardAIClient
from guardai_sdk.integrations.openai_agents import GuardedRunner, guarded_run
client = OGuardAIClient(base_url="http://localhost:3000")
agent = Agent(
name="Account Support",
instructions=(
"You draft short, formal German support replies. Reuse every {{type:id:cap}} "
"token EXACTLY as given and never invent a name, email, or number."
),
)
# One-shot: transform the input, run the agent on tokens, rehydrate outside the run.
reply = guarded_run(
agent,
client,
"Kunde Jonas Weber, j.weber@example.de, bittet um eine Kopie seiner Rechnung.",
policy="german-support",
language="de",
)
# Repeated: bind agent + client + options once, protect every run.
support = GuardedRunner(agent, client, policy="german-support", language="de")
reply2 = support.run("Kundin Petra Lang bittet um Ruecksendung ihrer Kaution.")In both cases the agent sees only tokenized text such as
Kunde {{person:p_001:...}}, {{email:e_001:...}}, bittet um eine Kopie seiner Rechnung., and the
returned reply has the real values restored per the output channel.
Runner and output extraction
runner is any (agent, text) -> result callable. When you omit it, the adapter lazily imports the
Agents SDK and defaults to Runner.run_sync(agent, text), then reads the text from
result.final_output. Supply your own when you run the agent differently, and supply
output_extractor when the run result carries its text somewhere other than final_output. If the
result is neither a string nor an object with a string final_output, and no output_extractor is
given, the adapter raises TypeError rather than guessing.
from agents import Runner
reply = guarded_run(
agent,
client,
user_input,
runner=lambda ag, text: Runner.run_sync(ag, text),
output_extractor=lambda result: result.final_output,
output_channel="user_output",
restore_mode="full",
)Safe metadata
guarded_run passes only the tokenized safe_text to the agent. The safe metadata that the policy
permits out (gender, formality, language, carried in the transform response entity_context) is not
injected into the agent prompt by this adapter. The honorific is applied on the restore side instead:
under german-support the person token restores to Herr {name} or Frau {name} from the gender
sealed in the session. If your agent's own grammar has to agree with that gender, read
entity_context from a lower-level client.transform(...) call and pass the hints into the agent
instructions yourself.
What the agent and tracer see
The agent input is the tokenized safe_text; the agent output is a draft that still carries tokens.
The transform happens before runner is invoked and the rehydrate happens after it returns, both
outside the run, so:
- the model receives
Kunde {{person:p_001:...}}, {{email:e_001:...}}, ..., never the real name or email, - the Agents SDK trace records that same tokenized input and a token-bearing output, never a raw or restored value,
- any tool the agent calls receives tokens, because the tool arguments are built from the tokenized text the model saw.
Raw PII stays inside the OGuardAI runtime. The restored reply exists only after the run, in the value
guarded_run returns to your caller.
How it works
- The adapter calls
/v1/transformonuser_input, replacing PII with semantic tokens. - If the transform returns no session state, the agent is never run (fail closed).
runner(agent, safe_text)runs the agent on the tokenized text and its output is extracted.- The adapter calls
/v1/rehydrateon that output using the sealed session, restoring the real values per the output channel (user_outputby default) and restore mode (fullby default).
The fail-closed step is not incidental. A transform that returns no session_state cannot be
rehydrated, so the adapter refuses to run the agent at all rather than emit an unrestorable draft.