Skip to content

Commit 741e16f

Browse files
committed
fix: use isolation_scopes to keep execute_tool spans separate
1 parent c7b7ae7 commit 741e16f

File tree

5 files changed

+49
-70
lines changed

5 files changed

+49
-70
lines changed

sentry_sdk/integrations/pydantic_ai/patches/agent_run.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sentry_sdk
44

55
from ..spans import invoke_agent_span, update_invoke_agent_span
6-
from ..utils import _capture_exception, push_invoke_agent_span, pop_invoke_agent_span
6+
from ..utils import _capture_exception, pop_agent
77

88
from typing import TYPE_CHECKING
99
from pydantic_ai.agent import Agent # type: ignore
@@ -70,8 +70,8 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
7070
if self._span is not None:
7171
update_invoke_agent_span(self._span, output)
7272
finally:
73-
# Pop span from contextvar stack
74-
pop_invoke_agent_span()
73+
# Pop agent from contextvar stack
74+
pop_agent()
7575

7676
# Clean up invoke span
7777
if self._span:
@@ -119,8 +119,8 @@ async def wrapper(self, *args, **kwargs):
119119
_capture_exception(exc)
120120
raise exc from None
121121
finally:
122-
# Pop span from contextvar stack
123-
pop_invoke_agent_span()
122+
# Pop agent from contextvar stack
123+
pop_agent()
124124

125125
return wrapper
126126

sentry_sdk/integrations/pydantic_ai/patches/tools.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from ..utils import (
99
_capture_exception,
1010
get_current_agent,
11-
get_current_invoke_agent_span,
1211
)
1312

1413
from typing import TYPE_CHECKING
@@ -53,30 +52,33 @@ async def wrapped_call_tool(self, call, allow_partial, wrap_validation_errors):
5352
if tool and HAS_MCP and isinstance(tool.toolset, MCPServer):
5453
tool_type = "mcp"
5554

56-
# Get agent and invoke_agent span from contextvar
55+
# Get agent from contextvar
5756
agent = get_current_agent()
58-
invoke_span = get_current_invoke_agent_span()
5957

60-
if invoke_span and tool:
58+
if agent and tool:
6159
try:
6260
args_dict = call.args_as_dict()
6361
except Exception:
6462
args_dict = call.args if isinstance(call.args, dict) else {}
6563

66-
# Create execute_tool span as a child of invoke_agent span
67-
# Passing parent_span ensures parallel tools are siblings under the same parent
68-
with execute_tool_span(
69-
name, args_dict, agent, tool_type=tool_type, parent_span=invoke_span
70-
) as span:
71-
try:
72-
result = await original_call_tool(
73-
self, call, allow_partial, wrap_validation_errors
74-
)
75-
update_execute_tool_span(span, result)
76-
return result
77-
except Exception as exc:
78-
_capture_exception(exc)
79-
raise exc from None
64+
# Create execute_tool span
65+
# Nesting is handled by isolation_scope() to ensure proper parent-child relationships
66+
with sentry_sdk.isolation_scope():
67+
with execute_tool_span(
68+
name,
69+
args_dict,
70+
agent,
71+
tool_type=tool_type,
72+
) as span:
73+
try:
74+
result = await original_call_tool(
75+
self, call, allow_partial, wrap_validation_errors
76+
)
77+
update_execute_tool_span(span, result)
78+
return result
79+
except Exception as exc:
80+
_capture_exception(exc)
81+
raise exc from None
8082

8183
# No span context - just call original
8284
return await original_call_tool(

sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,21 @@
1111
from typing import Any, Optional
1212

1313

14-
def execute_tool_span(
15-
tool_name, tool_args, agent, tool_type="function", parent_span=None
16-
):
17-
# type: (str, Any, Any, str, Optional[sentry_sdk.tracing.Span]) -> sentry_sdk.tracing.Span
14+
def execute_tool_span(tool_name, tool_args, agent, tool_type="function"):
15+
# type: (str, Any, Any, str) -> sentry_sdk.tracing.Span
1816
"""Create a span for tool execution.
1917
2018
Args:
2119
tool_name: The name of the tool being executed
2220
tool_args: The arguments passed to the tool
2321
agent: The agent executing the tool
2422
tool_type: The type of tool ("function" for regular tools, "mcp" for MCP services)
25-
parent_span: Optional parent span to create this as a child of. If provided,
26-
uses parent_span.start_child() to ensure parallel tools are siblings.
2723
"""
28-
if parent_span:
29-
# Create as child of the specified parent span
30-
# This ensures parallel tool calls are siblings under the same parent
31-
span = parent_span.start_child(
32-
op=OP.GEN_AI_EXECUTE_TOOL,
33-
name=f"execute_tool {tool_name}",
34-
origin=SPAN_ORIGIN,
35-
)
36-
else:
37-
# Create as child of current span
38-
span = sentry_sdk.start_span(
39-
op=OP.GEN_AI_EXECUTE_TOOL,
40-
name=f"execute_tool {tool_name}",
41-
origin=SPAN_ORIGIN,
42-
)
24+
span = sentry_sdk.start_span(
25+
op=OP.GEN_AI_EXECUTE_TOOL,
26+
name=f"execute_tool {tool_name}",
27+
origin=SPAN_ORIGIN,
28+
)
4329

4430
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool")
4531
span.set_data(SPANDATA.GEN_AI_TOOL_TYPE, tool_type)

sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
_set_available_tools,
99
_set_model_data,
1010
_should_send_prompts,
11-
push_invoke_agent_span,
11+
push_agent,
1212
)
1313

