Augments LabsAugments ADK

๐Ÿ›ก๏ธ Guardrails

Guardrails are the input and output safety gates on either side of the agent loop. They intercept every run at Stage 2 (before the LLM sees the prompt) and again at Stage 4 (after the loop produces its final reply), giving you deterministic, cost-free checkpoints for content policy, length limits, and any custom logic your application needs.

[!IMPORTANT] Core contract

A guardrail is a sync or async pure function. It takes a data object (prompt or output), inspects it, and returns a verdict. It never calls an LLM, never has side-effects on run state, and never fails silently.

Where guardrails sit in the pipeline

Every call to Runner.arun(agent, prompt) follows the same shape:

Input โ†’ [Stage 2: Input guardrails] โ†’ Agent loop โ†’ [Stage 4: Output guardrails] โ†’ Result

Stage 2 โ€” Input guardrails run before the loop opens. A tripwire stops the run immediately; the LLM never receives the prompt and you pay zero tokens.

Stage 4 โ€” Output guardrails run once the loop has produced its final reply, before RunResult is returned to the caller. Output guardrails support an optional remediation string: when set, the runner re-prompts the agent with that feedback instead of raising on the first failure, giving the model one chance to self-correct.

Guardrail results from both stages are stored in RunResult.guardrail_results (an AgentGuardrailResults object with .input and .output tuples) and are always present, whether or not any tripwire fired.

Verdict shape

Agent-level guardrail functions have the signature:

async def my_guardrail(
    data: AgentInputGuardrailData,           # or AgentOutputGuardrailData
) -> AgentGuardrailFunctionOutput:
    ...

AgentGuardrailFunctionOutput carries these fields:

FieldTypeMeaning
tripwire_triggeredboolTrue halts execution immediately
severityAgentGuardrailSeverity | NoneWhen set, overrides tripwire_triggered for the halt decision
output_infoAnyOptional metadata (entities found, pattern matched, โ€ฆ)
transformed_outputAnyComplete replacement text for the agent output (output guardrails, str only). When set, the runner substitutes it for RunResult.final_output and rewrites the trailing history message. Must also set tripwire_triggered=True.
changed_spanslist[GuardrailSpan] | NoneCharacter ranges the guardrail flagged โ€” for observability and audit only; never used to apply the transform.

Every verdict maps onto a shared GuardrailAction (PASS / RAISE / TRANSFORM) via resolved_action(). The runner dispatches uniformly on this vocabulary across agent, tool, and flow guardrail levels.

Severity levels

When severity is not None it overrides tripwire_triggered:

SeverityHalts?Use case
INFONoLow-confidence detection; telemetry only
WARNINGNoAuditable signal that should not block
ERRORYesSame effect as tripwire_triggered=True

When severity is None (the default), tripwire_triggered alone controls whether execution halts.

A tripped input guardrail raises AgentInputGuardrailTripwireTriggered; a tripped output guardrail raises AgentOutputGuardrailTripwireTriggered. Both are importable from augments.adk.exceptions.

Built-in guardrail hub

For the most common safety concerns the ADK ships ready-to-use guardrails under augments.adk.guardrails:

FactoryLevelDefault on_failDescription
pii_guardrail()outputRAISEDetects email, URL, and phone patterns; TRANSFORM available for automatic span-redaction.
injection_scan_guardrail()inputRAISEDetects high-confidence prompt-injection markers.
fence_untrusted_text(text)prompt helpern/aWraps untrusted text in a nonce-fenced delimiter for safe prompt construction.
wrong_language_guardrail(target_language=โ€ฆ)outputRAISETrips when the output is in the wrong language. Needs [guardrails-lingua] extra.
from augments.adk.guardrails import injection_scan_guardrail, pii_guardrail
from augments.adk.types.guardrails.action import GuardrailAction
 
agent = Agent(
    name="Support",
    system_prompt="Help customers.",
    guardrails=AgentGuardrails(
        input=[injection_scan_guardrail()],
        output=[pii_guardrail(on_fail=GuardrailAction.TRANSFORM)],
    ),
)

See Guardrail hub for the full reference including PatternScanner, mask_pii_spans, detect_wrong_language, and the guardrail audit side-car (RunResult.guardrail_audit).

Writing a guardrail

All guardrails are user-authored Python functions. The ADK provides hooks to attach them and a built-in hub for common checks. For application-specific logic, write a function as described below.

The @agent_input_guardrail and @agent_output_guardrail decorators are the idiomatic approach. They accept both sync and async functions.

from augments.adk.agents.agent_guardrails import (
    agent_input_guardrail,
    AgentInputGuardrailData,
    AgentGuardrailFunctionOutput,
)
 
# Async (most common)
@agent_input_guardrail
async def no_sql_injection(
    data: AgentInputGuardrailData,
) -> AgentGuardrailFunctionOutput:
    text = data.user_prompt if isinstance(data.user_prompt, str) else str(data.user_prompt)
    triggered = any(kw in text.upper() for kw in ("DROP TABLE", "OR 1=1", "UNION SELECT"))
    return AgentGuardrailFunctionOutput(
        tripwire_triggered=triggered,
        output_info={"reason": "SQL injection pattern detected"} if triggered else None,
    )
 
