1313 InputGuardrail ,
1414 InputGuardrailTripwireTriggered ,
1515 MaxTurnsExceeded ,
16+ ModelBehaviorError ,
17+ ModelRefusalError ,
18+ RunConfig ,
1619 RunContextWrapper ,
1720 Runner ,
1821 TResponseInputItem ,
22+ UserError ,
1923 _debug ,
2024)
25+ from agents .tracing .span_data import AgentSpanData
26+ from agents .tracing .spans import SpanImpl
2127
2228from .fake_model import FakeModel
2329from .test_responses import (
2733 get_handoff_tool_call ,
2834 get_text_message ,
2935)
30- from .testing_processor import fetch_normalized_spans
36+ from .testing_processor import SPAN_PROCESSOR_TESTING , fetch_normalized_spans , fetch_span_errors
3137
3238
3339@pytest .mark .asyncio
@@ -49,6 +55,7 @@ async def test_single_turn_model_error():
4955 "children" : [
5056 {
5157 "type" : "agent" ,
58+ "error" : {"message" : "Error in agent run" , "data" : {"error" : "test error" }},
5259 "data" : {
5360 "name" : "test_agent" ,
5461 "handoffs" : [],
@@ -102,6 +109,7 @@ async def test_multi_turn_no_handoffs():
102109 "children" : [
103110 {
104111 "type" : "agent" ,
112+ "error" : {"message" : "Error in agent run" , "data" : {"error" : "test error" }},
105113 "data" : {
106114 "name" : "test_agent" ,
107115 "handoffs" : [],
@@ -558,3 +566,167 @@ async def test_guardrail_error():
558566 }
559567 ]
560568 )
569+
570+
571+ SENSITIVE_ERROR_MESSAGE = "sensitive-error-detail"
572+
573+
574+ @pytest .mark .asyncio
575+ async def test_run_marks_agent_span_with_generic_error ():
576+ """A generic run failure marks the agent span, matching the streamed path."""
577+ model = FakeModel (tracing_enabled = True )
578+ model .set_next_output (ValueError ("test error" ))
579+
580+ with pytest .raises (ValueError , match = "test error" ):
581+ await Runner .run (Agent (name = "test_agent" , model = model ), input = "first_test" )
582+
583+ assert fetch_span_errors ("agent" ) == [
584+ {"message" : "Error in agent run" , "data" : {"error" : "test error" }}
585+ ]
586+
587+
588+ def test_run_sync_marks_agent_span_with_generic_error ():
589+ model = FakeModel (tracing_enabled = True )
590+ model .set_next_output (ValueError ("test error" ))
591+
592+ with pytest .raises (ValueError , match = "test error" ):
593+ Runner .run_sync (Agent (name = "test_agent" , model = model ), input = "first_test" )
594+
595+ assert fetch_span_errors ("agent" ) == [
596+ {"message" : "Error in agent run" , "data" : {"error" : "test error" }}
597+ ]
598+
599+
600+ @pytest .mark .asyncio
601+ async def test_run_agent_span_error_matches_streamed_path ():
602+ """The non-streamed and streamed paths record the same agent span error."""
603+ non_streamed_model = FakeModel (tracing_enabled = True )
604+ non_streamed_model .set_next_output (ValueError ("test error" ))
605+ with pytest .raises (ValueError ):
606+ await Runner .run (Agent (name = "test_agent" , model = non_streamed_model ), input = "first_test" )
607+ non_streamed_errors = fetch_span_errors ("agent" )
608+
609+ SPAN_PROCESSOR_TESTING .clear ()
610+
611+ streamed_model = FakeModel (tracing_enabled = True )
612+ streamed_model .set_next_output (ValueError ("test error" ))
613+ result = Runner .run_streamed (Agent (name = "test_agent" , model = streamed_model ), input = "first_test" )
614+ with pytest .raises (ValueError ):
615+ async for _ in result .stream_events ():
616+ pass
617+
618+ assert non_streamed_errors == fetch_span_errors ("agent" )
619+
620+
621+ @pytest .mark .asyncio
622+ async def test_run_agent_span_error_redacts_sensitive_data ():
623+ model = FakeModel (tracing_enabled = False )
624+ model .set_next_output (ValueError (SENSITIVE_ERROR_MESSAGE ))
625+
626+ with pytest .raises (ValueError ):
627+ await Runner .run (
628+ Agent (name = "test_agent" , model = model ),
629+ input = "first_test" ,
630+ run_config = RunConfig (trace_include_sensitive_data = False ),
631+ )
632+
633+ assert fetch_span_errors ("agent" ) == [
634+ {
635+ "message" : "Error in agent run" ,
636+ "data" : {"error" : "Error details are redacted." },
637+ }
638+ ]
639+
640+
641+ @pytest .mark .asyncio
642+ async def test_run_does_not_mark_agent_span_for_model_behavior_error ():
643+ """ModelBehaviorError is reported by the generation span, so the agent span stays clean."""
644+ model = FakeModel (tracing_enabled = True )
645+ model .set_next_output (ModelBehaviorError ("bad model output" ))
646+
647+ with pytest .raises (ModelBehaviorError ):
648+ await Runner .run (Agent (name = "test_agent" , model = model ), input = "first_test" )
649+
650+ assert fetch_span_errors ("agent" ) == []
651+
652+
653+ @pytest .mark .asyncio
654+ @pytest .mark .parametrize (
655+ ("error" , "expected_detail" ),
656+ [
657+ pytest .param (
658+ ModelRefusalError ("refused" ),
659+ "Model refused to produce output: refused" ,
660+ id = "model-refusal-error" ,
661+ ),
662+ pytest .param (UserError ("user problem" ), "user problem" , id = "user-error" ),
663+ ],
664+ )
665+ async def test_run_marks_agent_span_for_other_agents_exceptions (
666+ error : Exception , expected_detail : str
667+ ):
668+ """Agents exceptions without a dedicated agent-span error match the streamed path."""
669+ model = FakeModel (tracing_enabled = True )
670+ model .set_next_output (error )
671+
672+ with pytest .raises (type (error )):
673+ await Runner .run (Agent (name = "test_agent" , model = model ), input = "first_test" )
674+
675+ assert fetch_span_errors ("agent" ) == [
676+ {"message" : "Error in agent run" , "data" : {"error" : expected_detail }}
677+ ]
678+
679+
680+ @pytest .mark .asyncio
681+ async def test_run_keeps_specific_max_turns_agent_span_error ():
682+ """A more specific span error already on the agent span is not overwritten."""
683+ model = FakeModel (tracing_enabled = True )
684+ agent = Agent (name = "test_agent" , model = model , tools = [get_function_tool ("foo" , "res" )])
685+ model .add_multiple_turn_outputs (
686+ [
687+ [get_function_tool_call ("foo" , json .dumps ({"a" : "b" }), call_id = "c1" )],
688+ [get_function_tool_call ("foo" , json .dumps ({"a" : "b" }), call_id = "c2" )],
689+ [get_function_tool_call ("foo" , json .dumps ({"a" : "b" }), call_id = "c3" )],
690+ ]
691+ )
692+
693+ with pytest .raises (MaxTurnsExceeded ):
694+ await Runner .run (agent , input = "first_test" , max_turns = 2 )
695+
696+ assert fetch_span_errors ("agent" ) == [
697+ {"message" : "Max turns exceeded" , "data" : {"max_turns" : 2 }}
698+ ]
699+
700+
701+ @pytest .mark .asyncio
702+ async def test_run_attaches_agent_span_error_exactly_once (
703+ monkeypatch : pytest .MonkeyPatch ,
704+ ):
705+ """The generic error is recorded once, not re-applied by nested handlers."""
706+ recorded : list [Any ] = []
707+ original_set_error = SpanImpl .set_error
708+
709+ def counting_set_error (self : Any , error : Any ) -> None :
710+ if isinstance (self .span_data , AgentSpanData ):
711+ recorded .append (error )
712+ original_set_error (self , error )
713+
714+ monkeypatch .setattr (SpanImpl , "set_error" , counting_set_error )
715+
716+ model = FakeModel (tracing_enabled = True )
717+ model .set_next_output (ValueError ("test error" ))
718+ with pytest .raises (ValueError ):
719+ await Runner .run (Agent (name = "test_agent" , model = model ), input = "first_test" )
720+
721+ assert recorded == [{"message" : "Error in agent run" , "data" : {"error" : "test error" }}]
722+
723+
724+ @pytest .mark .asyncio
725+ async def test_successful_run_leaves_agent_span_without_error ():
726+ model = FakeModel (tracing_enabled = True )
727+ model .set_next_output ([get_text_message ("done" )])
728+
729+ result = await Runner .run (Agent (name = "test_agent" , model = model ), input = "first_test" )
730+
731+ assert result .final_output == "done"
732+ assert fetch_span_errors ("agent" ) == []
0 commit comments