From 9f2c047e8f6326d7451efb53c836a545b08d733c Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 27 Nov 2025 13:12:55 +0100 Subject: [PATCH 01/20] fix(openai-agents): Store invoke_agent span on agents.RunContextWrapper --- sentry_sdk/integrations/openai_agents/patches/agent_run.py | 3 ++- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index 5473915b48..b25bf82ad5 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -31,7 +31,8 @@ def _start_invoke_agent_span(context_wrapper, agent, kwargs): """Start an agent invocation span""" # Store the agent on the context wrapper so we can access it later context_wrapper._sentry_current_agent = agent - invoke_agent_span(context_wrapper, agent, kwargs) + span = invoke_agent_span(context_wrapper, agent, kwargs) + context_wrapper._sentry_agent_span = span def _end_invoke_agent_span(context_wrapper, agent, output=None): # type: (agents.RunContextWrapper, agents.Agent, Optional[Any]) -> None diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py index 2a9c5ebe66..35b83b3e0d 100644 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -75,7 +75,7 @@ def invoke_agent_span(context, agent, kwargs): def update_invoke_agent_span(context, agent, output): # type: (agents.RunContextWrapper, agents.Agent, Any) -> None - span = sentry_sdk.get_current_span() + span = context._sentry_agent_span if span: if should_send_default_pii(): From 4ca61e6aa627ebbf52fd3dcf9d0b356beb338957 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 27 Nov 2025 13:36:28 +0100 Subject: [PATCH 02/20] be defensive when accessing the agent span --- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py index 35b83b3e0d..c221070ae8 100644 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -75,7 +75,7 @@ def invoke_agent_span(context, agent, kwargs): def update_invoke_agent_span(context, agent, output): # type: (agents.RunContextWrapper, agents.Agent, Any) -> None - span = context._sentry_agent_span + span = getattr(context, "_sentry_agent_span", None) if span: if should_send_default_pii(): From 2c0edd5b2a9067b5063eee564629f815d3c664dc Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 09:25:50 +0100 Subject: [PATCH 03/20] fix(openai-agents): Avoid double span exit on exception --- .../openai_agents/patches/agent_run.py | 27 ++++++++++++------- .../openai_agents/patches/runner.py | 26 +++++++++--------- .../integrations/openai_agents/utils.py | 15 +++++++++++ 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index b25bf82ad5..bde41d2268 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -1,13 +1,17 @@ from functools import wraps from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.tracing_utils import set_span_errored from ..spans import invoke_agent_span, update_invoke_agent_span, handoff_span +from ..utils import _capture_exception, _record_exception_on_span from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any, Optional + from sentry_sdk.tracing import Span + try: import agents except ImportError: @@ -27,13 +31,15 @@ def _patch_agent_run(): original_execute_final_output = agents._run_impl.RunImpl.execute_final_output def _start_invoke_agent_span(context_wrapper, agent, kwargs): - # type: (agents.RunContextWrapper, agents.Agent, dict[str, Any]) -> None + # type: (agents.RunContextWrapper, agents.Agent, dict[str, Any]) -> Span """Start an agent invocation span""" # Store the agent on the context wrapper so we can access it later context_wrapper._sentry_current_agent = agent span = invoke_agent_span(context_wrapper, agent, kwargs) context_wrapper._sentry_agent_span = span + return span + def _end_invoke_agent_span(context_wrapper, agent, output=None): # type: (agents.RunContextWrapper, agents.Agent, Optional[Any]) -> None """End the agent invocation span""" @@ -65,18 +71,21 @@ async def patched_run_single_turn(cls, *args, **kwargs): context_wrapper = kwargs.get("context_wrapper") should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks") + span = getattr(context_wrapper, "_sentry_current_agent", None) # Start agent span when agent starts (but only once per agent) if should_run_agent_start_hooks and agent and context_wrapper: - # End any existing span for a different agent - if _has_active_agent_span(context_wrapper): - current_agent = _get_current_agent(context_wrapper) - if current_agent and current_agent != agent: - _end_invoke_agent_span(context_wrapper, current_agent) - - _start_invoke_agent_span(context_wrapper, agent, kwargs) + span = _start_invoke_agent_span(context_wrapper, agent, kwargs) # Call original method with all the correct parameters - result = await original_run_single_turn(*args, **kwargs) + try: + result = await original_run_single_turn(*args, **kwargs) + except Exception as exc: + if span is not None and span.timestamp is None: + _record_exception_on_span(span, exc) + span.__exit__(None, None, None) + + _capture_exception(exc) + raise exc from None return result diff --git a/sentry_sdk/integrations/openai_agents/patches/runner.py b/sentry_sdk/integrations/openai_agents/patches/runner.py index 745f30a38e..00323da3e3 100644 --- a/sentry_sdk/integrations/openai_agents/patches/runner.py +++ b/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -28,18 +28,18 @@ async def wrapper(*args, **kwargs): with sentry_sdk.isolation_scope(): agent = args[0] with agent_workflow_span(agent): - result = None - try: - result = await original_func(*args, **kwargs) - return result - except Exception as exc: - _capture_exception(exc) - - # It could be that there is a "invoke agent" span still open - current_span = sentry_sdk.get_current_span() - if current_span is not None and current_span.timestamp is None: - current_span.__exit__(None, None, None) - - raise exc from None + run_result = await original_func(*args, **kwargs) + + invoke_agent_span = getattr( + run_result.context_wrapper, "_sentry_agent_span", None + ) + + if ( + invoke_agent_span is not None + and invoke_agent_span.timestamp is None + ): + invoke_agent_span.__exit__(None, None, None) + + return run_result return wrapper diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index cc7c38553e..e54e4bf3da 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -36,6 +36,21 @@ def _capture_exception(exc): sentry_sdk.capture_event(event, hint=hint) +def _record_exception_on_span(span, error): + # type: (Optional[Span], Exception) -> Any + if span is not None: + set_span_errored(span) + span.set_data("span.status", "error") + + # Optionally capture the error details if we have them + if hasattr(error, "__class__"): + span.set_data("error.type", error.__class__.__name__) + if hasattr(error, "__str__"): + error_message = str(error) + if error_message: + span.set_data("error.message", error_message) + + def _set_agent_data(span, agent): # type: (sentry_sdk.tracing.Span, agents.Agent) -> None span.set_data( From 5a70ca065227848eb52b06b147767f5e405eddca Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 09:44:33 +0100 Subject: [PATCH 04/20] restore end existing span --- sentry_sdk/integrations/openai_agents/patches/agent_run.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index bde41d2268..7323fe2239 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -74,6 +74,12 @@ async def patched_run_single_turn(cls, *args, **kwargs): span = getattr(context_wrapper, "_sentry_current_agent", None) # Start agent span when agent starts (but only once per agent) if should_run_agent_start_hooks and agent and context_wrapper: + # End any existing span for a different agent + if _has_active_agent_span(context_wrapper): + current_agent = _get_current_agent(context_wrapper) + if current_agent and current_agent != agent: + _end_invoke_agent_span(context_wrapper, current_agent) + span = _start_invoke_agent_span(context_wrapper, agent, kwargs) # Call original method with all the correct parameters From baa1b59aa2c9a0aafb8d80e03b5959bafe183024 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 09:49:02 +0100 Subject: [PATCH 05/20] mypy --- sentry_sdk/integrations/openai_agents/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index e54e4bf3da..8e0350bd30 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -14,9 +14,11 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any + from typing import Any, Optional from agents import Usage + from sentry_sdk.tracing import Span + try: import agents From e6e40b1d78b953f91f361aa45107b4096c5a2863 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 09:58:10 +0100 Subject: [PATCH 06/20] access correct attribute --- sentry_sdk/integrations/openai_agents/patches/agent_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index 7323fe2239..3d52adaf2b 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -71,7 +71,7 @@ async def patched_run_single_turn(cls, *args, **kwargs): context_wrapper = kwargs.get("context_wrapper") should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks") - span = getattr(context_wrapper, "_sentry_current_agent", None) + span = getattr(context_wrapper, "_sentry_agent_span", None) # Start agent span when agent starts (but only once per agent) if should_run_agent_start_hooks and agent and context_wrapper: # End any existing span for a different agent From cb23da002d28a9f94667bf0be64e3db2a62fb185 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 10:12:45 +0100 Subject: [PATCH 07/20] deduplicate --- .../openai_agents/patches/error_tracing.py | 12 ++------- .../integrations/openai_agents/utils.py | 25 +++++++++---------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/error_tracing.py b/sentry_sdk/integrations/openai_agents/patches/error_tracing.py index 7d145267fc..2695f8a753 100644 --- a/sentry_sdk/integrations/openai_agents/patches/error_tracing.py +++ b/sentry_sdk/integrations/openai_agents/patches/error_tracing.py @@ -3,6 +3,7 @@ import sentry_sdk from sentry_sdk.consts import SPANSTATUS from sentry_sdk.tracing_utils import set_span_errored +from ..utils import _record_exception_on_span from typing import TYPE_CHECKING @@ -58,16 +59,7 @@ def sentry_attach_error_to_current_span(error, *args, **kwargs): # Set the current Sentry span to errored current_span = sentry_sdk.get_current_span() if current_span is not None: - set_span_errored(current_span) - current_span.set_data("span.status", "error") - - # Optionally capture the error details if we have them - if hasattr(error, "__class__"): - current_span.set_data("error.type", error.__class__.__name__) - if hasattr(error, "__str__"): - error_message = str(error) - if error_message: - current_span.set_data("error.message", error_message) + _record_exception_on_span(current_span, error) # Call the original function return original_attach_error(error, *args, **kwargs) diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index 8e0350bd30..0c471eec9a 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -14,7 +14,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Optional + from typing import Any from agents import Usage from sentry_sdk.tracing import Span @@ -39,18 +39,17 @@ def _capture_exception(exc): def _record_exception_on_span(span, error): - # type: (Optional[Span], Exception) -> Any - if span is not None: - set_span_errored(span) - span.set_data("span.status", "error") - - # Optionally capture the error details if we have them - if hasattr(error, "__class__"): - span.set_data("error.type", error.__class__.__name__) - if hasattr(error, "__str__"): - error_message = str(error) - if error_message: - span.set_data("error.message", error_message) + # type: (Span, Exception) -> Any + set_span_errored(span) + span.set_data("span.status", "error") + + # Optionally capture the error details if we have them + if hasattr(error, "__class__"): + span.set_data("error.type", error.__class__.__name__) + if hasattr(error, "__str__"): + error_message = str(error) + if error_message: + span.set_data("error.message", error_message) def _set_agent_data(span, agent): From bc982ed32ec320303b155a580411b0ebe01de7b5 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 10:50:00 +0100 Subject: [PATCH 08/20] delattr on exit --- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py index c221070ae8..63cd10d55e 100644 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -84,3 +84,4 @@ def update_invoke_agent_span(context, agent, output): ) span.__exit__(None, None, None) + delattr(context, "_sentry_agent_span") From 557fc90a3a28994e543e2ec6427b5a933f4b8ff0 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 10:52:01 +0100 Subject: [PATCH 09/20] delattr on exit --- sentry_sdk/integrations/openai_agents/patches/agent_run.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index 3d52adaf2b..77773f1cd8 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -89,6 +89,7 @@ async def patched_run_single_turn(cls, *args, **kwargs): if span is not None and span.timestamp is None: _record_exception_on_span(span, exc) span.__exit__(None, None, None) + delattr(context_wrapper, "_sentry_agent_span") _capture_exception(exc) raise exc from None From dd3063bdef85e7ccbf34b200bbc487a60b3d001c Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 11:18:34 +0100 Subject: [PATCH 10/20] call _end_invoke_agent_span instead of manually closing span --- sentry_sdk/integrations/openai_agents/patches/agent_run.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index 77773f1cd8..eb3af909d3 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -88,8 +88,7 @@ async def patched_run_single_turn(cls, *args, **kwargs): except Exception as exc: if span is not None and span.timestamp is None: _record_exception_on_span(span, exc) - span.__exit__(None, None, None) - delattr(context_wrapper, "_sentry_agent_span") + _end_invoke_agent_span(context_wrapper, agent) _capture_exception(exc) raise exc from None From e5d5c5288db57aa66f5ddae8977980414e9cc1d2 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 12:48:39 +0100 Subject: [PATCH 11/20] add except block --- .../openai_agents/patches/runner.py | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/runner.py b/sentry_sdk/integrations/openai_agents/patches/runner.py index 00323da3e3..f1a50fbd87 100644 --- a/sentry_sdk/integrations/openai_agents/patches/runner.py +++ b/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -1,9 +1,15 @@ from functools import wraps import sentry_sdk +from sentry_sdk.integrations import DidNotEnable -from ..spans import agent_workflow_span -from ..utils import _capture_exception +from ..spans import agent_workflow_span, end_invoke_agent_span +from ..utils import _capture_exception, _record_exception_on_span + +try: + from agents.exceptions import AgentsException +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") from typing import TYPE_CHECKING @@ -28,18 +34,26 @@ async def wrapper(*args, **kwargs): with sentry_sdk.isolation_scope(): agent = args[0] with agent_workflow_span(agent): - run_result = await original_func(*args, **kwargs) - - invoke_agent_span = getattr( - run_result.context_wrapper, "_sentry_agent_span", None - ) - - if ( - invoke_agent_span is not None - and invoke_agent_span.timestamp is None - ): - invoke_agent_span.__exit__(None, None, None) - + try: + run_result = await original_func(*args, **kwargs) + except AgentsException as exc: + context_wrapper = getattr(exc.run_data, "context_wrapper", None) + if context_wrapper is not None: + invoke_agent_span = getattr( + context_wrapper, "_sentry_agent_span", None + ) + + if ( + invoke_agent_span is not None + and invoke_agent_span.timestamp is None + ): + _record_exception_on_span(invoke_agent_span, exc) + end_invoke_agent_span(context_wrapper, agent) + + _capture_exception(exc) + raise exc from None + + end_invoke_agent_span(run_result.context_wrapper, agent) return run_result return wrapper From e738f3d2b5e1663f6d9e3d69a968b7095a998fd6 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 12:51:31 +0100 Subject: [PATCH 12/20] move end_invoke_agent_span --- .../integrations/openai_agents/patches/agent_run.py | 11 ++++++++--- .../integrations/openai_agents/spans/invoke_agent.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index eb3af909d3..80cd222b45 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -2,7 +2,12 @@ from sentry_sdk.integrations import DidNotEnable from sentry_sdk.tracing_utils import set_span_errored -from ..spans import invoke_agent_span, update_invoke_agent_span, handoff_span +from ..spans import ( + invoke_agent_span, + update_invoke_agent_span, + end_invoke_agent_span, + handoff_span, +) from ..utils import _capture_exception, _record_exception_on_span from typing import TYPE_CHECKING @@ -78,7 +83,7 @@ async def patched_run_single_turn(cls, *args, **kwargs): if _has_active_agent_span(context_wrapper): current_agent = _get_current_agent(context_wrapper) if current_agent and current_agent != agent: - _end_invoke_agent_span(context_wrapper, current_agent) + end_invoke_agent_span(context_wrapper, current_agent) span = _start_invoke_agent_span(context_wrapper, agent, kwargs) @@ -88,7 +93,7 @@ async def patched_run_single_turn(cls, *args, **kwargs): except Exception as exc: if span is not None and span.timestamp is None: _record_exception_on_span(span, exc) - _end_invoke_agent_span(context_wrapper, agent) + end_invoke_agent_span(context_wrapper, agent) _capture_exception(exc) raise exc from None diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py index 63cd10d55e..38b6a98a3f 100644 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -85,3 +85,13 @@ def update_invoke_agent_span(context, agent, output): span.__exit__(None, None, None) delattr(context, "_sentry_agent_span") + + +def end_invoke_agent_span(context_wrapper, agent, output=None): + # type: (agents.RunContextWrapper, agents.Agent, Optional[Any]) -> None + """End the agent invocation span""" + # Clear the stored agent + if hasattr(context_wrapper, "_sentry_current_agent"): + delattr(context_wrapper, "_sentry_current_agent") + + update_invoke_agent_span(context_wrapper, agent, output) From 64c2cfa915a6a3cce8cf9720a17af7e0a3ba0990 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 12:54:57 +0100 Subject: [PATCH 13/20] forgot __init__.py --- sentry_sdk/integrations/openai_agents/spans/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/openai_agents/spans/__init__.py b/sentry_sdk/integrations/openai_agents/spans/__init__.py index 3bc453cafa..64b979fc25 100644 --- a/sentry_sdk/integrations/openai_agents/spans/__init__.py +++ b/sentry_sdk/integrations/openai_agents/spans/__init__.py @@ -2,4 +2,8 @@ from .ai_client import ai_client_span, 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, update_invoke_agent_span # noqa: F401 +from .invoke_agent import ( + invoke_agent_span, + update_invoke_agent_span, + end_invoke_agent_span, +) # noqa: F401 From cea38a27f39976a36d3fd81a1181267ce9a92eb2 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 12:58:35 +0100 Subject: [PATCH 14/20] mypy --- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py index 38b6a98a3f..1c69ef8bfe 100644 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: import agents - from typing import Any + from typing import Any, Optional def invoke_agent_span(context, agent, kwargs): From 90f5dbae9ea3b16a9a77c0816be20cf25aeffa8a Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 13:25:20 +0100 Subject: [PATCH 15/20] capture_exception first --- sentry_sdk/integrations/openai_agents/patches/agent_run.py | 3 ++- sentry_sdk/integrations/openai_agents/patches/runner.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index 80cd222b45..2bd921b690 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -91,11 +91,12 @@ async def patched_run_single_turn(cls, *args, **kwargs): try: result = await original_run_single_turn(*args, **kwargs) except Exception as exc: + _capture_exception(exc) + if span is not None and span.timestamp is None: _record_exception_on_span(span, exc) end_invoke_agent_span(context_wrapper, agent) - _capture_exception(exc) raise exc from None return result diff --git a/sentry_sdk/integrations/openai_agents/patches/runner.py b/sentry_sdk/integrations/openai_agents/patches/runner.py index f1a50fbd87..671f83dcab 100644 --- a/sentry_sdk/integrations/openai_agents/patches/runner.py +++ b/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -37,6 +37,8 @@ async def wrapper(*args, **kwargs): try: run_result = await original_func(*args, **kwargs) except AgentsException as exc: + _capture_exception(exc) + context_wrapper = getattr(exc.run_data, "context_wrapper", None) if context_wrapper is not None: invoke_agent_span = getattr( @@ -50,7 +52,6 @@ async def wrapper(*args, **kwargs): _record_exception_on_span(invoke_agent_span, exc) end_invoke_agent_span(context_wrapper, agent) - _capture_exception(exc) raise exc from None end_invoke_agent_span(run_result.context_wrapper, agent) From 59732ed01b0b0ba5dfa6385688247b05e112c602 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 14:13:56 +0100 Subject: [PATCH 16/20] do not capture exception twice --- .../openai_agents/patches/agent_run.py | 6 + .../openai_agents/test_openai_agents.py | 164 ++++++++++++++++++ 2 files changed, 170 insertions(+) diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index 2bd921b690..a380c39877 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -19,6 +19,7 @@ try: import agents + from agents.exceptions import AgentsException except ImportError: raise DidNotEnable("OpenAI Agents not installed") @@ -90,6 +91,11 @@ async def patched_run_single_turn(cls, *args, **kwargs): # Call original method with all the correct parameters try: result = await original_run_single_turn(*args, **kwargs) + except AgentsException: + # AgentsException is caught on AgentRunner.run(). + # Exceptions are captured and agent invocation spans are explicitly finished + # as long as only AgentRunner.run() invokes AgentRunner._run_single_turn(). + raise except Exception as exc: _capture_exception(exc) diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index dd216d8a90..748455fe5b 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -5,6 +5,7 @@ import os from sentry_sdk.integrations.openai_agents import OpenAIAgentsIntegration +from sentry_sdk.integrations.asyncio import AsyncioIntegration from sentry_sdk.integrations.openai_agents.utils import safe_serialize from sentry_sdk.utils import parse_version @@ -21,6 +22,7 @@ ResponseOutputText, ResponseFunctionToolCall, ) +from agents.exceptions import MaxTurnsExceeded, ModelBehaviorError from agents.version import __version__ as OPENAI_AGENTS_VERSION from openai.types.responses.response_usage import ( @@ -349,6 +351,97 @@ async def test_handoff_span(sentry_init, capture_events, mock_usage): assert handoff_span["data"]["gen_ai.operation.name"] == "handoff" +@pytest.mark.asyncio +async def test_max_turns_before_handoff_span(sentry_init, capture_events, mock_usage): + """ + Example raising agents.exceptions.AgentsException after the agent invocation span is complete. + """ + # Create two simple agents with a handoff relationship + secondary_agent = agents.Agent( + name="secondary_agent", + instructions="You are a secondary agent.", + model="gpt-4o-mini", + ) + + primary_agent = agents.Agent( + name="primary_agent", + instructions="You are a primary agent that hands off to secondary agent.", + model="gpt-4o-mini", + handoffs=[secondary_agent], + ) + + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + with patch( + "agents.models.openai_responses.OpenAIResponsesModel.get_response" + ) as mock_get_response: + # Mock two responses: + # 1. Primary agent calls handoff tool + # 2. Secondary agent provides final response + handoff_response = ModelResponse( + output=[ + ResponseFunctionToolCall( + id="call_handoff_123", + call_id="call_handoff_123", + name="transfer_to_secondary_agent", + type="function_call", + arguments="{}", + ) + ], + usage=mock_usage, + response_id="resp_handoff_123", + ) + + final_response = ModelResponse( + 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", + ) + ], + usage=mock_usage, + response_id="resp_final_123", + ) + + mock_get_response.side_effect = [handoff_response, final_response] + + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + ) + + events = capture_events() + + with pytest.raises(MaxTurnsExceeded): + result = await agents.Runner.run( + primary_agent, + "Please hand off to secondary agent", + run_config=test_run_config, + max_turns=1, + ) + + assert result is not None + + (error, transaction) = events + spans = transaction["spans"] + handoff_span = spans[2] + + # Verify handoff span was created + assert handoff_span is not None + assert ( + handoff_span["description"] == "handoff from primary_agent to secondary_agent" + ) + assert handoff_span["data"]["gen_ai.operation.name"] == "handoff" + + @pytest.mark.asyncio async def test_tool_execution_span(sentry_init, capture_events, test_agent): """ @@ -601,6 +694,77 @@ def simple_test_tool(message: str) -> str: assert ai_client_span2["data"]["gen_ai.usage.total_tokens"] == 25 +@pytest.mark.asyncio +async def test_model_behavior_error(sentry_init, capture_events, test_agent): + """ + Example raising agents.exceptions.AgentsException before the agent invocation span is complete. + The mocked API response indicates that "wrong_tool" was called. + """ + + @agents.function_tool + def simple_test_tool(message: str) -> str: + """A simple tool""" + return f"Tool executed with: {message}" + + # Create agent with the tool + agent_with_tool = test_agent.clone(tools=[simple_test_tool]) + + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + with patch( + "agents.models.openai_responses.OpenAIResponsesModel.get_response" + ) as mock_get_response: + # Create a mock response that includes tool calls + tool_call = ResponseFunctionToolCall( + id="call_123", + call_id="call_123", + name="wrong_tool", + type="function_call", + arguments='{"message": "hello"}', + ) + + tool_response = ModelResponse( + output=[tool_call], + usage=Usage( + requests=1, input_tokens=10, output_tokens=5, total_tokens=15 + ), + response_id="resp_tool_123", + ) + + mock_get_response.side_effect = [tool_response] + + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + ) + + events = capture_events() + + with pytest.raises(ModelBehaviorError): + await agents.Runner.run( + agent_with_tool, + "Please use the simple test tool", + run_config=test_run_config, + ) + + (error, transaction) = events + spans = transaction["spans"] + ( + agent_span, + ai_client_span1, + ) = spans + + assert transaction["transaction"] == "test_agent workflow" + assert transaction["contexts"]["trace"]["origin"] == "auto.ai.openai_agents" + + assert agent_span["description"] == "invoke_agent test_agent" + assert agent_span["origin"] == "auto.ai.openai_agents" + + # Error due to unrecognized tool in model response. + assert agent_span["status"] == "internal_error" + assert agent_span["tags"]["status"] == "internal_error" + + @pytest.mark.asyncio async def test_error_handling(sentry_init, capture_events, test_agent): """ From e9e9e3af74aa361603733c449dde6d14923daeee Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 15:17:01 +0100 Subject: [PATCH 17/20] capture all exceptions again --- .../integrations/openai_agents/patches/agent_run.py | 4 ++-- .../integrations/openai_agents/patches/runner.py | 11 ++++++++++- sentry_sdk/integrations/openai_agents/utils.py | 5 +++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index a380c39877..d36e17c009 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -8,7 +8,7 @@ end_invoke_agent_span, handoff_span, ) -from ..utils import _capture_exception, _record_exception_on_span +from ..utils import _capture_exception, _record_exception_on_span, _SingleTurnException from typing import TYPE_CHECKING @@ -103,7 +103,7 @@ async def patched_run_single_turn(cls, *args, **kwargs): _record_exception_on_span(span, exc) end_invoke_agent_span(context_wrapper, agent) - raise exc from None + raise _SingleTurnException(exc) return result diff --git a/sentry_sdk/integrations/openai_agents/patches/runner.py b/sentry_sdk/integrations/openai_agents/patches/runner.py index 671f83dcab..b913c82f4a 100644 --- a/sentry_sdk/integrations/openai_agents/patches/runner.py +++ b/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -4,7 +4,7 @@ from sentry_sdk.integrations import DidNotEnable from ..spans import agent_workflow_span, end_invoke_agent_span -from ..utils import _capture_exception, _record_exception_on_span +from ..utils import _capture_exception, _record_exception_on_span, _SingleTurnException try: from agents.exceptions import AgentsException @@ -53,6 +53,15 @@ async def wrapper(*args, **kwargs): end_invoke_agent_span(context_wrapper, agent) raise exc from None + except _SingleTurnException as exc: + # Handled in _run_single_turn() patch. + raise exc.original from None + except Exception as exc: + # Invoke agent span is not finished in this case. + # This is much less likely to occur than other cases because + # AgentRunner.run() is "just" a while loop around _run_single_turn. + _capture_exception(exc) + raise exc from None end_invoke_agent_span(run_result.context_wrapper, agent) return run_result diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index 0c471eec9a..37c65688a2 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -26,6 +26,11 @@ raise DidNotEnable("OpenAI Agents not installed") +class _SingleTurnException(Exception): + def __init__(self, original): + self.original = original + + def _capture_exception(exc): # type: (Any) -> None set_span_errored() From 4580edca011ce2269b2395460204acca5cdb03ee Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 15:20:25 +0100 Subject: [PATCH 18/20] type annotation --- sentry_sdk/integrations/openai_agents/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index 37c65688a2..00248d7a2f 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -28,6 +28,7 @@ class _SingleTurnException(Exception): def __init__(self, original): + # type: Exception -> None self.original = original From 0477a4ba3239937c8c5ee81fbc116a585704c1d0 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 15:22:45 +0100 Subject: [PATCH 19/20] mypy --- sentry_sdk/integrations/openai_agents/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index 00248d7a2f..fc2dedd45d 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -28,7 +28,7 @@ class _SingleTurnException(Exception): def __init__(self, original): - # type: Exception -> None + # type: (Exception) -> None self.original = original From ef3ddc639270b45fd1a9c022e70a54728ccdb502 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 1 Dec 2025 15:33:36 +0100 Subject: [PATCH 20/20] remove unreachable assertion --- tests/integrations/openai_agents/test_openai_agents.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index 748455fe5b..4ee04dc21f 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -421,15 +421,13 @@ async def test_max_turns_before_handoff_span(sentry_init, capture_events, mock_u events = capture_events() with pytest.raises(MaxTurnsExceeded): - result = await agents.Runner.run( + await agents.Runner.run( primary_agent, "Please hand off to secondary agent", run_config=test_run_config, max_turns=1, ) - assert result is not None - (error, transaction) = events spans = transaction["spans"] handoff_span = spans[2]