From 44f1ddb5f496daead1fa14a4c886fc511a2bb705 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 9 Sep 2026 15:21:45 +0200 Subject: [PATCH 1/2] fix(pydantic-ai): Stop capturing tool execution spans when validation fails --- .../integrations/pydantic_ai/__init__.py | 35 +++++++++++++++++++ .../integrations/pydantic_ai/patches/tools.py | 18 ++++++---- .../pydantic_ai/test_pydantic_ai.py | 29 --------------- 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/sentry_sdk/integrations/pydantic_ai/__init__.py b/sentry_sdk/integrations/pydantic_ai/__init__.py index a3215aa284..a93e3e66aa 100644 --- a/sentry_sdk/integrations/pydantic_ai/__init__.py +++ b/sentry_sdk/integrations/pydantic_ai/__init__.py @@ -51,6 +51,41 @@ async def on_model_request( update_ai_client_span(span, response) return response + # @hooks.on.tool_execute + # async def sentry_wrap_tool_execute( + # ctx: "RunContext[Any]", + # *, + # call: "ToolCallPart", + # tool_def: "ToolDefinition", + # args: "ValidatedToolArgs", + # handler: "WrapToolExecuteHandler", + # ) -> "Any": + # with execute_tool_span( + # call.tool_name, + # args, + # ctx.agent, + # tool_definition=tool_def, + # ) as span: + # try: + # result = await handler(args) + # update_execute_tool_span(span, result) + # return result + # except ToolRetryError as exc: + # exc_info = sys.exc_info() + # with capture_internal_exceptions(): + # from sentry_sdk.integrations.pydantic_ai import ( + # PydanticAIIntegration, + # ) + # integration = sentry_sdk.get_client().get_integration( + # PydanticAIIntegration, + # ) + # if ( + # integration is not None + # and integration.handled_tool_call_exceptions + # ): + # _capture_exception(exc, handled=True) + # reraise(*exc_info) + original_init = Agent.__init__ @functools.wraps(original_init) diff --git a/sentry_sdk/integrations/pydantic_ai/patches/tools.py b/sentry_sdk/integrations/pydantic_ai/patches/tools.py index 958d729fbb..a88e1d5712 100644 --- a/sentry_sdk/integrations/pydantic_ai/patches/tools.py +++ b/sentry_sdk/integrations/pydantic_ai/patches/tools.py @@ -1,4 +1,5 @@ import sys +from contextlib import nullcontext from functools import wraps from typing import TYPE_CHECKING @@ -60,11 +61,15 @@ async def wrapped_execute_tool_call( # Create execute_tool span # Nesting is handled by isolation_scope() to ensure proper parent-child relationships with sentry_sdk.isolation_scope(): - with execute_tool_span( - name, - args_dict, - agent, - tool_definition=selected_tool_definition, + with ( + execute_tool_span( + name, + args_dict, + agent, + tool_definition=selected_tool_definition, + ) + if validated.args_valid + else nullcontext() ) as span: try: result = await original_execute_tool_call( @@ -73,7 +78,8 @@ async def wrapped_execute_tool_call( *args, **kwargs, ) - update_execute_tool_span(span, result) + if span is not None: + update_execute_tool_span(span, result) return result except ToolRetryError as exc: exc_info = sys.exc_info() diff --git a/tests/integrations/pydantic_ai/test_pydantic_ai.py b/tests/integrations/pydantic_ai/test_pydantic_ai.py index 03f468e497..b862a11dfa 100644 --- a/tests/integrations/pydantic_ai/test_pydantic_ai.py +++ b/tests/integrations/pydantic_ai/test_pydantic_ai.py @@ -1022,24 +1022,6 @@ def add_numbers(a: Annotated[int, Field(gt=0, lt=0)], b: int) -> int: chat_spans = [ s for s in spans if s["attributes"].get("sentry.op", "") == "gen_ai.chat" ] - tool_spans = [ - s - for s in spans - if s["attributes"].get("sentry.op", "") == "gen_ai.execute_tool" - ] - - # Should have tool spans - assert len(tool_spans) >= 1 - - # Check tool spans - model_retry_tool_span = tool_spans[0] - assert "execute_tool" in model_retry_tool_span["name"] - assert ( - model_retry_tool_span["attributes"]["gen_ai.operation.name"] - == "execute_tool" - ) - assert model_retry_tool_span["attributes"]["gen_ai.tool.name"] == "add_numbers" - assert "gen_ai.tool.input" in model_retry_tool_span["attributes"] # Check chat spans have available_tools assert "gen_ai.request.available_tools" in chat_spans[0]["attributes"] @@ -1073,17 +1055,6 @@ def add_numbers(a: Annotated[int, Field(gt=0, lt=0)], b: int) -> int: # Find child span types (invoke_agent is the transaction, not a child span) chat_spans = [s for s in spans if s["op"] == "gen_ai.chat"] - tool_spans = [s for s in spans if s["op"] == "gen_ai.execute_tool"] - - # Should have tool spans - assert len(tool_spans) >= 1 - - # Check tool spans - model_retry_tool_span = tool_spans[0] - assert "execute_tool" in model_retry_tool_span["description"] - assert model_retry_tool_span["data"]["gen_ai.operation.name"] == "execute_tool" - assert model_retry_tool_span["data"]["gen_ai.tool.name"] == "add_numbers" - assert "gen_ai.tool.input" in model_retry_tool_span["data"] # Check chat spans have available_tools assert "gen_ai.request.available_tools" in chat_spans[0]["data"] From 1fc035d10c3275173605d4e6be7cd153a071c2e2 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 9 Sep 2026 15:22:24 +0200 Subject: [PATCH 2/2] . --- .../integrations/pydantic_ai/__init__.py | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/sentry_sdk/integrations/pydantic_ai/__init__.py b/sentry_sdk/integrations/pydantic_ai/__init__.py index a93e3e66aa..a3215aa284 100644 --- a/sentry_sdk/integrations/pydantic_ai/__init__.py +++ b/sentry_sdk/integrations/pydantic_ai/__init__.py @@ -51,41 +51,6 @@ async def on_model_request( update_ai_client_span(span, response) return response - # @hooks.on.tool_execute - # async def sentry_wrap_tool_execute( - # ctx: "RunContext[Any]", - # *, - # call: "ToolCallPart", - # tool_def: "ToolDefinition", - # args: "ValidatedToolArgs", - # handler: "WrapToolExecuteHandler", - # ) -> "Any": - # with execute_tool_span( - # call.tool_name, - # args, - # ctx.agent, - # tool_definition=tool_def, - # ) as span: - # try: - # result = await handler(args) - # update_execute_tool_span(span, result) - # return result - # except ToolRetryError as exc: - # exc_info = sys.exc_info() - # with capture_internal_exceptions(): - # from sentry_sdk.integrations.pydantic_ai import ( - # PydanticAIIntegration, - # ) - # integration = sentry_sdk.get_client().get_integration( - # PydanticAIIntegration, - # ) - # if ( - # integration is not None - # and integration.handled_tool_call_exceptions - # ): - # _capture_exception(exc, handled=True) - # reraise(*exc_info) - original_init = Agent.__init__ @functools.wraps(original_init)