1414
from typing import TYPE_CHECKING
@@ -33,9 +33,9 @@ def invoke_agent_span(user_prompt, agent, model, model_settings, is_streaming=Fa
3333

3434
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent")
3535

36-
# Push span and agent to contextvar stack immediately after span creation
36+
# Push agent to contextvar stack immediately after span creation
3737
# This ensures the agent is available in get_current_agent() before _set_model_data is called
38-
push_invoke_agent_span(span, agent, is_streaming)
38+
push_agent(agent, is_streaming)
3939

4040
_set_agent_data(span, agent)
4141
_set_model_data(span, model, model_settings)

sentry_sdk/integrations/pydantic_ai/utils.py

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,41 +11,32 @@
1111
from typing import Any, Optional
1212

1313

14-
# Store the current invoke_agent span in a contextvar for re-entrant safety
14+
# Store the current agent context in a contextvar for re-entrant safety
1515
# Using a list as a stack to support nested agent calls
16-
_invoke_agent_span_stack = ContextVar("pydantic_ai_invoke_agent_span_stack", default=[]) # type: ContextVar[list[dict[str, Any]]]
16+
_agent_context_stack = ContextVar("pydantic_ai_agent_context_stack", default=[]) # type: ContextVar[list[dict[str, Any]]]
1717

1818

19-
def push_invoke_agent_span(span, agent, is_streaming=False):
20-
# type: (sentry_sdk.tracing.Span, Any, bool) -> None
21-
"""Push an invoke_agent span onto the stack along with its agent and streaming flag."""
22-
stack = _invoke_agent_span_stack.get().copy()
23-
stack.append({"span": span, "agent": agent, "is_streaming": is_streaming})
24-
_invoke_agent_span_stack.set(stack)
19+
def push_agent(agent, is_streaming=False):
20+
# type: (Any, bool) -> None
21+
"""Push an agent context onto the stack along with its streaming flag."""
22+
stack = _agent_context_stack.get().copy()
23+
stack.append({"agent": agent, "is_streaming": is_streaming})
24+
_agent_context_stack.set(stack)
2525

2626

27-
def pop_invoke_agent_span():
27+
def pop_agent():
2828
# type: () -> None
29-
"""Pop an invoke_agent span from the stack."""
30-
stack = _invoke_agent_span_stack.get().copy()
29+
"""Pop an agent context from the stack."""
30+
stack = _agent_context_stack.get().copy()
3131
if stack:
3232
stack.pop()
33-
_invoke_agent_span_stack.set(stack)
34-
35-
36-
def get_current_invoke_agent_span():
37-
# type: () -> Optional[sentry_sdk.tracing.Span]
38-
"""Get the current invoke_agent span (top of stack)."""
39-
stack = _invoke_agent_span_stack.get()
40-
if stack:
41-
return stack[-1]["span"]
42-
return None
33+
_agent_context_stack.set(stack)
4334

4435

4536
def get_current_agent():
4637
# type: () -> Any
4738
"""Get the current agent from the contextvar stack."""
48-
stack = _invoke_agent_span_stack.get()
39+
stack = _agent_context_stack.get()
4940
if stack:
5041
return stack[-1]["agent"]
5142
return None
@@ -54,7 +45,7 @@ def get_current_agent():
5445
def get_is_streaming():
5546
# type: () -> bool
5647
"""Get the streaming flag from the contextvar stack."""
57-
stack = _invoke_agent_span_stack.get()
48+
stack = _agent_context_stack.get()
5849
if stack:
5950
return stack[-1].get("is_streaming", False)
6051
return False

0 commit comments

Comments
 (0)