Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9f2c047
fix(openai-agents): Store invoke_agent span on agents.RunContextWrapper
alexander-alderman-webb Nov 27, 2025
4ca61e6
be defensive when accessing the agent span
alexander-alderman-webb Nov 27, 2025
2c0edd5
fix(openai-agents): Avoid double span exit on exception
alexander-alderman-webb Dec 1, 2025
cea080b
Merge branch 'master' into webb/store-span-on-openai-agents-context-w…
alexander-alderman-webb Dec 1, 2025
f9521de
Merge branch 'webb/store-span-on-openai-agents-context-wrapper' into …
alexander-alderman-webb Dec 1, 2025
5a70ca0
restore end existing span
alexander-alderman-webb Dec 1, 2025
baa1b59
mypy
alexander-alderman-webb Dec 1, 2025
e6e40b1
access correct attribute
alexander-alderman-webb Dec 1, 2025
cb23da0
deduplicate
alexander-alderman-webb Dec 1, 2025
bc982ed
delattr on exit
alexander-alderman-webb Dec 1, 2025
a8fb881
Merge branch 'webb/store-span-on-openai-agents-context-wrapper' into …
alexander-alderman-webb Dec 1, 2025
557fc90
delattr on exit
alexander-alderman-webb Dec 1, 2025
dd3063b
call _end_invoke_agent_span instead of manually closing span
alexander-alderman-webb Dec 1, 2025
e5d5c52
add except block
alexander-alderman-webb Dec 1, 2025
e738f3d
move end_invoke_agent_span
alexander-alderman-webb Dec 1, 2025
64c2cfa
forgot __init__.py
alexander-alderman-webb Dec 1, 2025
cea38a2
mypy
alexander-alderman-webb Dec 1, 2025
90f5dba
capture_exception first
alexander-alderman-webb Dec 1, 2025
59732ed
do not capture exception twice
alexander-alderman-webb Dec 1, 2025
e9e9e3a
capture all exceptions again
alexander-alderman-webb Dec 1, 2025
4580edc
type annotation
alexander-alderman-webb Dec 1, 2025
0477a4b
mypy
alexander-alderman-webb Dec 1, 2025
ef3ddc6
remove unreachable assertion
alexander-alderman-webb Dec 1, 2025
470bbbb
merge master
alexander-alderman-webb Dec 1, 2025
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
37 changes: 32 additions & 5 deletions sentry_sdk/integrations/openai_agents/patches/agent_run.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
from functools import wraps

from sentry_sdk.integrations import DidNotEnable
from ..spans import invoke_agent_span, update_invoke_agent_span, handoff_span
from sentry_sdk.tracing_utils import set_span_errored
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, _SingleTurnException

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any, Optional

from sentry_sdk.tracing import Span

try:
import agents
from agents.exceptions import AgentsException
except ImportError:
raise DidNotEnable("OpenAI Agents not installed")

Expand All @@ -27,13 +37,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"""
Expand Down Expand Up @@ -65,18 +77,33 @@ 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_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
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)

_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 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)

if span is not None and span.timestamp is None:
_record_exception_on_span(span, exc)
end_invoke_agent_span(context_wrapper, agent)

raise _SingleTurnException(exc)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I created _SingleTurnException to avoid calling capture_exception() twice.

Otherwise, exceptions raised in _run_single_turn() that are not of type AgentsException would be captured in both run() and _run_single_turn().


return result

Expand Down
12 changes: 2 additions & 10 deletions sentry_sdk/integrations/openai_agents/patches/error_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
44 changes: 34 additions & 10 deletions sentry_sdk/integrations/openai_agents/patches/runner.py
Original file line number Diff line number Diff line change
@@ -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, _SingleTurnException

try:
from agents.exceptions import AgentsException
except ImportError:
raise DidNotEnable("OpenAI Agents not installed")

from typing import TYPE_CHECKING

Expand All @@ -28,18 +34,36 @@ 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:
run_result = await original_func(*args, **kwargs)
except AgentsException 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)
context_wrapper = getattr(exc.run_data, "context_wrapper", None)
Copy link

Choose a reason for hiding this comment

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

Bug: AttributeError when accessing exc.run_data without checking existence

The code accesses exc.run_data.context_wrapper using getattr but doesn't first verify that exc has a run_data attribute. If AgentsException lacks a run_data attribute, accessing exc.run_data will raise an AttributeError before getattr is called. The code should use getattr(exc, "run_data", None) first to safely check for the attribute's existence, consistent with the defensive pattern used for context_wrapper.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In practice it's always set as AgentRunner.run() has a big try-except as the top-level.

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)

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.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I could add a get_current_span() again in this case and we would never lose spans.

You also can't guarantee that the SDK does not crash if you try to exit a span you obtain with get_current_span(), so I decided the just drop the span for now.

Much of AgentRunner.run() that is not spent in _run_single_turn() is before the agent invocation span is created anyway. The only cases in which we lose a span is that any of the post-turn steps like output guardrails raise an exception that is not an AgentsException.

# 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)
Copy link

Choose a reason for hiding this comment

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

Bug: run_result.context_wrapper is accessed directly without defensive checking, risking an AttributeError if the attribute is missing.
Severity: CRITICAL | Confidence: High

🔍 Detailed Analysis

Line 66 in runner.py directly accesses run_result.context_wrapper without checking if the attribute exists. If the RunResult object returned by agents.Runner.run() lacks a context_wrapper attribute, an AttributeError will be raised, causing an application crash. This contrasts with the exception handling path (lines 42-45), which defensively uses getattr(exc.run_data, "context_wrapper", None).

💡 Suggested Fix

Replace direct access run_result.context_wrapper with getattr(run_result, "context_wrapper", None) to safely retrieve the attribute, similar to the exception path.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: sentry_sdk/integrations/openai_agents/patches/runner.py#L66

Potential issue: Line 66 in `runner.py` directly accesses `run_result.context_wrapper`
without checking if the attribute exists. If the `RunResult` object returned by
`agents.Runner.run()` lacks a `context_wrapper` attribute, an `AttributeError` will be
raised, causing an application crash. This contrasts with the exception handling path
(lines 42-45), which defensively uses `getattr(exc.run_data, "context_wrapper", None)`.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 4596996

Copy link
Contributor Author

Choose a reason for hiding this comment

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

duplicate of #5174 (comment)

return run_result

return wrapper
6 changes: 5 additions & 1 deletion sentry_sdk/integrations/openai_agents/spans/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 11 additions & 1 deletion sentry_sdk/integrations/openai_agents/spans/invoke_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

if TYPE_CHECKING:
import agents
from typing import Any
from typing import Any, Optional


def invoke_agent_span(context, agent, kwargs):
Expand Down Expand Up @@ -91,3 +91,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)
22 changes: 22 additions & 0 deletions sentry_sdk/integrations/openai_agents/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,21 @@
from typing import Any
from agents import Usage

from sentry_sdk.tracing import Span

try:
import agents

except ImportError:
raise DidNotEnable("OpenAI Agents not installed")


class _SingleTurnException(Exception):
def __init__(self, original):
# type: (Exception) -> None
self.original = original


def _capture_exception(exc):
# type: (Any) -> None
set_span_errored()
Expand All @@ -37,6 +45,20 @@ def _capture_exception(exc):
sentry_sdk.capture_event(event, hint=hint)


def _record_exception_on_span(span, error):
# 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):
# type: (sentry_sdk.tracing.Span, agents.Agent) -> None
span.set_data(
Expand Down
Loading