AI
Guardrails OpenAI Agents SDK: Cek Input, Output dan Tool
Oktober 202611 menit baca

Guardrails adalah cek yang dijalankan SDK pada input agent, output akhirnya, atau panggilan function tool tertentu. Setiap guardrail mengembalikan putusan, dan jika tripwire-nya aktif, runner memunculkan exception dan menghentikan run. Input dan output guardrail dipasang di agent, sedangkan tool guardrail dipasang di tool tertentu.
Ya, secara default. Dengan run_in_parallel=True, guardrail dan agent mulai bersamaan sehingga latency tetap rendah, tetapi agent bisa sudah memakai token dan memanggil tool sebelum tripwire membatalkannya. Dengan run_in_parallel=False, guardrail selesai lebih dulu, sehingga permintaan yang diblokir tidak pernah sampai ke model atau tool.
Input guardrail hanya berjalan untuk agent pertama dalam run, dan output guardrail hanya untuk agent yang menghasilkan output akhir. Spesialis yang menerima handoff bukan agent pertama, jadi input guardrail-nya dilewati. Untuk cek yang harus berlaku siapa pun agent yang bertindak, pasang tool guardrail di function tool itu sendiri.
reject_content melewatkan panggilan tool, atau mengganti output-nya, dengan pesan yang dibaca model sebagai gantinya, sehingga run tetap berjalan dan agent bisa memperbaiki diri. raise_exception menghentikan seluruh run dengan ToolInputGuardrailTripwireTriggered atau ToolOutputGuardrailTripwireTriggered. Gunakan yang pertama untuk kesalahan yang bisa dipulihkan dan yang kedua untuk kondisi yang tidak boleh terjadi.
Tidak. Tool guardrail memakai pipeline function tool, jadi tool hosted seperti WebSearchTool, FileSearchTool dan HostedMCPTool, serta tool built-in seperti ComputerTool dan ShellTool, tidak melewatinya. Panggilan handoff dan Agent.as_tool() juga tidak punya opsi tool guardrail. Server MCP lokal bisa memasang guardrail di setiap tool yang diekspos.

