Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d246712
feat(pydantic-ai): Add tool description to execute_tool spans
ericapisani Mar 5, 2026
b07537a
Merge branch 'master' into ep/add-tool-description-to-pydantic-ai-spans
ericapisani Mar 6, 2026
a136913
ref(pydantic-ai): Fix type annotation style in execute_tool_span
ericapisani Mar 6, 2026
352c03d
Merge branch 'ep/add-tool-description-to-pydantic-ai-spans' of github…
ericapisani Mar 6, 2026
a0bd0dc
test(pydantic-ai): Fix assertion for missing tool description
ericapisani Mar 6, 2026
b80dd97
fix(pydantic-ai): Safely access tool description attribute
ericapisani Mar 6, 2026
46743ae
Address CR comments
ericapisani Mar 6, 2026
bf85285
cleanups
ericapisani Mar 6, 2026
7a7f2bc
changes to make warden happy
ericapisani Mar 6, 2026
aee6675
test(pydantic-ai): Update test to verify tool description is omitted …
ericapisani Mar 9, 2026
2b7dca9
Claude forgot to include the functionality that required the test cha…
ericapisani Mar 9, 2026
3c9206d
Merge branch 'master' into ep/add-tool-description-to-pydantic-ai-spans
ericapisani Mar 9, 2026
2ae0c03
Switching to geetattr
ericapisani Mar 9, 2026
d204945
test(openai-agents): Remove test for unreachable state (#5584)
alexander-alderman-webb Mar 9, 2026
c2ce8bc
ref: Add experimental streaming API (5) (#5592)
sentrivana Mar 9, 2026
8a8427f
ref: Add new_trace, continue_trace to span first (6) (#5593)
sentrivana Mar 9, 2026
8ffcab9
test(openai-agents): Replace mocks with `httpx` in non-error single-r…
alexander-alderman-webb Mar 9, 2026
e8120f8
ref: Add streaming trace decorator (7) (#5594)
sentrivana Mar 9, 2026
f50264f
ci: Do not run actions on `potel-base` (#5614)
sentrivana Mar 9, 2026
f55c183
test(openai-agents): Replace mocks with `httpx` in API error test (#5…
alexander-alderman-webb Mar 9, 2026
ae8c320
test(openai-agents): Replace mocks with `httpx` in handoff tests (#5604)
alexander-alderman-webb Mar 9, 2026
056fc7c
test(openai-agents): Replace mocks with `httpx` in MCP tool tests (#5…
alexander-alderman-webb Mar 9, 2026
5a0e853
cleanup
ericapisani Mar 9, 2026
2f2db4c
Merge branch 'ep/add-tool-description-to-pydantic-ai-spans' of github…
ericapisani Mar 9, 2026
9f71684
Merge branch 'master' into ep/add-tool-description-to-pydantic-ai-spans
ericapisani Mar 9, 2026
597bb31
Remove unnecessary test
ericapisani Mar 9, 2026
241ba49
Merge branch 'ep/add-tool-description-to-pydantic-ai-spans' of github…
ericapisani Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sentry_sdk/integrations/pydantic_ai/patches/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ async def wrapped_execute_tool_call(
call = validated.call
name = call.tool_name
tool = self.tools.get(name) if self.tools else None
selected_tool_definition = getattr(tool, "tool_def", None)

# Determine tool type by checking tool.toolset
tool_type = "function"
Expand All @@ -73,6 +74,7 @@ async def wrapped_execute_tool_call(
args_dict,
agent,
tool_type=tool_type,
tool_definition=selected_tool_definition,
) as span:
try:
result = await original_execute_tool_call(
Expand Down Expand Up @@ -127,6 +129,7 @@ async def wrapped_call_tool(
# Extract tool info before calling original
name = call.tool_name
tool = self.tools.get(name) if self.tools else None
selected_tool_definition = getattr(tool, "tool_def", None)

# Determine tool type by checking tool.toolset
tool_type = "function" # default
Expand All @@ -150,6 +153,7 @@ async def wrapped_call_tool(
args_dict,
agent,
tool_type=tool_type,
tool_definition=selected_tool_definition,
) as span:
try:
result = await original_call_tool(
Expand Down
14 changes: 13 additions & 1 deletion sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@

if TYPE_CHECKING:
from typing import Any, Optional
from pydantic_ai._tool_manager import ToolDefinition # type: ignore


def execute_tool_span(
tool_name: str, tool_args: "Any", agent: "Any", tool_type: str = "function"
tool_name: str,
tool_args: "Any",
agent: "Any",
tool_type: str = "function",
tool_definition: "Optional[ToolDefinition]" = None,
) -> "sentry_sdk.tracing.Span":
"""Create a span for tool execution.

Expand All @@ -21,6 +26,7 @@ def execute_tool_span(
tool_args: The arguments passed to the tool
agent: The agent executing the tool
tool_type: The type of tool ("function" for regular tools, "mcp" for MCP services)
tool_definition: The definition of the tool, if available
"""
span = sentry_sdk.start_span(
op=OP.GEN_AI_EXECUTE_TOOL,
Expand All @@ -32,6 +38,12 @@ def execute_tool_span(
span.set_data(SPANDATA.GEN_AI_TOOL_TYPE, tool_type)
span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name)

if tool_definition is not None:
span.set_data(
SPANDATA.GEN_AI_TOOL_DESCRIPTION,
getattr(tool_definition, "description", None),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty description still set in span data

Medium Severity

The condition if tool_definition is not None and hasattr(tool_definition, "description") doesn't filter out empty descriptions. Pydantic-ai's ToolDefinition is a dataclass where description: str is a required field — for tools without docstrings, it's set to "". Since the attribute always exists, hasattr returns True and span.set_data is called with an empty string. This contradicts the intended behavior (and the corresponding test assertion) that the GEN_AI_TOOL_DESCRIPTION key be omitted when no meaningful description exists. The condition also needs to check that tool_definition.description is truthy.

Fix in Cursor Fix in Web


_set_agent_data(span, agent)

if _should_send_prompts() and tool_args is not None:
Expand Down
87 changes: 81 additions & 6 deletions tests/integrations/pydantic_ai/test_pydantic_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
from sentry_sdk.integrations.pydantic_ai import PydanticAIIntegration
from sentry_sdk.integrations.pydantic_ai.spans.ai_client import _set_input_messages
from sentry_sdk.integrations.pydantic_ai.spans.utils import _set_usage_data
from sentry_sdk.integrations.pydantic_ai.spans.execute_tool import execute_tool_span

from pydantic_ai import Agent
from pydantic_ai.messages import BinaryContent, UserPromptPart
from pydantic_ai.usage import RequestUsage
from pydantic_ai.models.test import TestModel
from pydantic_ai.exceptions import ModelRetry, UnexpectedModelBehavior
from pydantic_ai._tool_manager import ToolDefinition


@pytest.fixture
Expand Down Expand Up @@ -2360,7 +2361,6 @@
"""
import sentry_sdk
from sentry_sdk.integrations.pydantic_ai.spans.execute_tool import (
execute_tool_span,
update_execute_tool_span,
)

Expand All @@ -2386,7 +2386,6 @@
Test execute_tool span with MCP tool type.
"""
import sentry_sdk
from sentry_sdk.integrations.pydantic_ai.spans.execute_tool import execute_tool_span

sentry_init(
integrations=[PydanticAIIntegration()],
Expand All @@ -2411,7 +2410,6 @@
"""
import sentry_sdk
from sentry_sdk.integrations.pydantic_ai.spans.execute_tool import (
execute_tool_span,
update_execute_tool_span,
)

Expand All @@ -2437,7 +2435,6 @@
Test execute_tool span with None args.
"""
import sentry_sdk
from sentry_sdk.integrations.pydantic_ai.spans.execute_tool import execute_tool_span

sentry_init(
integrations=[PydanticAIIntegration()],
Expand Down Expand Up @@ -2482,9 +2479,8 @@
"""
import sentry_sdk
from sentry_sdk.integrations.pydantic_ai.spans.execute_tool import (
execute_tool_span,
update_execute_tool_span,
)

Check failure on line 2483 in tests/integrations/pydantic_ai/test_pydantic_ai.py

View workflow job for this annotation

GitHub Actions / warden: code-review

NameError: execute_tool_span is used but no longer imported

The import of `execute_tool_span` was removed from the import statement, but line 2493 still calls `execute_tool_span("test_tool", {"arg": "value"}, None, "function")`. This will cause a `NameError: name 'execute_tool_span' is not defined` at runtime when the test executes.

Check failure on line 2483 in tests/integrations/pydantic_ai/test_pydantic_ai.py

View workflow job for this annotation

GitHub Actions / warden: find-bugs

Test will fail with NameError due to missing execute_tool_span import

The diff removes the import of `execute_tool_span` from the import statement, but line 2493 still uses `execute_tool_span("test_tool", {"arg": "value"}, None, "function")`. This will cause a `NameError: name 'execute_tool_span' is not defined` when the test `test_update_execute_tool_span_with_none_result` runs.

sentry_init(
integrations=[PydanticAIIntegration()],
Expand Down Expand Up @@ -2794,3 +2790,82 @@
(span_data,) = event["spans"]
assert span_data["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 80
assert span_data["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20


@pytest.mark.asyncio
async def test_tool_description_in_execute_tool_span(sentry_init, capture_events):
"""
Test that tool description from the tool's docstring is included in execute_tool spans.
"""
agent = Agent(
"test",
name="test_agent",
system_prompt="You are a helpful test assistant.",
)

@agent.tool_plain
def multiply_numbers(a: int, b: int) -> int:
"""Multiply two numbers and return the product."""
return a * b

sentry_init(
integrations=[PydanticAIIntegration()],
traces_sample_rate=1.0,
send_default_pii=True,
)

events = capture_events()

result = await agent.run("What is 5 times 3?")
assert result is not None

(transaction,) = events
spans = transaction["spans"]

tool_spans = [s for s in spans if s["op"] == "gen_ai.execute_tool"]
assert len(tool_spans) >= 1

tool_span = tool_spans[0]
assert tool_span["data"]["gen_ai.tool.name"] == "multiply_numbers"
assert SPANDATA.GEN_AI_TOOL_DESCRIPTION in tool_span["data"]
assert "Multiply two numbers" in tool_span["data"][SPANDATA.GEN_AI_TOOL_DESCRIPTION]


@pytest.mark.asyncio
async def test_tool_without_description_omits_tool_description(
sentry_init, capture_events
):
"""
Test that execute_tool spans omit tool description when the tool has no docstring.
"""
agent = Agent(
"test",
name="test_agent",
system_prompt="You are a helpful test assistant.",
)

@agent.tool_plain
def no_docs_tool(a: int, b: int) -> int:
return a + b

sentry_init(
integrations=[PydanticAIIntegration()],
traces_sample_rate=1.0,
send_default_pii=True,
)

events = capture_events()

result = await agent.run("What is 5 + 3?")
assert result is not None

(transaction,) = events
spans = transaction["spans"]

tool_spans = [s for s in spans if s["op"] == "gen_ai.execute_tool"]
assert len(tool_spans) >= 1

tool_span = tool_spans[0]
assert tool_span["data"]["gen_ai.tool.name"] == "no_docs_tool"
assert SPANDATA.GEN_AI_TOOL_DESCRIPTION in tool_span["data"]
assert tool_span["data"][SPANDATA.GEN_AI_TOOL_DESCRIPTION] is None
Loading