11import sys
22from functools import wraps
33
4+ from sentry_sdk .consts import SPANDATA
45from sentry_sdk .integrations import DidNotEnable
5- from sentry_sdk .utils import reraise
6+ from sentry_sdk .utils import capture_internal_exceptions , reraise
67from ..spans import (
78 invoke_agent_span ,
89 end_invoke_agent_span ,
@@ -31,22 +32,10 @@ def _patch_agent_run() -> None:
3132
3233 # Store original methods
3334 original_run_single_turn = agents .run .AgentRunner ._run_single_turn
35+ original_run_single_turn_streamed = agents .run .AgentRunner ._run_single_turn_streamed
3436 original_execute_handoffs = agents ._run_impl .RunImpl .execute_handoffs
3537 original_execute_final_output = agents ._run_impl .RunImpl .execute_final_output
3638
37- def _start_invoke_agent_span (
38- context_wrapper : "agents.RunContextWrapper" ,
39- agent : "agents.Agent" ,
40- kwargs : "dict[str, Any]" ,
41- ) -> "Span" :
42- """Start an agent invocation span"""
43- # Store the agent on the context wrapper so we can access it later
44- context_wrapper ._sentry_current_agent = agent
45- span = invoke_agent_span (context_wrapper , agent , kwargs )
46- context_wrapper ._sentry_agent_span = span
47-
48- return span
49-
5039 def _has_active_agent_span (context_wrapper : "agents.RunContextWrapper" ) -> bool :
5140 """Check if there's an active agent span for this context"""
5241 return getattr (context_wrapper , "_sentry_current_agent" , None ) is not None
@@ -57,6 +46,46 @@ def _get_current_agent(
5746 """Get the current agent from context wrapper"""
5847 return getattr (context_wrapper , "_sentry_current_agent" , None )
5948
49+ def _close_streaming_workflow_span (agent : "Optional[agents.Agent]" ) -> None :
50+ """Close the workflow span for streaming executions if it exists."""
51+ if agent and hasattr (agent , "_sentry_workflow_span" ):
52+ workflow_span = agent ._sentry_workflow_span
53+ workflow_span .__exit__ (* sys .exc_info ())
54+ delattr (agent , "_sentry_workflow_span" )
55+
56+ def _maybe_start_agent_span (
57+ context_wrapper : "agents.RunContextWrapper" ,
58+ agent : "agents.Agent" ,
59+ should_run_agent_start_hooks : bool ,
60+ span_kwargs : "dict[str, Any]" ,
61+ is_streaming : bool = False ,
62+ ) -> "Optional[Span]" :
63+ """
64+ Start an agent invocation span if conditions are met.
65+ Handles ending any existing span for a different agent.
66+
67+ Returns the new span if started, or the existing span if conditions aren't met.
68+ """
69+ if not (should_run_agent_start_hooks and agent and context_wrapper ):
70+ return getattr (context_wrapper , "_sentry_agent_span" , None )
71+
72+ # End any existing span for a different agent
73+ if _has_active_agent_span (context_wrapper ):
74+ current_agent = _get_current_agent (context_wrapper )
75+ if current_agent and current_agent != agent :
76+ end_invoke_agent_span (context_wrapper , current_agent )
77+
78+ # Store the agent on the context wrapper so we can access it later
79+ context_wrapper ._sentry_current_agent = agent
80+ span = invoke_agent_span (context_wrapper , agent , span_kwargs )
81+ context_wrapper ._sentry_agent_span = span
82+ agent ._sentry_agent_span = span
83+
84+ if is_streaming :
85+ span .set_data (SPANDATA .GEN_AI_RESPONSE_STREAMING , True )
86+
87+ return span
88+
6089 @wraps (
6190 original_run_single_turn .__func__
6291 if hasattr (original_run_single_turn , "__func__" )
@@ -68,28 +97,18 @@ async def patched_run_single_turn(
6897 """Patched _run_single_turn that creates agent invocation spans"""
6998 agent = kwargs .get ("agent" )
7099 context_wrapper = kwargs .get ("context_wrapper" )
71- should_run_agent_start_hooks = kwargs .get ("should_run_agent_start_hooks" )
72-
73- span = getattr (context_wrapper , "_sentry_agent_span" , None )
74- # Start agent span when agent starts (but only once per agent)
75- if should_run_agent_start_hooks and agent and context_wrapper :
76- # End any existing span for a different agent
77- if _has_active_agent_span (context_wrapper ):
78- current_agent = _get_current_agent (context_wrapper )
79- if current_agent and current_agent != agent :
80- end_invoke_agent_span (context_wrapper , current_agent )
100+ should_run_agent_start_hooks = kwargs .get ("should_run_agent_start_hooks" , False )
81101
82- span = _start_invoke_agent_span (context_wrapper , agent , kwargs )
83- agent ._sentry_agent_span = span
102+ span = _maybe_start_agent_span (
103+ context_wrapper , agent , should_run_agent_start_hooks , kwargs
104+ )
84105
85- # Call original method with all the correct parameters
86106 try :
87107 result = await original_run_single_turn (* args , ** kwargs )
88108 except Exception as exc :
89109 if span is not None and span .timestamp is None :
90110 _record_exception_on_span (span , exc )
91111 end_invoke_agent_span (context_wrapper , agent )
92-
93112 reraise (* sys .exc_info ())
94113
95114 return result
@@ -117,7 +136,11 @@ async def patched_execute_handoffs(
117136 # Call original method with all parameters
118137 try :
119138 result = await original_execute_handoffs (* args , ** kwargs )
120-
139+ except Exception :
140+ exc_info = sys .exc_info ()
141+ with capture_internal_exceptions ():
142+ _close_streaming_workflow_span (agent )
143+ reraise (* exc_info )
121144 finally :
122145 # End span for current agent after handoff processing is complete
123146 if agent and context_wrapper and _has_active_agent_span (context_wrapper ):
@@ -139,18 +162,84 @@ async def patched_execute_final_output(
139162 context_wrapper = kwargs .get ("context_wrapper" )
140163 final_output = kwargs .get ("final_output" )
141164
142- # Call original method with all parameters
143165 try :
144166 result = await original_execute_final_output (* args , ** kwargs )
145167 finally :
146- # End span for current agent after final output processing is complete
147- if agent and context_wrapper and _has_active_agent_span (context_wrapper ):
148- end_invoke_agent_span (context_wrapper , agent , final_output )
168+ with capture_internal_exceptions ():
169+ if (
170+ agent
171+ and context_wrapper
172+ and _has_active_agent_span (context_wrapper )
173+ ):
174+ end_invoke_agent_span (context_wrapper , agent , final_output )
175+ # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper)
176+ _close_streaming_workflow_span (agent )
177+
178+ return result
179+
180+ @wraps (
181+ original_run_single_turn_streamed .__func__
182+ if hasattr (original_run_single_turn_streamed , "__func__" )
183+ else original_run_single_turn_streamed
184+ )
185+ async def patched_run_single_turn_streamed (
186+ cls : "agents.Runner" , * args : "Any" , ** kwargs : "Any"
187+ ) -> "Any" :
188+ """Patched _run_single_turn_streamed that creates agent invocation spans for streaming.
189+
190+ Note: Unlike _run_single_turn which uses keyword-only arguments (*,),
191+ _run_single_turn_streamed uses positional arguments. The call signature is:
192+ _run_single_turn_streamed(
193+ streamed_result, # args[0]
194+ agent, # args[1]
195+ hooks, # args[2]
196+ context_wrapper, # args[3]
197+ run_config, # args[4]
198+ should_run_agent_start_hooks, # args[5]
199+ tool_use_tracker, # args[6]
200+ all_tools, # args[7]
201+ server_conversation_tracker, # args[8] (optional)
202+ )
203+ """
204+ streamed_result = args [0 ] if len (args ) > 0 else kwargs .get ("streamed_result" )
205+ agent = args [1 ] if len (args ) > 1 else kwargs .get ("agent" )
206+ context_wrapper = args [3 ] if len (args ) > 3 else kwargs .get ("context_wrapper" )
207+ should_run_agent_start_hooks = bool (
208+ args [5 ]
209+ if len (args ) > 5
210+ else kwargs .get ("should_run_agent_start_hooks" , False )
211+ )
212+
213+ span_kwargs : "dict[str, Any]" = {}
214+ if streamed_result and hasattr (streamed_result , "input" ):
215+ span_kwargs ["original_input" ] = streamed_result .input
216+
217+ span = _maybe_start_agent_span (
218+ context_wrapper ,
219+ agent ,
220+ should_run_agent_start_hooks ,
221+ span_kwargs ,
222+ is_streaming = True ,
223+ )
224+
225+ try :
226+ result = await original_run_single_turn_streamed (* args , ** kwargs )
227+ except Exception as exc :
228+ exc_info = sys .exc_info ()
229+ with capture_internal_exceptions ():
230+ if span is not None and span .timestamp is None :
231+ _record_exception_on_span (span , exc )
232+ end_invoke_agent_span (context_wrapper , agent )
233+ _close_streaming_workflow_span (agent )
234+ reraise (* exc_info )
149235
150236 return result
151237
152238 # Apply patches
153239 agents .run .AgentRunner ._run_single_turn = classmethod (patched_run_single_turn )
240+ agents .run .AgentRunner ._run_single_turn_streamed = classmethod (
241+ patched_run_single_turn_streamed
242+ )
154243 agents ._run_impl .RunImpl .execute_handoffs = classmethod (patched_execute_handoffs )
155244 agents ._run_impl .RunImpl .execute_final_output = classmethod (
156245 patched_execute_final_output
0 commit comments