Ringkasan Utama
Guardrails di OpenAI Agents SDK ada tiga lapis: input guardrail hanya berjalan di agent pertama, output guardrail hanya di agent terakhir, dan tool guardrail di setiap panggilan function tool yang dijaga. Cek input berjalan paralel secara default, jadi agent bisa sudah memakai token dan memanggil tool sebelum tripwire aktif. Gunakan mode blocking atau tool guardrail untuk aksi yang punya side effect.
Versi pertama asisten keuangan yang saya rancang untuk back-office ERP hanya punya satu guardrail: cek input yang menolak semua permintaan di luar urusan buku besar. Kelihatannya sudah lengkap, sampai saya membaca bagian execution mode di dokumentasinya. Secara default cek itu berjalan bersamaan dengan agent, dan dokumentasinya menulis terang-terangan bahwa saat tripwire aktif, agent mungkin sudah memakai token dan menjalankan tool. Untuk agent yang tool utamanya memposting jurnal, kalimat sudah menjalankan tool itulah inti masalahnya.
Tulisan ini membahas tiga lapis guardrail di OpenAI Agents SDK sesuai dokumentasi untuk openai-agents 0.22.3, rilis PyPI tanggal 17 September 2026: input guardrail, output guardrail, dan tool guardrail per panggilan. Masing-masing diterapkan ke risiko nyata di workflow akuntansi, yaitu permintaan di luar topik, data pribadi yang bocor lewat jawaban, dan panggilan tool yang akan memposting ke cabang yang salah, lengkap dengan exception yang perlu Anda tangkap. SDK TypeScript mendokumentasikan tiga lapis yang sama dengan nama camelCase, seperti runInParallel dan toolExecution, jadi desainnya bisa dipakai apa adanya.
Fakta paling berguna soal guardrail adalah di mana ia berjalan, karena tempatnya tidak seperti yang biasanya diasumsikan. Guardrail dikonfigurasi di agent atau di tool, tetapi runner hanya menjalankannya di batas workflow tertentu.
| Lapis | Kapan berjalan | Apa yang dilihat dan bisa dilakukan | Exception saat tripwire |
|---|---|---|---|
| Input guardrail, di Agent.input_guardrails | Hanya jika agent itu adalah agent pertama dalam run. Paralel dengan agent secara default, atau sebelum agent dengan run_in_parallel=False | Input yang sama dengan yang diterima agent. Mengembalikan GuardrailFunctionOutput berisi tripwire_triggered dan output_info | InputGuardrailTripwireTriggered |
| Output guardrail, di Agent.output_guardrails | Hanya di agent yang menghasilkan output akhir, selalu setelah agent selesai. Tidak ada opsi paralel | Output akhir, bertipe sesuai output_type agent. Bentuk GuardrailFunctionOutput yang sama | OutputGuardrailTripwireTriggered |
| Tool guardrail, di sebuah FunctionTool | Di setiap pemanggilan tool tersebut, cek input sebelum eksekusi dan cek output sesudahnya, siapa pun agent yang memanggil | Nama tool dan string argumen mentah, atau hasil tool. Mengembalikan allow, reject_content atau raise_exception | ToolInputGuardrailTripwireTriggered atau ToolOutputGuardrailTripwireTriggered |
Baca kolom kedua dua kali. Dalam setup triage, saat agent front-desk melakukan handoff ke spesialis keuangan, input guardrail milik spesialis tidak pernah berjalan karena ia bukan agent pertama, dan output guardrail milik agent triage juga tidak pernah berjalan karena ia tidak menghasilkan jawaban akhir. Dokumentasinya menyatakan langsung: jika Anda butuh cek di sekitar setiap panggilan function tool dalam workflow yang memakai manager, handoff atau spesialis yang didelegasikan, gunakan tool guardrail, jangan hanya mengandalkan guardrail level agent.
Input guardrail adalah fungsi apa pun yang menerima run context, agent dan input, lalu mengembalikan GuardrailFunctionOutput. Pola yang umum adalah agent classifier kecil dengan output_type bertipe, dijalankan dari dalam guardrail. Satu-satunya keputusan yang benar-benar penting adalah execution mode, yang Anda atur di decorator.
from pydantic import BaseModel
from agents import (
Agent,
GuardrailFunctionOutput,
RunContextWrapper,
Runner,
TResponseInputItem,
)
from agents.decorators import input_guardrail
class ScopeCheck(BaseModel):
in_scope: bool
reason: str
# A small, cheap agent whose only job is a yes/no verdict.
# Point it at the cheapest model you trust for classification.
scope_checker = Agent(
name="Finance scope check",
instructions=(
"Answer in_scope=true only if the request is about this company's "
"ledger, journals, invoices or branch accounts. Anything else is false."
),
output_type=ScopeCheck,
)
# Blocking: the finance agent does not start until this returns.
# Costs one extra round-trip of latency; buys zero tokens and zero
# tool calls on a request that was never going to be served.
@input_guardrail(name="finance_scope", run_in_parallel=False)
async def finance_scope(
ctx: RunContextWrapper, agent: Agent, input: str | list[TResponseInputItem]
) -> GuardrailFunctionOutput:
verdict = await Runner.run(scope_checker, input, context=ctx.context)
return GuardrailFunctionOutput(
output_info=verdict.final_output, # kept for logging
tripwire_triggered=not verdict.final_output.in_scope,
)Mode paralel, yang menjadi default, menjalankan guardrail dan agent bersamaan, sehingga permintaan yang bersih tidak menambah latency. Mode blocking membuat agent menunggu putusan guardrail, yang berarti satu round-trip classifier di setiap permintaan, tetapi saat tripwire aktif agent tidak pernah berjalan: tidak ada token di model mahal dan tidak ada panggilan tool. Saya memilih blocking untuk agent mana pun yang memegang tool dengan side effect, dan tetap paralel untuk asisten read-only, karena run yang dibatalkan di sana hanya membuang sedikit token.
Mode paralel adalah optimasi biaya, bukan batas keamanan. SDK mendokumentasikan bahwa saat input guardrail paralel aktif, agent mungkin sudah memakai token dan menjalankan tool sebelum dibatalkan. Jika turn pertama bisa memanggil tool yang menulis ke buku besar, mengirim email atau memanggil API pembayaran, pasang run_in_parallel=False atau taruh cek yang sebenarnya di tool itu sendiri.
Output guardrail menerima output akhir setelah agent terakhir selesai, karena itu tidak ada mode paralel yang perlu dipilih. Cek berbasis model juga bisa dipakai di sini, tetapi untuk data pribadi, pola deterministik lebih cepat dan lebih mudah dinalar. Contoh di bawah aktif jika jawaban mengandung NIK 16 digit atau alamat email.
import re
from agents import Agent, GuardrailFunctionOutput, RunContextWrapper
from agents.decorators import output_guardrail
# NIK (Indonesian national ID) is 16 digits. NPWP and bank account numbers
# deserve their own patterns; start with the ones your ledger actually stores.
NIK = re.compile(r"\b\d{16}\b")
EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")
@output_guardrail(name="no_pii_in_reply")
async def no_pii_in_reply(
ctx: RunContextWrapper, agent: Agent, output: str
) -> GuardrailFunctionOutput:
hits = {
"nik": len(NIK.findall(output)),
"email": len(EMAIL.findall(output)),
}
return GuardrailFunctionOutput(
# Log counts, never the matched values: output_info ends up in logs.
output_info=hits,
tripwire_triggered=any(hits.values()),
)
finance_agent = Agent(
name="Finance assistant",
instructions="Answer questions about journals and branch balances.",
input_guardrails=[finance_scope],
output_guardrails=[no_pii_in_reply],
)Ada dua perilaku dari dokumentasi yang menentukan cara memakainya. Pertama, tripwire dan crash diperlakukan berbeda: saat tripwire aktif, session menyimpan panggilan tool dan output yang sudah selesai tetapi membuang jawaban akhir yang ditolak, sedangkan exception yang muncul di dalam guardrail dianggap putusan yang tidak diketahui, dan item turn terakhir disimpan dulu sebelum error dimunculkan. Jadi kembalikan putusan, jangan raise. Kedua, jika tool_use_behavior menjadikan hasil sebuah tool sebagai output akhir lalu guardrail menolaknya, SDK mengganti payload yang tersimpan dengan placeholder Output withheld by an output guardrail. Teks itu bisa diganti lewat RunConfig.output_guardrail_blocked_message, tetapi jangan isi dengan data, karena teks itu disimpan dan diputar ulang.
Tool guardrail adalah lapis yang memang dibuat untuk kasus ERP. Ia menempel di tool, jadi berjalan di setiap panggilan siapa pun agent yang memanggil, dan ia melihat argumen yang benar-benar dihasilkan model. Input guardrail di bawah membaca string tool_arguments mentah serta context object milik pemanggil, lalu memilih satu dari tiga hasil.
import json
from dataclasses import dataclass
from pydantic import BaseModel
from agents import ToolGuardrailFunctionOutput, function_tool
from agents.decorators import tool_input_guardrail
@dataclass
class ErpUser: # passed as Runner.run(..., context=ErpUser(...))
user_id: str
allowed_branches: set[str]
class JournalLine(BaseModel):
account: str
debit: float
credit: float
@tool_input_guardrail
def branch_and_balance(data):
# data.context is a ToolContext: it carries tool_name and the RAW
# tool_arguments string, and .context is your own ErpUser object.
args = json.loads(data.context.tool_arguments or "{}")
user: ErpUser = data.context.context
branch = args.get("branch_code")
if branch not in user.allowed_branches:
# Recoverable: the call is skipped and the model reads this text
# instead of a tool result, so it can ask the user which branch.
return ToolGuardrailFunctionOutput.reject_content(
f"Branch {branch} is not one this user may post to. "
f"Allowed: {', '.join(sorted(user.allowed_branches))}."
)
lines = args.get("lines", [])
debit = round(sum(l["debit"] for l in lines), 2)
credit = round(sum(l["credit"] for l in lines), 2)
if debit != credit:
# Not recoverable by rephrasing: an unbalanced entry means the model
# has lost the plot. Halt the run with ToolInputGuardrailTripwireTriggered.
return ToolGuardrailFunctionOutput.raise_exception(
output_info={"debit": debit, "credit": credit}
)
return ToolGuardrailFunctionOutput.allow()
@function_tool(tool_input_guardrails=[branch_and_balance])
def post_journal_entry(branch_code: str, memo: str, lines: list[JournalLine]) -> str:
"""Post a balanced journal entry to the given branch ledger."""
... # call the ERP here; the guardrail has already runDua jalur penolakan itu punya tugas berbeda. reject_content melewatkan panggilan dan memberi model sebuah pesan sebagai pengganti hasil tool, sehingga run tetap berjalan dan agent bisa menanyakan cabang mana yang dimaksud pengguna. raise_exception menghentikan seluruh run dengan ToolInputGuardrailTripwireTriggered, dan ini tepat untuk kondisi yang seharusnya tidak pernah dicapai model, misalnya debit yang tidak sama dengan kredit. Perhatikan bahwa daftar cabang diambil dari context object Anda, bukan dari prompt, sehingga model tidak bisa membujuk dirinya masuk ke cabang lain.
Jika tool juga memakai needs_approval, input tool guardrail biasanya berjalan setelah manusia menyetujui dan tepat sebelum eksekusi. Atur RunConfig.tool_execution ke ToolExecutionConfig dengan pre_approval_tool_input_guardrails=True, maka cek yang sama juga berjalan sebelum permintaan approval dimunculkan, sehingga tidak ada orang yang diminta menyetujui jurnal yang toh akan ditolak. Cek itu tetap berjalan lagi setelah approval.
Celah-celahnya terdokumentasi, dan masing-masing adalah tempat agent bisa bertindak tanpa cek yang Anda kira sudah terpasang di depannya.
Aturan praktis yang saya ambil dari daftar ini: apa pun yang menulis data harus berupa function tool milik Anda sendiri, dengan tool guardrail di atasnya. Jika penulisan lewat tool hosted atau server MCP remote, guardrail-nya harus ada di sisi server, karena SDK tidak bisa melihat isi panggilan tersebut.
Setiap lapis memunculkan exception sendiri, dan datanya disimpan di tempat berbeda. Tripwire level agent menyediakan guardrail_result, yang menyebut nama guardrail dan menyimpan outputnya. Tripwire tool menyediakan guardrail dan ToolGuardrailFunctionOutput-nya langsung di exception. Keempatnya bisa di-import dari package agents.
from agents import (
InputGuardrailTripwireTriggered,
OutputGuardrailTripwireTriggered,
Runner,
ToolInputGuardrailTripwireTriggered,
ToolOutputGuardrailTripwireTriggered,
)
async def ask_finance(message: str, user: ErpUser) -> dict:
try:
result = await Runner.run(finance_agent, message, context=user)
return {"status": 200, "reply": result.final_output}
except InputGuardrailTripwireTriggered as exc:
# Agent-level tripwires carry guardrail_result; it names the guardrail.
name = exc.guardrail_result.guardrail.get_name()
log.info("input tripwire", guardrail=name, user=user.user_id)
return {"status": 422, "reply": "I can only help with ledger questions."}
except OutputGuardrailTripwireTriggered:
# The draft answer was rejected. Do not retry blindly with the same
# prompt; it will usually produce the same identifiers again.
return {"status": 502, "reply": "The answer contained personal data and was withheld."}
except ToolInputGuardrailTripwireTriggered as exc:
# Tool tripwires expose the verdict directly on exc.output.
log.warning("journal blocked", info=exc.output.output_info, user=user.user_id)
return {"status": 409, "reply": "That entry does not balance, so nothing was posted."}
except ToolOutputGuardrailTripwireTriggered as exc:
log.warning("tool output blocked", info=exc.output.output_info)
return {"status": 502, "reply": "A lookup returned data I am not allowed to show."}Petakan masing-masing ke respons yang berbeda, karena artinya berbeda bagi pemanggil: permintaan di luar cakupan adalah error dari sisi klien, jawaban yang ditahan adalah penolakan dari sisi server, dan jurnal yang diblokir adalah konflik yang tidak menulis apa pun. Dokumentasi juga mencatat bahwa exception.run_data menyimpan hasil guardrail yang terkumpul sebelum run berhenti, termasuk hasil tool guardrail dari turn yang sudah selesai, dan itulah yang tepat untuk dikirim ke audit log. run_data bisa bernilai None jika exception muncul di luar jalur yang dikelola runner, jadi periksa dulu sebelum membacanya.
Panduan OpenAI sendiri memisahkan keduanya dengan jelas: guardrail memvalidasi input, output atau perilaku tool secara otomatis, sedangkan review manusia menjeda run agar seseorang atau sebuah policy bisa menyetujui aksi yang sensitif. Dalam praktik, saya menyusunnya dengan urutan berikut untuk agent apa pun yang menyentuh uang.
Tidak satu pun dari ini menggantikan permission di ERP itu sendiri. Guardrail adalah cek di dalam proses Anda; role dan izin cabang milik buku besar adalah batas yang tetap bertahan ketika agent, SDK, atau kode guardrail Anda punya bug.
Aturan yang saya bawa dari sini: pasang cek di batas tempat aksi terjadi, bukan di tempat percakapan dimulai. Input guardrail menjaga biaya dan cakupan, output guardrail melindungi apa yang keluar, tetapi hanya tool guardrail yang dijamin berdiri di depan setiap penulisan data, siapa pun agent yang melakukannya.