# Sync (also supported โ€” the runner awaits if needed)
@agent_input_guardrail
def block_empty_input(
    data: AgentInputGuardrailData,
) -> AgentGuardrailFunctionOutput:
    text = data.user_prompt if isinstance(data.user_prompt, str) else str(data.user_prompt)
    return AgentGuardrailFunctionOutput(
        tripwire_triggered=len(text.strip()) == 0,
        output_info={"reason": "Empty input rejected"},
    )

Use keyword arguments to set name, run_in_parallel, timeout, and timeout_policy:

@agent_input_guardrail(
    name="sql_injection_guard",
    run_in_parallel=False,    # Block before the LLM call
    timeout=2.0,
    timeout_policy=AgentTimeoutPolicy.FAIL,
)
async def guarded_sql_check(
    data: AgentInputGuardrailData,
) -> AgentGuardrailFunctionOutput:
    ...

Output guardrails follow the same pattern with @agent_output_guardrail, which additionally supports remediation and max_retries:

from augments.adk.agents.agent_guardrails import (
    agent_output_guardrail,
    AgentOutputGuardrailData,
    AgentGuardrailFunctionOutput,
)
 
@agent_output_guardrail(
    remediation="Your reply contained a code block with credentials. "
                "Please regenerate without including sensitive tokens.",
    max_retries=2,
)
async def no_credentials_in_output(
    data: AgentOutputGuardrailData,
) -> AgentGuardrailFunctionOutput:
    text = str(data.output)
    triggered = any(
        marker in text.lower() for marker in ("api_key=", "password=", "secret=")
    )
    return AgentGuardrailFunctionOutput(
        tripwire_triggered=triggered,
        output_info={"reason": "Credential pattern in output"},
    )

Using the dataclass directly

When you need to share a guardrail instance across agents or configure it dynamically, construct AgentInputGuardrail or AgentOutputGuardrail directly:

from augments.adk.agents.agent_guardrails import (
    AgentInputGuardrail,
    AgentInputGuardrailData,
    AgentGuardrailFunctionOutput,
)
 
async def _check_length(data: AgentInputGuardrailData) -> AgentGuardrailFunctionOutput:
    text = data.user_prompt if isinstance(data.user_prompt, str) else str(data.user_prompt)
    return AgentGuardrailFunctionOutput(
        tripwire_triggered=len(text) > 8000,
        output_info={"length": len(text)},
    )
 
length_guard = AgentInputGuardrail(
    guardrail_function=_check_length,
    name="max_prompt_length",
    run_in_parallel=False,
)

Registering guardrails on an agent

Pass guardrails as an AgentGuardrails config on the agent:

from augments.adk import Agent
from augments.adk.agents.agent_guardrails import AgentGuardrails
 
agent = Agent(
    name="support-bot",
    system_prompt="You are a customer support assistant.",
    guardrails=AgentGuardrails(
        input=[
            length_guard,
            no_sql_injection,            # decorator-based guardrail
        ],
        output=[
            no_credentials_in_output,    # custom output guardrail
        ],
    ),
)

Run-scope guardrails

RunConfig.guardrails applies a second layer of guardrails that run before the agent's own guardrails, across every agent in the run. This is useful for organisation-wide policies applied uniformly regardless of which agent handles the request:

from augments.adk.run import RunConfig
from augments.adk.agents.agent_guardrails import AgentGuardrails
 
result = await Runner.arun(
    agent,
    prompt,
    run_config=RunConfig(
        guardrails=AgentGuardrails(
            input=[organisation_content_guard],
        )
    ),
)

Referencing guardrails from declarative config

In a JSON or YAML agent config, attach guardrail functions via a dotted ref pointing to an AgentInputGuardrail or AgentOutputGuardrail instance in an importable module:

"guardrails": {
  "input": [
    {"ref": "my_pkg.guards.no_sql_injection"},
    {"ref": "my_pkg.guards.length_guard"}
  ],
  "output": [
    {"ref": "my_pkg.guards.no_credentials_in_output"}
  ]
}

The referenced symbols must be AgentInputGuardrail (or AgentOutputGuardrail) instances at module scope โ€” decorator-created or dataclass-constructed. See Declarative config for the full schema.

Tool-level vs agent-level guardrails

Agent-level guardrails (described above) operate on the whole-run boundary. Tool-level guardrails operate on each individual tool invocation and have a richer three-state verdict.

Agent guardrailsTool guardrails
ScopeWhole run (input prompt / final output)Single tool call (arguments / return value)
ClassesAgentInputGuardrail, AgentOutputGuardrailToolInputGuardrail, ToolOutputGuardrail
Decorators@agent_input_guardrail, @agent_output_guardrail@tool_input_guardrail, @tool_output_guardrail
Verdict typeAgentGuardrailFunctionOutputToolGuardrailFunctionOutput
Halt signaltripwire_triggered=True or severity=ERROR.raise_exception()
Soft rejectseverity=WARNING (record, no halt).reject_content(message) โ€” LLM sees the message, loop continues
Passtripwire_triggered=False.allow()
When to usePrompt-level policy (content in user input, compliance on final reply)Call-level policy (block a specific argument pattern, redact a field from tool output)

