Skip to content

Commit 3df0d58

Browse files
committed
feat: add run_in_parallel parameter to input guardrails
1 parent 4bc33e3 commit 3df0d58

File tree

5 files changed

+133
-7
lines changed

5 files changed

+133
-7
lines changed

docs/guardrails.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Guardrails
22

3-
Guardrails run _in parallel_ to your agents, enabling you to do checks and validations of user input. For example, imagine you have an agent that uses a very smart (and hence slow/expensive) model to help with customer requests. You wouldn't want malicious users to ask the model to help them with their math homework. So, you can run a guardrail with a fast/cheap model. If the guardrail detects malicious usage, it can immediately raise an error, which stops the expensive model from running and saves you time/money.
3+
Guardrails enable you to do checks and validations of user input and agent output. For example, imagine you have an agent that uses a very smart (and hence slow/expensive) model to help with customer requests. You wouldn't want malicious users to ask the model to help them with their math homework. So, you can run a guardrail with a fast/cheap model. If the guardrail detects malicious usage, it can immediately raise an error and prevent the expensive model from running, saving you time and money.
44

55
There are two kinds of guardrails:
66

@@ -19,6 +19,14 @@ Input guardrails run in 3 steps:
1919

2020
Input guardrails are intended to run on user input, so an agent's guardrails only run if the agent is the *first* agent. You might wonder, why is the `guardrails` property on the agent instead of passed to `Runner.run`? It's because guardrails tend to be related to the actual Agent - you'd run different guardrails for different agents, so colocating the code is useful for readability.
2121

22+
### Execution modes
23+
24+
Input guardrails support two execution modes:
25+
26+
- **Parallel execution** (default, `run_in_parallel=True`): The guardrail runs concurrently with the agent's execution. This provides the best latency since both start at the same time. However, if the guardrail fails, the agent may have already consumed tokens and executed tools before being cancelled.
27+
28+
- **Blocking execution** (`run_in_parallel=False`): The guardrail runs and completes *before* the agent starts. If the guardrail tripwire is triggered, the agent never executes, preventing token consumption and tool execution. This is ideal for cost optimization and when you want to avoid potential side effects from tool calls.
29+
2230
## Output guardrails
2331

2432
Output guardrails run in 3 steps:
@@ -31,6 +39,8 @@ Output guardrails run in 3 steps:
3139

3240
Output guardrails are intended to run on the final agent output, so an agent's guardrails only run if the agent is the *last* agent. Similar to the input guardrails, we do this because guardrails tend to be related to the actual Agent - you'd run different guardrails for different agents, so colocating the code is useful for readability.
3341

42+
Output guardrails always run after the agent completes, so they don't support the `run_in_parallel` parameter.
43+
3444
## Tripwires
3545

3646
If the input or output fails the guardrail, the Guardrail can signal this with a tripwire. As soon as we see a guardrail that has triggered the tripwires, we immediately raise a `{Input,Output}GuardrailTripwireTriggered` exception and halt the Agent execution.

examples/agent_patterns/input_guardrails.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@
1717
"""
1818
This example shows how to use guardrails.
1919
20-
Guardrails are checks that run in parallel to the agent's execution.
20+
Guardrails are checks that run either in parallel with the agent or before the agent starts.
2121
They can be used to do things like:
2222
- Check if input messages are off-topic
2323
- Check that input messages don't violate any policies
2424
- Take over control of the agent's execution if an unexpected input is detected
2525
2626
In this example, we'll setup an input guardrail that trips if the user is asking to do math homework.
2727
If the guardrail trips, we'll respond with a refusal message.
28+
29+
By default, guardrails run in parallel with the agent for better latency.
30+
You can set run_in_parallel=False to run the guardrail before the agent starts,
31+
which saves token costs if the guardrail fails (the agent never starts).
2832
"""
2933

3034

src/agents/guardrail.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ class InputGuardrail(Generic[TContext]):
9797
function's name.
9898
"""
9999

100+
run_in_parallel: bool = True
101+
"""Whether the guardrail runs concurrently with the agent (True, default) or before
102+
the agent starts (False).
103+
"""
104+
100105
def get_name(self) -> str:
101106
if self.name:
102107
return self.name
@@ -209,6 +214,7 @@ def input_guardrail(
209214
def input_guardrail(
210215
*,
211216
name: str | None = None,
217+
run_in_parallel: bool = True,
212218
) -> Callable[
213219
[_InputGuardrailFuncSync[TContext_co] | _InputGuardrailFuncAsync[TContext_co]],
214220
InputGuardrail[TContext_co],
@@ -221,6 +227,7 @@ def input_guardrail(
221227
| None = None,
222228
*,
223229
name: str | None = None,
230+
run_in_parallel: bool = True,
224231
) -> (
225232
InputGuardrail[TContext_co]
226233
| Callable[
@@ -235,8 +242,14 @@ def input_guardrail(
235242
@input_guardrail
236243
def my_sync_guardrail(...): ...
237244
238-
@input_guardrail(name="guardrail_name")
245+
@input_guardrail(name="guardrail_name", run_in_parallel=False)
239246
async def my_async_guardrail(...): ...
247+
248+
Args:
249+
func: The guardrail function to wrap.
250+
name: Optional name for the guardrail. If not provided, uses the function's name.
251+
run_in_parallel: Whether to run the guardrail concurrently with the agent (True, default)
252+
or before the agent starts (False).
240253
"""
241254

