diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py index 65358732d3..590fb48ddc 100644 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ b/sentry_sdk/integrations/openai_agents/__init__.py @@ -8,6 +8,7 @@ _get_all_tools, _run_single_turn, _run_single_turn_streamed, + _execute_handoffs, _create_run_wrapper, _create_run_streamed_wrapper, _patch_agent_run, @@ -32,10 +33,11 @@ try: # AgentRunner methods moved in v0.8 # https://github.com/openai/openai-agents-python/commit/3ce7c24d349b77bb750062b7e0e856d9ff48a5d5#diff-7470b3a5c5cbe2fcbb2703dc24f326f45a5819d853be2b1f395d122d278cd911 - from agents.run_internal import run_loop, turn_preparation + from agents.run_internal import run_loop, turn_preparation, turn_resolution except ImportError: run_loop = None turn_preparation = None + turn_resolution = None from typing import TYPE_CHECKING @@ -86,7 +88,7 @@ class OpenAIAgentsIntegration(Integration): Hosted MCP Tools are run as part of the Responses API call, and involve OpenAI reaching out to an external MCP server. An agent can handoff to another agent, also directed by the return value of the Responses API and run post-API call in the loop. Handoffs are a way to switch agent-wide configuration. - - Handoffs are executed by calling `RunImpl.execute_handoffs()`. The method is patched in `patched_execute_handoffs()` + - Handoffs are executed by calling `RunImpl.execute_handoffs()`. The method is patched with `patches._execute_handoffs()` """ identifier = "openai_agents" @@ -139,6 +141,20 @@ async def new_wrapped_run_single_turn_streamed( agents.run.run_single_turn_streamed = new_wrapped_run_single_turn_streamed + original_execute_handoffs = turn_resolution.execute_handoffs + + @wraps(original_execute_handoffs) + async def new_wrapped_execute_handoffs( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_handoffs( + original_execute_handoffs, *args, **kwargs + ) + + agents.run_internal.turn_resolution.execute_handoffs = ( + new_wrapped_execute_handoffs + ) + return original_get_all_tools = AgentRunner._get_all_tools @@ -188,3 +204,15 @@ async def old_wrapped_run_single_turn_streamed( agents.run.AgentRunner._run_single_turn_streamed = classmethod( old_wrapped_run_single_turn_streamed ) + + original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs + + @wraps(agents._run_impl.RunImpl.execute_handoffs.__func__) + async def old_wrapped_execute_handoffs( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_handoffs(original_execute_handoffs, *args, **kwargs) + + agents._run_impl.RunImpl.execute_handoffs = classmethod( + old_wrapped_execute_handoffs + ) diff --git a/sentry_sdk/integrations/openai_agents/patches/__init__.py b/sentry_sdk/integrations/openai_agents/patches/__init__.py index 0c06d96cc4..ce2ba58d85 100644 --- a/sentry_sdk/integrations/openai_agents/patches/__init__.py +++ b/sentry_sdk/integrations/openai_agents/patches/__init__.py @@ -1,5 +1,10 @@ from .models import _get_model # noqa: F401 from .tools import _get_all_tools # noqa: F401 from .runner import _create_run_wrapper, _create_run_streamed_wrapper # noqa: F401 -from .agent_run import _run_single_turn, _run_single_turn_streamed, _patch_agent_run # noqa: F401 +from .agent_run import ( + _run_single_turn, + _run_single_turn_streamed, + _execute_handoffs, + _patch_agent_run, +) # noqa: F401 from .error_tracing import _patch_error_tracing # noqa: F401 diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index 01812604e1..3c5fd6b623 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -166,55 +166,52 @@ async def _run_single_turn_streamed( return result -def _patch_agent_run() -> None: +async def _execute_handoffs( + original_execute_handoffs: "Callable[..., SingleStepResult]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": """ - Patches AgentRunner methods to create agent invocation spans. - This directly patches the execution flow to track when agents start and stop. + Patched execute_handoffs that + - creates and manages handoff spans. + - ends the agent invocation span. + - ends the workflow span if the response is streamed and an exception is raised in `execute_handoffs()`. """ - # Store original methods - original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs - original_execute_final_output = agents._run_impl.RunImpl.execute_final_output + context_wrapper = kwargs.get("context_wrapper") + run_handoffs = kwargs.get("run_handoffs") + agent = kwargs.get("agent") - @wraps( - original_execute_handoffs.__func__ - if hasattr(original_execute_handoffs, "__func__") - else original_execute_handoffs - ) - async def patched_execute_handoffs( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "Any": - """ - Patched execute_handoffs that - - creates and manages handoff spans. - - ends the agent invocation span. - - ends the workflow span if the response is streamed and an exception is raised in `execute_handoffs()`. - """ + # 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) - context_wrapper = kwargs.get("context_wrapper") - run_handoffs = kwargs.get("run_handoffs") - agent = kwargs.get("agent") + # Call original method with all parameters + try: + result = await original_execute_handoffs(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _close_streaming_workflow_span(agent) + reraise(*exc_info) + finally: + # End span for current agent after handoff processing is complete + if agent and context_wrapper and _has_active_agent_span(context_wrapper): + end_invoke_agent_span(context_wrapper, 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) + return result - # Call original method with all parameters - try: - result = await original_execute_handoffs(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _close_streaming_workflow_span(agent) - reraise(*exc_info) - finally: - # End span for current agent after handoff processing is complete - if agent and context_wrapper and _has_active_agent_span(context_wrapper): - end_invoke_agent_span(context_wrapper, agent) - return result +def _patch_agent_run() -> None: + """ + Patches AgentRunner methods to create agent invocation spans. + This directly patches the execution flow to track when agents start and stop. + """ + + # Store original methods + original_execute_final_output = agents._run_impl.RunImpl.execute_final_output @wraps( original_execute_final_output.__func__ @@ -250,7 +247,6 @@ async def patched_execute_final_output( return result # Apply patches - agents._run_impl.RunImpl.execute_handoffs = classmethod(patched_execute_handoffs) agents._run_impl.RunImpl.execute_final_output = classmethod( patched_execute_final_output )