diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index 8be5a97685..dd31703e1c 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -7,7 +7,6 @@ from sentry_sdk.utils import capture_internal_exceptions, reraise from ..spans import ( - handoff_span, invoke_agent_span, update_invoke_agent_span, ) @@ -212,16 +211,9 @@ async def _execute_handoffs( context_wrapper: "Optional[agents.RunContextWrapper]" = kwargs.get( "context_wrapper" ) - run_handoffs = kwargs.get("run_handoffs") # openai-agents >= 0.14 renamed `agent` to `public_agent`. agent: "Optional[agents.Agent]" = kwargs.get("public_agent", kwargs.get("agent")) - # Create Sentry handoff span for the first handoff (agents library only processes the first one) - if run_handoffs: - first_handoff = run_handoffs[0] - handoff_agent_name = first_handoff.handoff.agent_name - handoff_span(context_wrapper, agent, handoff_agent_name) - # Call original method with all parameters try: result = await original_execute_handoffs(*args, **kwargs) diff --git a/sentry_sdk/integrations/openai_agents/spans/__init__.py b/sentry_sdk/integrations/openai_agents/spans/__init__.py index 2675edab34..0b6ae0edd5 100644 --- a/sentry_sdk/integrations/openai_agents/spans/__init__.py +++ b/sentry_sdk/integrations/openai_agents/spans/__init__.py @@ -1,6 +1,5 @@ from .ai_client import ai_client_context, update_ai_client_span # noqa: F401 from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 -from .handoff import handoff_span # noqa: F401 from .invoke_agent import ( invoke_agent_span, # noqa: F401 update_invoke_agent_span, # noqa: F401 diff --git a/sentry_sdk/integrations/openai_agents/spans/handoff.py b/sentry_sdk/integrations/openai_agents/spans/handoff.py deleted file mode 100644 index 89efe2740e..0000000000 --- a/sentry_sdk/integrations/openai_agents/spans/handoff.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA - -from ..consts import SPAN_ORIGIN - -if TYPE_CHECKING: - from typing import Optional - - import agents - - -def handoff_span( - context: "Optional[agents.RunContextWrapper]", - from_agent: "Optional[agents.Agent]", - to_agent_name: str, -) -> None: - with sentry_sdk.start_span( - name=f"handoff from {from_agent.name} to {to_agent_name}", # type: ignore[union-attr] - attributes={ - "sentry.op": OP.GEN_AI_HANDOFF, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "handoff", - }, - ) as span: - # Add conversation ID from agent - conv_id = getattr(from_agent, "_sentry_conversation_id", None) - if conv_id: - span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index a793240e93..227001b3ec 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -16,7 +16,7 @@ Usage, ) from agents.computer import Computer -from agents.exceptions import MaxTurnsExceeded, ModelBehaviorError +from agents.exceptions import ModelBehaviorError from agents.items import ( ResponseFunctionToolCall, ResponseOutputMessage, @@ -1618,274 +1618,6 @@ def test_agent_invocation_span_sync( ) -@pytest.mark.asyncio -async def test_handoff_span( - sentry_init, - capture_items, - get_model_response, -): - """ - Test that handoff spans are created when agents hand off to other agents. - """ - client = AsyncOpenAI(api_key="test-key") - model = OpenAIResponsesModel(model="gpt-4-mini", openai_client=client) - - # Create two simple agents with a handoff relationship - secondary_agent = agents.Agent( - name="secondary_agent", - instructions="You are a secondary agent.", - model=model, - ) - - primary_agent = agents.Agent( - name="primary_agent", - instructions="You are a primary agent that hands off to secondary agent.", - model=model, - handoffs=[secondary_agent], - ) - - handoff_response = get_model_response( - Response( - id="resp_tool_123", - output=[ - ResponseFunctionToolCall( - id="call_handoff_123", - call_id="call_handoff_123", - name="transfer_to_secondary_agent", - type="function_call", - arguments="{}", - ) - ], - parallel_tool_calls=False, - tool_choice="none", - tools=[], - created_at=10000000, - model="gpt-4", - object="response", - usage=ResponseUsage( - input_tokens=10, - input_tokens_details=InputTokensDetails( - cached_tokens=0, - cache_write_tokens=0, - ), - output_tokens=20, - output_tokens_details=OutputTokensDetails( - reasoning_tokens=5, - ), - total_tokens=30, - ), - ), - serialize_pydantic=True, - ) - - final_response = get_model_response( - Response( - id="resp_final_123", - output=[ - ResponseOutputMessage( - id="msg_final", - type="message", - status="completed", - content=[ - ResponseOutputText( - text="I'm the specialist and I can help with that!", - type="output_text", - annotations=[], - ) - ], - role="assistant", - ) - ], - parallel_tool_calls=False, - tool_choice="none", - tools=[], - created_at=10000000, - model="gpt-4", - object="response", - usage=ResponseUsage( - input_tokens=10, - input_tokens_details=InputTokensDetails( - cached_tokens=0, - cache_write_tokens=0, - ), - output_tokens=20, - output_tokens_details=OutputTokensDetails( - reasoning_tokens=5, - ), - total_tokens=30, - ), - ), - serialize_pydantic=True, - ) - with patch.object( - primary_agent.model._client._client, - "send", - side_effect=[handoff_response, final_response], - ) as _: - sentry_init( - integrations=[OpenAIAgentsIntegration()], - disabled_integrations=[StdlibIntegration], - traces_sample_rate=1.0, - ) - - items = capture_items("span") - - result = await agents.Runner.run( - primary_agent, - "Please hand off to secondary agent", - run_config=test_run_config, - ) - - assert result is not None - - sentry_sdk.flush() - spans = [item.payload for item in items] - handoff_span = next( - span - for span in spans - if span["attributes"].get("sentry.op") == OP.GEN_AI_HANDOFF - ) - - # Verify handoff span was created - assert handoff_span is not None - assert handoff_span["name"] == "handoff from primary_agent to secondary_agent" - assert handoff_span["attributes"]["gen_ai.operation.name"] == "handoff" - - -@pytest.mark.asyncio -async def test_max_turns_before_handoff_span( - sentry_init, - capture_items, - get_model_response, -): - """ - Example raising agents.exceptions.AgentsException after the agent invocation span is complete. - """ - client = AsyncOpenAI(api_key="test-key") - model = OpenAIResponsesModel(model="gpt-4-mini", openai_client=client) - - # Create two simple agents with a handoff relationship - secondary_agent = agents.Agent( - name="secondary_agent", - instructions="You are a secondary agent.", - model=model, - ) - - primary_agent = agents.Agent( - name="primary_agent", - instructions="You are a primary agent that hands off to secondary agent.", - model=model, - handoffs=[secondary_agent], - ) - - handoff_response = get_model_response( - Response( - id="resp_tool_123", - output=[ - ResponseFunctionToolCall( - id="call_handoff_123", - call_id="call_handoff_123", - name="transfer_to_secondary_agent", - type="function_call", - arguments="{}", - ) - ], - parallel_tool_calls=False, - tool_choice="none", - tools=[], - created_at=10000000, - model="gpt-4", - object="response", - usage=ResponseUsage( - input_tokens=10, - input_tokens_details=InputTokensDetails( - cached_tokens=0, - cache_write_tokens=0, - ), - output_tokens=20, - output_tokens_details=OutputTokensDetails( - reasoning_tokens=5, - ), - total_tokens=30, - ), - ), - serialize_pydantic=True, - ) - - final_response = get_model_response( - Response( - id="resp_final_123", - output=[ - ResponseOutputMessage( - id="msg_final", - type="message", - status="completed", - content=[ - ResponseOutputText( - text="I'm the specialist and I can help with that!", - type="output_text", - annotations=[], - ) - ], - role="assistant", - ) - ], - parallel_tool_calls=False, - tool_choice="none", - tools=[], - created_at=10000000, - model="gpt-4", - object="response", - usage=ResponseUsage( - input_tokens=10, - input_tokens_details=InputTokensDetails( - cached_tokens=0, - cache_write_tokens=0, - ), - output_tokens=20, - output_tokens_details=OutputTokensDetails( - reasoning_tokens=5, - ), - total_tokens=30, - ), - ), - serialize_pydantic=True, - ) - with patch.object( - primary_agent.model._client._client, - "send", - side_effect=[handoff_response, final_response], - ) as _: - sentry_init( - integrations=[OpenAIAgentsIntegration()], - disabled_integrations=[StdlibIntegration], - traces_sample_rate=1.0, - ) - - items = capture_items("span") - - with pytest.raises(MaxTurnsExceeded): - await agents.Runner.run( - primary_agent, - "Please hand off to secondary agent", - run_config=test_run_config, - max_turns=1, - ) - - sentry_sdk.flush() - spans = [item.payload for item in items] - handoff_span = next( - span - for span in spans - if span["attributes"].get("sentry.op") == OP.GEN_AI_HANDOFF - ) - - # Verify handoff span was created - assert handoff_span is not None - assert handoff_span["name"] == "handoff from primary_agent to secondary_agent" - assert handoff_span["attributes"]["gen_ai.operation.name"] == "handoff" - - @pytest.mark.parametrize("user_hooks", [True, False]) @pytest.mark.asyncio async def test_tool_execution_span(