242255
def decorator(
@@ -246,6 +259,7 @@ def decorator(
246259
guardrail_function=f,
247260
# If not set, guardrail name uses the function’s name by default.
248261
name=name if name else f.__name__,
262+
run_in_parallel=run_in_parallel,
249263
)
250264

251265
if func is not None:

src/agents/run.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -599,11 +599,31 @@ async def run(
599599
)
600600

601601
if current_turn == 1:
602+
# Separate guardrails based on execution mode.
603+
all_input_guardrails = starting_agent.input_guardrails + (
604+
run_config.input_guardrails or []
605+
)
606+
sequential_guardrails = [
607+
g for g in all_input_guardrails if not g.run_in_parallel
608+
]
609+
parallel_guardrails = [g for g in all_input_guardrails if g.run_in_parallel]
610+
611+
# Run blocking guardrails first, before agent starts.
612+
# (will raise exception if tripwire triggered).
613+
sequential_results = []
614+
if sequential_guardrails:
615+
sequential_results = await self._run_input_guardrails(
616+
starting_agent,
617+
sequential_guardrails,
618+
_copy_str_or_list(prepared_input),
619+
context_wrapper,
620+
)
621+
622+
# Run parallel guardrails + agent together.
602623
input_guardrail_results, turn_result = await asyncio.gather(
603624
self._run_input_guardrails(
604625
starting_agent,
605-
starting_agent.input_guardrails
606-
+ (run_config.input_guardrails or []),
626+
parallel_guardrails,
607627
_copy_str_or_list(prepared_input),
608628
context_wrapper,
609629
),
@@ -620,6 +640,9 @@ async def run(
620640
server_conversation_tracker=server_conversation_tracker,
621641
),
622642
)
643+
644+
# Combine sequential and parallel results.
645+
input_guardrail_results = sequential_results + input_guardrail_results
623646
else:
624647
turn_result = await self._run_single_turn(
625648
agent=current_agent,
@@ -997,11 +1020,31 @@ async def _start_streaming(
9971020
break
9981021

9991022
if current_turn == 1:
1000-
# Run the input guardrails in the background and put the results on the queue
1023+
# Separate guardrails based on execution mode.
1024+
all_input_guardrails = starting_agent.input_guardrails + (
1025+
run_config.input_guardrails or []
1026+
)
1027+
sequential_guardrails = [
1028+
g for g in all_input_guardrails if not g.run_in_parallel
1029+
]
1030+
parallel_guardrails = [g for g in all_input_guardrails if g.run_in_parallel]
1031+
1032+
# Run sequential guardrails first (will raise exception if tripwire triggered).
1033+
if sequential_guardrails:
1034+
await cls._run_input_guardrails_with_queue(
1035+
starting_agent,
1036+
sequential_guardrails,
1037+
ItemHelpers.input_to_new_input_list(prepared_input),
1038+
context_wrapper,
1039+
streamed_result,
1040+
current_span,
1041+
)
1042+
1043+
# Run parallel guardrails in background.
10011044
streamed_result._input_guardrails_task = asyncio.create_task(
10021045
cls._run_input_guardrails_with_queue(
10031046
starting_agent,
1004-
starting_agent.input_guardrails + (run_config.input_guardrails or []),
1047+
parallel_guardrails,
10051048
ItemHelpers.input_to_new_input_list(prepared_input),
10061049
context_wrapper,
10071050
streamed_result,

tests/test_guardrails.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,58 @@ async def test_output_guardrail_decorators():
260260
assert not result.output.tripwire_triggered
261261
assert result.output.output_info == "test_4"
262262
assert guardrail.get_name() == "Custom name"
263+
264+
265+
@pytest.mark.asyncio
266+
async def test_input_guardrail_run_in_parallel_default():
267+
guardrail = InputGuardrail(
268+
guardrail_function=lambda ctx, agent, input: GuardrailFunctionOutput(
269+
output_info=None, tripwire_triggered=False
270+
)
271+
)
272+
assert guardrail.run_in_parallel is True
273+
274+
275+
@pytest.mark.asyncio
276+
async def test_input_guardrail_run_in_parallel_false():
277+
guardrail = InputGuardrail(
278+
guardrail_function=lambda ctx, agent, input: GuardrailFunctionOutput(
279+
output_info=None, tripwire_triggered=False
280+
),
281+
run_in_parallel=False,
282+
)
283+
assert guardrail.run_in_parallel is False
284+
285+
286+
@pytest.mark.asyncio
287+
async def test_input_guardrail_decorator_with_run_in_parallel():
288+
@input_guardrail(run_in_parallel=False)
289+
def blocking_guardrail(
290+
context: RunContextWrapper[Any], agent: Agent[Any], input: str | list[TResponseInputItem]
291+
) -> GuardrailFunctionOutput:
292+
return GuardrailFunctionOutput(
293+
output_info="blocking",
294+
tripwire_triggered=False,
295+
)
296+
297+
assert blocking_guardrail.run_in_parallel is False
298+
result = await blocking_guardrail.run(
299+
agent=Agent(name="test"), input="test", context=RunContextWrapper(context=None)
300+
)
301+
assert not result.output.tripwire_triggered
302+
assert result.output.output_info == "blocking"
303+
304+
305+
@pytest.mark.asyncio
306+
async def test_input_guardrail_decorator_with_name_and_run_in_parallel():
307+
@input_guardrail(name="custom_name", run_in_parallel=False)
308+
def named_blocking_guardrail(
309+
context: RunContextWrapper[Any], agent: Agent[Any], input: str | list[TResponseInputItem]
310+
) -> GuardrailFunctionOutput:
311+
return GuardrailFunctionOutput(
312+
output_info="named_blocking",
313+
tripwire_triggered=False,
314+
)
315+
316+
assert named_blocking_guardrail.get_name() == "custom_name"
317+
assert named_blocking_guardrail.run_in_parallel is False

0 commit comments

Comments
 (0)