Every tool in an OpenAI Agents SDK agent is a FunctionTool — there's no separate "graph node" or "gate" concept to reason about, unlike some other agent frameworks. That makes the integration surface simple: one wrapper, applied once per tool. The interesting part is how that wrapper works, and why it doesn't reuse the SDK's own guardrail primitive.
Why you need this
FunctionTools can do real things — write to a database, call an internal API, send a message. The model decides which tool to call and with what arguments; nothing stops it from calling a tool your policy would reject unless something checks before the tool actually runs.
Prerequisites
pip install agf-sdk[openai-agents]
export AGF_API_KEY=agfk_your_key_here
Guard a function tool
guard_function_tool wraps an already-built FunctionTool and returns a new one — the original is never mutated. Swap the guarded version into your agent in place of the original:
import os
from agents import Agent, function_tool
from agf.client import AGFClient
from agf.keys import build_self_signed_chain
from agf.openai_agents import guard_function_tool
client = AGFClient(api_key=os.environ["AGF_API_KEY"])
AGF_AGENT_ID = "did:agf:agt_01abc"
def _refund_chain_provider(ctx):
# Built fresh per call — a chain built once expires in 5 minutes.
return build_self_signed_chain(AGF_PRIVATE_KEY_PEM, AGF_AGENT_ID, "tool:issue_refund", "agf")
@function_tool
def issue_refund(order_id: str, amount: str) -> str:
return process_refund(order_id, amount)
guarded_issue_refund = guard_function_tool(
issue_refund,
client,
agent_id=AGF_AGENT_ID,
action_type="tool:issue_refund",
chain_provider=_refund_chain_provider,
validate_execution=True,
report_outcome=True,
)
agent = Agent(name="support-agent", tools=[guarded_issue_refund])
Why not the SDK's own guardrail hook?
The OpenAI Agents SDK already ships ToolInputGuardrail — a native tool-level hook attached via function_tool(..., tool_input_guardrails=[...]). We looked at building on it and decided against it, for two concrete reasons:
- Its
raise_exception()behavior collapses every failure — DENY, REVIEW_REQUIRED, anything — into one genericToolInputGuardrailTripwireTriggeredexception. The real reason lives in a nested.output.output_infofield, not as a distinguishable exception type your code canexcepton. - There's no output-side hook that sees both success and exception for the same call. Reporting an execution outcome (did the tool actually run, or not) needs exactly that.
guard_function_tool wraps on_invoke_tool directly instead — the same "wrap the tool object" pattern the LangChain and CrewAI adapters already use. That preserves AGFDeniedError and AGFReviewRequiredError as their real, specific types, and lets a single try/except around the real call drive accurate outcome reporting.
Accurate outcome reporting on tool errors
One real caveat, worth knowing before you turn report_outcome on: @function_tool's default error handling catches a tool's internal exception and turns it into a string result the model sees — before report_outcome ever gets a chance to see it. This is a real, verified behavior of the underlying SDK, not an AGF limitation.
If accurate "not executed" reporting on internal tool failures matters for your integration, build the tool with that catch disabled:
@function_tool(failure_error_function=None)
def issue_refund(order_id: str, amount: str) -> str:
return process_refund(order_id, amount)
Without it, a tool that raises internally still gets reported as "executed" — which is accurate to what actually happened at the guard boundary (the tool did run; it just failed inside), just worth knowing isn't the same as "the failure was caught and reported."
Next steps
- OpenAI Agents SDK integration reference →
- LangGraph integration — guard_node for graph-based agents →
- Full Python SDK reference →
The AGF Python SDK is open source. GitHub →

