Guardrails#
Guardrails are Maida’s opt-in way to stop a run before it burns more time, tokens, or tool calls than you intended.
They are runtime safety limits and evidence capture tools, not the post-run policy gate:
Use the normal projected event view only
Record normal trace evidence before aborting
Raise a dedicated exception so your code knows the run was stopped on purpose
Keep default behavior unchanged unless you enable a guardrail
Use the policy-v2 maida run gate for behavioral policy enforcement while it
executes isolated candidate trials.
What guardrails do#
When a configured threshold is crossed, Maida:
Records the relevant warning or event using the existing trace format
Raises
LoopAbortorGuardrailExceededRecords
ERRORFinalizes the run with
RUN_ENDandstatus="error"
The resulting ERROR payload includes:
guardrailthresholdactualerror_typemessage
That means the UI and raw trace both show not just that the run failed, but why it was intentionally stopped.
Available guardrails#
Parameter |
Type |
Default |
Meaning |
|---|---|---|---|
|
|
|
Abort when loop detection emits |
|
|
|
Minimum repeated pattern count required to abort on loop |
|
|
|
Abort after more than N LLM calls |
|
|
|
Abort after more than N tool calls |
|
|
|
Abort after more than N total events |
|
|
|
Abort when elapsed run time reaches the limit |
Notes:
Count-based guardrails trigger at N+1, not at N.
max_duration_striggers when elapsed time is greater than or equal to the configured value.stop_on_loopdoes not create a new event type. It relies on the existingLOOP_WARNINGevent, then aborts.
LangChain / LangGraph#
Guardrails work with LangChain/LangGraph via LangChainCallbackHandler. When a guardrail fires, the handler raises _MaidaAbortSignal (a BaseException) which bypasses both LangChain’s callback error handling and LangGraph’s graph executor — stopping the run immediately and preventing further token-wasting LLM calls.
from maida import LoopAbort, trace
from maida.integrations import LangChainCallbackHandler
@trace(stop_on_loop=True, stop_on_loop_min_repetitions=3)
def run_agent():
handler = LangChainCallbackHandler()
return graph.invoke(state, config={"callbacks": [handler]})
try:
run_agent()
except LoopAbort as exc:
print(f"Stopped the loop: {exc}")
The handler also stores the exception on handler.abort_exception as a defensive fallback, with a handler.raise_if_aborted() convenience method. To reuse a handler across runs, call handler.reset() between runs.
OpenAI Agents SDK#
Guardrails work with the OpenAI Agents SDK via the tracing processor. When a guardrail fires, the processor raises _MaidaAbortSignal (a BaseException) which bypasses the SDK’s except Exception error handling — stopping the run immediately.
from maida import trace, LoopAbort
from maida.integrations import openai_agents
@trace(stop_on_loop=True)
def run_agent():
result = Runner.run_sync(agent, input)
return result
try:
run_agent()
except LoopAbort as exc:
print(f"Loop detected: {exc}")
As a defensive fallback, the exception is also stored on PROCESSOR.abort_exception with a PROCESSOR.raise_if_aborted() convenience method.
Quick examples#
Stop a looping agent immediately#
from maida import LoopAbort, record_llm_call, record_tool_call, trace
@trace(stop_on_loop=True)
def run_agent():
for _ in range(10):
record_tool_call("search_db", args={"q": "refund policy"}, result={"hits": 3})
record_llm_call(model="gpt-4.1", prompt="Summarize", response="Retrying...")
try:
run_agent()
except LoopAbort as exc:
print(f"Stopped because of a loop: {exc}")
Cap LLM and tool usage during development#
from maida import GuardrailExceeded, record_llm_call, record_tool_call, traced_run
try:
with traced_run(
name="react_guarded",
max_llm_calls=8,
max_tool_calls=12,
max_events=40,
max_duration_s=30,
):
# ... your agent loop ...
record_llm_call(model="gpt-4.1", prompt="...", response="...")
record_tool_call(name="search", args={"q": "docs"}, result={"hits": 2})
except GuardrailExceeded as exc:
print(exc.guardrail, exc.threshold, exc.actual)
Configuration surfaces#
Guardrails can be configured in four places:
@trace(...)traced_run(...)Project or user YAML config
Environment variables
Precedence#
Highest wins:
Function arguments passed to
@trace(...)ortraced_run(...)Environment variables
Project YAML:
.maida/config.yamlUser YAML:
~/.maida/config.yamlDefaults
Decorator and context manager#
from maida import trace, traced_run
@trace(stop_on_loop=True, max_llm_calls=50)
def guarded_fn():
...
with traced_run(stop_on_loop=True, max_llm_calls=50):
...
Environment variables#
export MAIDA_STOP_ON_LOOP=1
export MAIDA_STOP_ON_LOOP_MIN_REPETITIONS=3
export MAIDA_MAX_LLM_CALLS=50
export MAIDA_MAX_TOOL_CALLS=50
export MAIDA_MAX_EVENTS=200
export MAIDA_MAX_DURATION_S=60
YAML config#
# .maida/config.yaml or ~/.maida/config.yaml
guardrails:
stop_on_loop: true
stop_on_loop_min_repetitions: 3
max_llm_calls: 50
max_tool_calls: 50
max_events: 200
max_duration_s: 60
How guardrails appear in traces#
Loop guardrail#
If loop detection fires and stop_on_loop=True:
Maida first writes
LOOP_WARNINGThen raises
LoopAbortThen writes
ERRORThen writes
RUN_END(status="error")
Count and duration guardrails#
For max_llm_calls, max_tool_calls, max_events, and max_duration_s:
Maida writes the event that crossed the limit
Then raises
GuardrailExceededThen writes
ERRORThen writes
RUN_END(status="error")
This gives you full evidence of the step that actually tripped the limit.
Choosing sensible defaults#
Some practical starting points for local development:
stop_on_loop=Truefor ReAct-style or planner/executor loopsmax_llm_calls=10to30for prompt iterationmax_tool_calls=10to25for tool-heavy developmentmax_events=50to200when you want a hard ceiling on trace sizemax_duration_s=15to60for runs that should finish quickly
Use tighter limits in tests and demos, and looser limits when you are intentionally exploring bigger workflows.