Tool guardrail example:

from augments.adk.tools.tool_guardrails import (
    tool_input_guardrail,
    ToolInputGuardrailData,
    ToolGuardrailFunctionOutput,
)
 
@tool_input_guardrail
async def reject_internal_paths(
    data: ToolInputGuardrailData,
) -> ToolGuardrailFunctionOutput:
    args = data.context.tool_args or {}
    path = args.get("path", "")
    if isinstance(path, str) and path.startswith("/etc"):
        return ToolGuardrailFunctionOutput.reject_content(
            message="Access to /etc is not permitted.",
            output_info={"path": path},
        )
    return ToolGuardrailFunctionOutput.allow()

Register tool guardrails directly on a FunctionTool via input_guardrails / output_guardrails, or pass them inside a ToolGuardrails config to FunctionTool.

Guardrails are pure โ€” no LLM calls

[!WARNING] Guardrails must not call an LLM

Guardrail functions must not call an LLM. The contract is a sync or async pure function: deterministic, bounded-latency, zero extra tokens.

If you need a nuanced semantic check โ€” context-dependent detection, entailment scoring, factuality grading โ€” that belongs in the evals subsystem (graders), not in a guardrail. Evals run on test data; guardrails run on production traffic.

Common patterns

Composing guardrails in sequence

The AgentGuardrails.input and AgentGuardrails.output lists run in order. A tripwire on any one of them halts the run; subsequent guardrails in the list do not execute. Build your most selective (fastest) checks first:

AgentGuardrails(
    input=[
        block_empty_input,          # fastest, cheapest
        no_sql_injection,           # pattern matching
        custom_topic_guard,         # application-specific
    ],
)

Per-tenant guardrail policies

Thread tenant identity through RunContext.context and gate inside the guardrail function:

@agent_input_guardrail
async def tenant_policy(
    data: AgentInputGuardrailData,
) -> AgentGuardrailFunctionOutput:
    ctx = data.context.context
    if isinstance(ctx, dict):
        tier = ctx.get("tier", "free")
        text = data.user_prompt if isinstance(data.user_prompt, str) else str(data.user_prompt)
        if tier == "free" and len(text) > 2000:
            return AgentGuardrailFunctionOutput(
                tripwire_triggered=True,
                output_info={"reason": "Prompt length exceeds free-tier limit"},
            )
    return AgentGuardrailFunctionOutput(tripwire_triggered=False)

Testing a guardrail in isolation

Because guardrails are pure functions, you can call them directly without spinning up a full agent run:

import asyncio
from unittest.mock import MagicMock
from augments.adk.agents.agent_guardrails import AgentInputGuardrailData
 
async def test_no_sql_injection_fires():
    agent_mock = MagicMock()
    context_mock = MagicMock()
 
    data = AgentInputGuardrailData(
        context=context_mock,
        agent=agent_mock,
        user_prompt="SELECT * FROM users; DROP TABLE users;--",
    )
    result = await no_sql_injection.run(data)
    assert result.tripwire_triggered is True
 
asyncio.run(test_no_sql_injection_fires())

AgentInputGuardrail.run() and AgentOutputGuardrail.run() are async and accept the matching *Data object directly, making unit tests straightforward without the Runner.

Timeout handling

Both input and output guardrails accept timeout (seconds) and timeout_policy:

from augments.adk.agents.agent_guardrails import AgentTimeoutPolicy
 
@agent_input_guardrail(
    timeout=1.0,
    timeout_policy=AgentTimeoutPolicy.PASS,   # continue silently on timeout
    on_timeout=log_timeout_metric,
)
async def slow_external_check(
    data: AgentInputGuardrailData,
) -> AgentGuardrailFunctionOutput:
    ...

FAIL (the default) treats a timeout as a tripwire trigger โ€” safe by default. PASS lets the run continue silently, useful when an external check is advisory and not availability-critical.

See also

  • Guardrail hub โ€” built-in hub reference: pii_guardrail, injection_scan_guardrail, fence_untrusted_text, wrong_language_guardrail, PatternScanner, the GuardrailAction vocabulary, and the guardrail audit side-car (RunResult.guardrail_audit).
  • Agent guardrails โ€” full reference for agent-level guardrails: TRANSFORM action, severity, timeout, remediation, hooks, and exception types.
  • Concepts โ€” the "Guardrails vs Middleware vs Hooks vs Sandbox" section explains how to choose the right extension point.
  • LLM-as-judge belongs in the evals package, not in guardrails.
  • Tools guide โ€” tool-level guardrails (ToolInputGuardrail, ToolOutputGuardrail) in full detail.
  • Architecture overview โ€” Stages 2 and 4 in context.
  • Declarative config โ€” the guardrails field and dotted ref syntax.