33import sentry_sdk
44
55from ..spans import invoke_agent_span , update_invoke_agent_span
6- from ..utils import _capture_exception
6+ from ..utils import _capture_exception , pop_agent , push_agent
77
88from typing import TYPE_CHECKING
99from pydantic_ai .agent import Agent # type: ignore
@@ -41,17 +41,20 @@ async def __aenter__(self):
4141 self ._isolation_scope = sentry_sdk .isolation_scope ()
4242 self ._isolation_scope .__enter__ ()
4343
44- # Store agent reference and streaming flag
45- sentry_sdk .get_current_scope ().set_context (
46- "pydantic_ai_agent" , {"_agent" : self .agent , "_streaming" : self .is_streaming }
47- )
48-
4944 # Create invoke_agent span (will be closed in __aexit__)
5045 self ._span = invoke_agent_span (
51- self .user_prompt , self .agent , self .model , self .model_settings
46+ self .user_prompt ,
47+ self .agent ,
48+ self .model ,
49+ self .model_settings ,
50+ self .is_streaming ,
5251 )
5352 self ._span .__enter__ ()
5453
54+ # Push agent to contextvar stack after span is successfully created and entered
55+ # This ensures proper pairing with pop_agent() in __aexit__ even if exceptions occur
56+ push_agent (self .agent , self .is_streaming )
57+
5558 # Enter the original context manager
5659 result = await self .original_ctx_manager .__aenter__ ()
5760 self ._result = result
@@ -71,7 +74,9 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
7174 if self ._span is not None :
7275 update_invoke_agent_span (self ._span , output )
7376 finally :
74- sentry_sdk .get_current_scope ().remove_context ("pydantic_ai_agent" )
77+ # Pop agent from contextvar stack
78+ pop_agent ()
79+
7580 # Clean up invoke span
7681 if self ._span :
7782 self ._span .__exit__ (exc_type , exc_val , exc_tb )
@@ -97,19 +102,19 @@ async def wrapper(self, *args, **kwargs):
97102 # Isolate each workflow so that when agents are run in asyncio tasks they
98103 # don't touch each other's scopes
99104 with sentry_sdk .isolation_scope ():
100- # Store agent reference and streaming flag in Sentry scope for access in nested spans
101- # We store the full agent to allow access to tools and system prompts
102- sentry_sdk .get_current_scope ().set_context (
103- "pydantic_ai_agent" , {"_agent" : self , "_streaming" : is_streaming }
104- )
105-
106105 # Extract parameters for the span
107106 user_prompt = kwargs .get ("user_prompt" ) or (args [0 ] if args else None )
108107 model = kwargs .get ("model" )
109108 model_settings = kwargs .get ("model_settings" )
110109
111110 # Create invoke_agent span
112- with invoke_agent_span (user_prompt , self , model , model_settings ) as span :
111+ with invoke_agent_span (
112+ user_prompt , self , model , model_settings , is_streaming
113+ ) as span :
114+ # Push agent to contextvar stack after span is successfully created and entered
115+ # This ensures proper pairing with pop_agent() in finally even if exceptions occur
116+ push_agent (self , is_streaming )
117+
113118 try :
114119 result = await original_func (self , * args , ** kwargs )
115120
@@ -122,7 +127,8 @@ async def wrapper(self, *args, **kwargs):
122127 _capture_exception (exc )
123128 raise exc from None
124129 finally :
125- sentry_sdk .get_current_scope ().remove_context ("pydantic_ai_agent" )
130+ # Pop agent from contextvar stack
131+ pop_agent ()
126132
127133 return wrapper
128134
0 commit comments