Guardrail Detection
SDKPassively detect, classify, and audit guardrails in your agent traces — zero runtime enforcement, no changes to existing code.
Traccia's guardrail detection engine runs as an OTel span processor. After every span ends, it inspects span attributes and classifies any guardrail signals into structured GuardrailFinding objects. When the root span ends, it evaluates which guardrail categories are missing given the agent's observed capabilities and writes a GuardrailSummary to the root span.
Zero config — automatic from init()
traccia.init(). You do not need to register any processor or change any existing code to get Tier B (provider-native) and Tier C (heuristic) signals. Tier A (explicit) requires annotation.Detection Tiers
Every GuardrailFinding has a source_type and confidence. The three tiers reflect how reliably the SDK can infer that a guardrail exists and whether it fired.
| Tier | source_type | confidence | How it fires |
|---|---|---|---|
| A — Explicit | explicit | high or medium | Developer annotates a function with @observe(as_type="guardrail") or wraps a check in guardrail_span(). |
| B — Provider-native | provider_native | high or medium | Deterministic structured fields: llm.finish_reason, llm.stop_reason, llm.safety_ratings. |
| C — Heuristic | heuristic | low | Pattern-matching on tool error messages (denial keywords). Always labeled as heuristic. Does not count as coverage for the missing-guardrail evaluator. |
Low confidence does not mean coverage
detected_categories but does not remove the category from missing_categories. Seeing a possible denial is not the same as having a guardrail. Both signals are preserved so you can investigate.Tier A — Explicit annotation
Explicit annotation gives you high confidence findings and is the only way to prove a specific guardrail exists in your agent. There are two approaches:
Option 1: guardrail_span() context manager
Recommended for inline checks. Pre-fills all canonical attributes; you only set guardrail.triggered.
from traccia.guardrails import guardrail_span
with guardrail_span( "pii_check", category="pii", # GuardrailCategory value enforcement_mode="warn", # "block", "warn", "log_only") as span: result = run_pii_scanner(user_input) span.set_attribute("guardrail.triggered", result.found_pii) # If triggered=True → finding status = "triggered" # If triggered=False → finding status = "not_triggered" # If not set → finding status = "present" (guardrail ran, result unknown)Option 2: @observe(as_type="guardrail") decorator
For function-level guardrails. If the function returns a bool, guardrail.triggered is set automatically — True means triggered/blocked, False means the guardrail ran and did not fire.
from traccia import observe
INJECTION_KEYWORDS = ["ignore previous", "ignore all", "jailbreak", "act as", "dan mode"]
@observe( as_type="guardrail", attributes={ "guardrail.name": "prompt_injection_check", "guardrail.category": "prompt_injection", "guardrail.enforcement_mode": "block", },)def check_injection(text: str) -> bool: """Returns True (triggered) if injection pattern detected.""" return any(kw in text.lower() for kw in INJECTION_KEYWORDS)
# In your agent:if check_injection(user_input): return {"error": "Request blocked: injection attempt detected."}Auto-triggered from bool return
guardrail.triggered in attributes={} takes precedence and the return value is ignored. For non-bool returns, set guardrail.triggered manually via get_current_span().Required attributes for high confidence
If any of the three required attributes are missing, confidence is downgraded to medium and a warning is logged to traccia.guardrails.
| Attribute | Required | Values |
|---|---|---|
| guardrail.name | required | Any human-readable string |
| guardrail.category | required | input_validation, prompt_injection, pii, moderation, tool_permission, output_validation, rate_limit, unknown |
| guardrail.triggered | required | True = guardrail fired, False = ran, did not fire |
| guardrail.enforcement_mode | optional | block, warn, log_only, unknown |
| guardrail.policy_id | optional | Policy version reference for audit trail |
Tier B — Provider-native detection (automatic)
No annotation needed. These signals are captured automatically by the LLM instrumentation patches and framework callbacks, then wired to the detector.
| Provider | Signal | Span attribute | Confidence |
|---|---|---|---|
| OpenAI | finish_reason = "content_filter" | llm.finish_reason | high |
| Azure OpenAI | finish_reason = "content_filtered" | llm.finish_reason | high |
| Google GenAI | finish_reason = "SAFETY" | llm.finish_reason | high |
| Anthropic | stop_reason = "content_filter" (requires llm.vendor anthropic or claude) | llm.stop_reason + llm.vendor | high |
| Anthropic | Policy phrases in error.message (vendor + LLM span signals required) | error.message + llm.vendor + llm.model/… | medium |
| Google / LangChain | blocked: true or probability: HIGH in safety ratings | llm.safety_ratings (JSON) | medium |
| Any provider | Response incomplete + refusal text in completion | llm.response.status + llm.completion | medium |
What you need to do
pip install traccia[langchain]) and call init(). The patches and callbacks write these attributes automatically.Tier C — Heuristic detection
Disabling Tier C via init()
Heuristics are on by default. Disable without touching processors:
from traccia import init
init(guardrail_heuristics=False)# Or: TRACCIA_GUARDRAIL_HEURISTICS=false# Or traccia.toml [instrumentation] guardrail_heuristics = falseTier A and Tier B are unchanged. If enrichment fails, a warning is logged and traces still export; set logger traccia.guardrails.processor to DEBUG for tracebacks.
When a tool or function span raises an error whose message contains denial-like keywords (permission, denied, unauthorized, forbidden, not allowed), a heuristic tool_permission finding is emitted with low confidence.
Known false-positive error types — excluded automatically
Even when a Tier C finding is emitted, it does not count as coverage. The tool_permission category will still appear in missing_categories. This is intentional: "we saw something that might be a guardrail" is not the same as "we have a guardrail."
Missing guardrail evaluator
When the root span ends, the evaluator inspects all spans in the trace, infers the agent's capabilities, and reports which guardrail categories are expected but not detected.
Capability inference rules
| Capability | Evidence in trace |
|---|---|
| calls_llm | Any span with llm.model |
| handles_user_text | Any span with llm.prompt (may be internal — see note below) |
| produces_user_text | Any span with llm.completion |
| uses_tools | Any span with span.type=tool or agent.tool.name |
| has_external_actions | Tool spans or handoff spans |
Required-guardrail matrix
| Observed capability | Expected guardrail categories | Missing confidence |
|---|---|---|
| calls_llm + handles_user_text | input_validation, prompt_injection | medium |
| produces_user_text | output_validation, moderation | medium |
| uses_tools | tool_permission | high |
Why medium for input_validation/prompt_injection? llm.prompt is present on any LLM call, including batch pipelines over internal documents. We cannot confirm from the trace alone that the prompt is user-provided. Use suppression (below) to opt out for internal agents.
Suppressing false positive missing warnings
If your agent is a batch pipeline or internal-only service where certain guardrail categories genuinely do not apply, set the suppression attribute on any span in the run:
from traccia.guardrails import guardrail_span, ATTR_GUARDRAIL_SUPPRESS_MISSINGfrom traccia import span
# Option 1: convenience parameter on guardrail_spanwith guardrail_span( "pipeline_root", category="unknown", suppress_missing=["prompt_injection", "input_validation"],): run_batch_pipeline(documents)
# Option 2: set directly on any spanwith span("pipeline_root") as s: s.set_attribute( ATTR_GUARDRAIL_SUPPRESS_MISSING, ["prompt_injection", "input_validation"], ) run_batch_pipeline(documents)What suppression does and does not do
missing_categories in the summary. It does not affect detected findings or triggered categories. If a finding was actually detected for a suppressed category, it still appears in the trace.Valid suppression values (GuardrailCategory strings):
input_validationprompt_injectionpiimoderationtool_permissionoutput_validationrate_limitunknownOutput — what appears in traces
All guardrail data is written as span attributes and flows through every configured exporter (OTLP, file, console). No separate API call or storage mechanism is used.
Per span (with guardrail signal)
guardrail.finding.count(int)— Number of findings on this spanguardrail.findings(JSON string)— Array of GuardrailFinding objectsRoot span (aggregated summary)
guardrail.summary(JSON string)— Full GuardrailSummaryguardrail.summary.coverage_confidence(string)— high / medium / lowguardrail.summary.missing_count(int)— Number of missing categoriesguardrail.summary.detected_categories(string)— Comma-separated categoriesguardrail.findings(JSON string)— All findings from the runguardrail.finding.count(int)— Total findings in the runExample GuardrailSummary (root span attribute)
{ "detected_categories": ["pii", "moderation"], "triggered_categories": ["pii"], "missing_categories": [ { "category": "input_validation", "why_required": "Agent makes LLM calls with prompt data (may be user-provided)", "missing_confidence": "medium", "evidence_ref": {"attribute_keys": ["calls_llm", "handles_user_text"]} }, { "category": "tool_permission", "why_required": "Agent uses tool/function calls", "missing_confidence": "high", "evidence_ref": {"attribute_keys": ["uses_tools"]} } ], "coverage_confidence": "high", "capabilities_observed": ["calls_llm", "handles_user_text", "produces_user_text", "uses_tools"], "limitations": [ "2 expected guardrail categories not detected in trace." ]}What coverage_confidence means
coverage_confidence measures signal quality, not protection breadth. An agent with a single explicit high-confidence guardrail and 3 missing categories will show high coverage confidence. It means what we detected, we detected reliably — but the missing categories indicate the agent may be under-protected.
| Value | Meaning |
|---|---|
| high | All detected findings are explicit or provider-native HIGH confidence. |
| medium | Mix of HIGH and MEDIUM confidence findings. |
| low | No findings detected, or all findings are heuristic-only (LOW). Out-of-band guardrails are not visible to tracing. |
Hard limits — what cannot be captured
These are fundamental limits of trace-based inference, not implementation gaps. They will not be closed with more heuristics.
Out-of-band guardrails are invisible
API gateways, proxy-level filters, and external validators that run outside the traced process cannot be detected unless they write span attributes into the trace.
Presence ≠ correctness
A guardrail can exist and be misconfigured. Traces show observed outcomes, not policy correctness. A pii_check that never triggers because it always returns False looks identical to a well-functioning one from the trace perspective.
Absence ≠ missing guardrail
guardrail.triggered = False means the guardrail ran and did not fire. No guardrail span at all means either no guardrail exists, or it exists but is out-of-band. The missing-guardrail evaluator always calls this out in limitations.
llm.prompt does not prove user input
Any LLM call sets llm.prompt — including batch pipelines over internal documents. handles_user_text is always MEDIUM confidence because of this. Use suppression to opt out for known internal-only agents.
Prompt injection cannot be inferred from output alone
You cannot tell from the model's response whether a prompt injection check ran. Only an explicit guardrail span proves the check was executed.
Heuristic keywords miss unusual phrasing
Tier C fires on specific denial keywords. A real tool denial with an unusual error message will be missed. An explicit guardrail_span wrapping the tool call is the only reliable alternative.
Complete example: multi-guardrail agent
import tracciafrom traccia import observe, spanfrom traccia.guardrails import guardrail_span, ATTR_GUARDRAIL_SUPPRESS_MISSINGfrom openai import OpenAI
traccia.init(agent_id="content-agent", agent_name="Content Agent", env="production")
INJECTION_KEYWORDS = ["ignore previous", "jailbreak", "act as", "dan mode"]
# --- Tier A: explicit guardrail via decorator (auto-triggered from bool return) ---
@observe( as_type="guardrail", attributes={ "guardrail.name": "prompt_injection_check", "guardrail.category": "prompt_injection", "guardrail.enforcement_mode": "block", },)def check_injection(text: str) -> bool: return any(kw in text.lower() for kw in INJECTION_KEYWORDS)
def run(user_input: str) -> str: with span("agent.run") as root: root.set_attribute("agent.id", "content-agent")
# Tier A: prompt injection check (bool return sets guardrail.triggered) if check_injection(user_input): return "Request blocked: injection attempt detected."
# Tier A: PII check via context manager with guardrail_span("pii_check", category="pii", enforcement_mode="warn") as gs: has_pii = scan_for_pii(user_input) gs.set_attribute("guardrail.triggered", has_pii) if has_pii: from traccia.processors.redaction_processor import redact_string user_input = redact_string(user_input)
# Tier A: input length rate limit with guardrail_span("input_length", category="rate_limit", enforcement_mode="block") as gs: too_long = len(user_input) > 4000 gs.set_attribute("guardrail.triggered", too_long) if too_long: return "Input too long. Please shorten your request."
# LLM call — Tier B signals (content_filter etc.) captured automatically client = OpenAI() response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": user_input}], ) return response.choices[0].message.content or ""
# The root span will contain:# guardrail.summary.detected_categories: ["prompt_injection", "pii", "rate_limit"]# guardrail.summary.triggered_categories: (whichever fired)# guardrail.summary.missing_categories: [output_validation, moderation] (if not added)# guardrail.summary.coverage_confidence: "high" (all explicit)OpenAI Agents SDK — automatic guardrail capture
Python Only
The OpenAI Agents SDK has a first-class concept of input/output guardrails. Traccia captures these automatically as Tier A explicit findings when the OpenAI Agents integration is active — no annotation needed.
from traccia import initfrom agents import Agent, Runner, input_guardrail, GuardrailFunctionOutput
init() # Automatically captures guardrail spans from the Agents SDK
@input_guardrailasync def safety_check(ctx, agent, input): # ... your check logic ... return GuardrailFunctionOutput(output_info=info, tripwire_triggered=is_unsafe)
agent = Agent( name="Safe Agent", instructions="...", input_guardrails=[safety_check],)
# Traccia captures: agent.span.type=guardrail, agent.guardrail.name, agent.guardrail.triggered# These become Tier A explicit findings automatically.In the Traccia dashboard
Trace and agent views surface this data as Guardrail Posture (coverage, findings, missing categories, and optional curation). See the platform guide: Guardrail Posture (platform).
© 2026 Traccia.