@@ -1997,3 +1997,140 @@ async def raise_after_sibling_starts(ctx, agent, agent_output):
19971997
19981998 assert sibling_cancelled .is_set (), "Sibling task should have been cancelled"
19991999 assert not sibling_completed .is_set (), "Sibling task should not have completed"
2000+
2001+
2002+ def _ordered_input_guardrails (
2003+ * , second_triggers : bool , second_raises : bool = False , run_in_parallel : bool = False
2004+ ) -> list [InputGuardrail [Any ]]:
2005+ """Build two guardrails whose completion order is fixed by an explicit barrier."""
2006+ first_done = asyncio .Event ()
2007+
2008+ async def first_fn (
2009+ context : RunContextWrapper [Any ], agent : Agent [Any ], input : str | list [TResponseInputItem ]
2010+ ) -> GuardrailFunctionOutput :
2011+ first_done .set ()
2012+ return GuardrailFunctionOutput (output_info = "passes" , tripwire_triggered = False )
2013+
2014+ async def second_fn (
2015+ context : RunContextWrapper [Any ], agent : Agent [Any ], input : str | list [TResponseInputItem ]
2016+ ) -> GuardrailFunctionOutput :
2017+ await first_done .wait ()
2018+ if second_raises :
2019+ raise RuntimeError ("guardrail exploded" )
2020+ return GuardrailFunctionOutput (output_info = "second" , tripwire_triggered = second_triggers )
2021+
2022+ return [
2023+ InputGuardrail (guardrail_function = first_fn , name = "passes" , run_in_parallel = run_in_parallel ),
2024+ InputGuardrail (
2025+ guardrail_function = second_fn ,
2026+ name = "raises" if second_raises else "trips" ,
2027+ run_in_parallel = run_in_parallel ,
2028+ ),
2029+ ]
2030+
2031+
2032+ def _tripwire_agent (model : FakeModel , * , run_in_parallel : bool ) -> Agent [Any ]:
2033+ return Agent (
2034+ name = "guardrail_results_agent" ,
2035+ model = model ,
2036+ input_guardrails = _ordered_input_guardrails (
2037+ second_triggers = True , run_in_parallel = run_in_parallel
2038+ ),
2039+ )
2040+
2041+
2042+ def _result_names (results : list [Any ]) -> list [str ]:
2043+ return [result .guardrail .get_name () for result in results ]
2044+
2045+
2046+ @pytest .mark .asyncio
2047+ @pytest .mark .parametrize ("run_in_parallel" , [False , True ])
2048+ async def test_input_guardrail_tripwire_reports_results (run_in_parallel : bool ):
2049+ """Runner.run() reports every completed guardrail result on the raised tripwire."""
2050+ model = FakeModel ()
2051+ model .set_next_output ([get_text_message ("hello" )])
2052+
2053+ with pytest .raises (InputGuardrailTripwireTriggered ) as exc_info :
2054+ await Runner .run (_tripwire_agent (model , run_in_parallel = run_in_parallel ), "test input" )
2055+
2056+ run_data = exc_info .value .run_data
2057+ assert run_data is not None
2058+ assert _result_names (run_data .input_guardrail_results ) == ["passes" , "trips" ]
2059+ assert exc_info .value .guardrail_result .guardrail .get_name () == "trips"
2060+
2061+
2062+ @pytest .mark .asyncio
2063+ @pytest .mark .parametrize ("run_in_parallel" , [False , True ])
2064+ async def test_input_guardrail_tripwire_reports_results_streamed (run_in_parallel : bool ):
2065+ """The streamed path reports the same results, including on the streamed result object."""
2066+ model = FakeModel ()
2067+ model .set_next_output ([get_text_message ("hello" )])
2068+
2069+ result = Runner .run_streamed (
2070+ _tripwire_agent (model , run_in_parallel = run_in_parallel ), "test input"
2071+ )
2072+ with pytest .raises (InputGuardrailTripwireTriggered ) as exc_info :
2073+ async for _ in result .stream_events ():
2074+ pass
2075+
2076+ run_data = exc_info .value .run_data
2077+ assert run_data is not None
2078+ assert _result_names (run_data .input_guardrail_results ) == ["passes" , "trips" ]
2079+ assert _result_names (result .input_guardrail_results ) == ["passes" , "trips" ]
2080+
2081+
2082+ def test_input_guardrail_tripwire_reports_results_sync ():
2083+ """Runner.run_sync() matches the async entry points."""
2084+ model = FakeModel ()
2085+ model .set_next_output ([get_text_message ("hello" )])
2086+
2087+ with pytest .raises (InputGuardrailTripwireTriggered ) as exc_info :
2088+ Runner .run_sync (_tripwire_agent (model , run_in_parallel = False ), "test input" )
2089+
2090+ run_data = exc_info .value .run_data
2091+ assert run_data is not None
2092+ assert _result_names (run_data .input_guardrail_results ) == ["passes" , "trips" ]
2093+
2094+
2095+ @pytest .mark .asyncio
2096+ async def test_input_guardrail_results_reported_on_success ():
2097+ """Passing guardrails still land on the successful result exactly once."""
2098+ model = FakeModel ()
2099+ model .set_next_output ([get_text_message ("hello" )])
2100+ agent = Agent (
2101+ name = "guardrail_results_agent" ,
2102+ model = model ,
2103+ input_guardrails = [
2104+ InputGuardrail (
2105+ guardrail_function = get_sync_guardrail (triggers = False ),
2106+ name = "blocking" ,
2107+ run_in_parallel = False ,
2108+ ),
2109+ InputGuardrail (
2110+ guardrail_function = get_sync_guardrail (triggers = False ),
2111+ name = "parallel" ,
2112+ run_in_parallel = True ,
2113+ ),
2114+ ],
2115+ )
2116+
2117+ result = await Runner .run (agent , "test input" )
2118+
2119+ assert _result_names (result .input_guardrail_results ) == ["blocking" , "parallel" ]
2120+
2121+
2122+ @pytest .mark .asyncio
2123+ async def test_input_guardrail_exception_reports_completed_results ():
2124+ """A guardrail raising a non-tripwire error still preserves earlier results."""
2125+
2126+ collected : list [Any ] = []
2127+ with pytest .raises (RuntimeError , match = "guardrail exploded" ):
2128+ await run_input_guardrails (
2129+ Agent (name = "t" ),
2130+ _ordered_input_guardrails (second_triggers = False , second_raises = True ),
2131+ "test input" ,
2132+ RunContextWrapper (context = None ),
2133+ collected ,
2134+ )
2135+
2136+ assert _result_names (collected ) == ["passes" ]
0 commit comments