Skip to content

Commit 8d0b6cc

Browse files
fix(openai-agents): Avoid double span exit on exception (#5174)
Retrieve the agent invocation span from context wrapper attached to the return value of `AgentRunner.run()`, or attached to the `AgentsException` in the error case. If an exception is raised in `AgentRunner._run_single_turn()`, terminate the agent invocation span there, since the outer `AgentRunner.run()` only attaches the context wrapper to subclasses of `AgentsException`. Prevents an unhandled exception due to a double span exit caused by `sentry_sdk.get_current_span()` returning the parent of the agent invocation span. Follows on from commit 996f935.
1 parent d2d3d35 commit 8d0b6cc

File tree

7 files changed

+246
-36
lines changed

7 files changed

+246
-36
lines changed

sentry_sdk/integrations/openai_agents/patches/agent_run.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
import sys
12
from functools import wraps
23

34
from sentry_sdk.integrations import DidNotEnable
4-
from ..spans import invoke_agent_span, update_invoke_agent_span, handoff_span
5+
from sentry_sdk.utils import reraise
6+
from ..spans import (
7+
invoke_agent_span,
8+
end_invoke_agent_span,
9+
handoff_span,
10+
)
11+
from ..utils import _record_exception_on_span
512

613
from typing import TYPE_CHECKING
714

@@ -38,15 +45,6 @@ def _start_invoke_agent_span(context_wrapper, agent, kwargs):
3845

3946
return span
4047

41-
def _end_invoke_agent_span(context_wrapper, agent, output=None):
42-
# type: (agents.RunContextWrapper, agents.Agent, Optional[Any]) -> None
43-
"""End the agent invocation span"""
44-
# Clear the stored agent
45-
if hasattr(context_wrapper, "_sentry_current_agent"):
46-
delattr(context_wrapper, "_sentry_current_agent")
47-
48-
update_invoke_agent_span(context_wrapper, agent, output)
49-
5048
def _has_active_agent_span(context_wrapper):
5149
# type: (agents.RunContextWrapper) -> bool
5250
"""Check if there's an active agent span for this context"""
@@ -69,19 +67,27 @@ async def patched_run_single_turn(cls, *args, **kwargs):
6967
context_wrapper = kwargs.get("context_wrapper")
7068
should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks")
7169

70+
span = getattr(context_wrapper, "_sentry_agent_span", None)
7271
# Start agent span when agent starts (but only once per agent)
7372
if should_run_agent_start_hooks and agent and context_wrapper:
7473
# End any existing span for a different agent
7574
if _has_active_agent_span(context_wrapper):
7675
current_agent = _get_current_agent(context_wrapper)
7776
if current_agent and current_agent != agent:
78-
_end_invoke_agent_span(context_wrapper, current_agent)
77+
end_invoke_agent_span(context_wrapper, current_agent)
7978

8079
span = _start_invoke_agent_span(context_wrapper, agent, kwargs)
8180
agent._sentry_agent_span = span
8281

8382
# Call original method with all the correct parameters
84-
result = await original_run_single_turn(*args, **kwargs)
83+
try:
84+
result = await original_run_single_turn(*args, **kwargs)
85+
except Exception as exc:
86+
if span is not None and span.timestamp is None:
87+
_record_exception_on_span(span, exc)
88+
end_invoke_agent_span(context_wrapper, agent)
89+
90+
reraise(*sys.exc_info())
8591

8692
return result
8793

@@ -111,7 +117,7 @@ async def patched_execute_handoffs(cls, *args, **kwargs):
111117
finally:
112118
# End span for current agent after handoff processing is complete
113119
if agent and context_wrapper and _has_active_agent_span(context_wrapper):
114-
_end_invoke_agent_span(context_wrapper, agent)
120+
end_invoke_agent_span(context_wrapper, agent)
115121

116122
return result
117123

@@ -134,7 +140,7 @@ async def patched_execute_final_output(cls, *args, **kwargs):
134140
finally:
135141
# End span for current agent after final output processing is complete
136142
if agent and context_wrapper and _has_active_agent_span(context_wrapper):
137-
_end_invoke_agent_span(context_wrapper, agent, final_output)
143+
end_invoke_agent_span(context_wrapper, agent, final_output)
138144

139145
return result
140146

sentry_sdk/integrations/openai_agents/patches/error_tracing.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sentry_sdk
44
from sentry_sdk.consts import SPANSTATUS
55
from sentry_sdk.tracing_utils import set_span_errored
6+
from ..utils import _record_exception_on_span
67

78
from typing import TYPE_CHECKING
89

@@ -58,16 +59,7 @@ def sentry_attach_error_to_current_span(error, *args, **kwargs):
5859
# Set the current Sentry span to errored
5960
current_span = sentry_sdk.get_current_span()
6061
if current_span is not None:
61-
set_span_errored(current_span)
62-
current_span.set_data("span.status", "error")
63-
64-
# Optionally capture the error details if we have them
65-
if hasattr(error, "__class__"):
66-
current_span.set_data("error.type", error.__class__.__name__)
67-
if hasattr(error, "__str__"):
68-
error_message = str(error)
69-
if error_message:
70-
current_span.set_data("error.message", error_message)
62+
_record_exception_on_span(current_span, error)
7163

7264
# Call the original function
7365
return original_attach_error(error, *args, **kwargs)

sentry_sdk/integrations/openai_agents/patches/runner.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
from functools import wraps
22

33
import sentry_sdk
4+
from sentry_sdk.integrations import DidNotEnable
45

5-
from ..spans import agent_workflow_span
6-
from ..utils import _capture_exception
6+
from ..spans import agent_workflow_span, end_invoke_agent_span
7+
from ..utils import _capture_exception, _record_exception_on_span
8+
9+
try:
10+
from agents.exceptions import AgentsException
11+
except ImportError:
12+
raise DidNotEnable("OpenAI Agents not installed")
713

814
from typing import TYPE_CHECKING
915

@@ -29,19 +35,34 @@ async def wrapper(*args, **kwargs):
2935
# Clone agent because agent invocation spans are attached per run.
3036
agent = args[0].clone()
3137
with agent_workflow_span(agent):
32-
result = None
3338
args = (agent, *args[1:])
3439
try:
35-
result = await original_func(*args, **kwargs)
36-
return result
37-
except Exception as exc:
40+
run_result = await original_func(*args, **kwargs)
41+
except AgentsException as exc:
3842
_capture_exception(exc)
3943

40-
# It could be that there is a "invoke agent" span still open
41-
current_span = sentry_sdk.get_current_span()
42-
if current_span is not None and current_span.timestamp is None:
43-
current_span.__exit__(None, None, None)
44+
context_wrapper = getattr(exc.run_data, "context_wrapper", None)
45+
if context_wrapper is not None:
46+
invoke_agent_span = getattr(
47+
context_wrapper, "_sentry_agent_span", None
48+
)
49+
50+
if (
51+
invoke_agent_span is not None
52+
and invoke_agent_span.timestamp is None
53+
):
54+
_record_exception_on_span(invoke_agent_span, exc)
55+
end_invoke_agent_span(context_wrapper, agent)
4456

4557
raise exc from None
58+
except Exception as exc:
59+
# Invoke agent span is not finished in this case.
60+
# This is much less likely to occur than other cases because
61+
# AgentRunner.run() is "just" a while loop around _run_single_turn.
62+
_capture_exception(exc)
63+
raise exc from None
64+
65+
end_invoke_agent_span(run_result.context_wrapper, agent)
66+
return run_result
4667

4768
return wrapper

sentry_sdk/integrations/openai_agents/spans/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22
from .ai_client import ai_client_span, update_ai_client_span # noqa: F401
33
from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401
44
from .handoff import handoff_span # noqa: F401
5-
from .invoke_agent import invoke_agent_span, update_invoke_agent_span # noqa: F401
5+
from .invoke_agent import (
6+
invoke_agent_span,
7+
update_invoke_agent_span,
8+
end_invoke_agent_span,
9+
) # noqa: F401

sentry_sdk/integrations/openai_agents/spans/invoke_agent.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
if TYPE_CHECKING:
1818
import agents
19-
from typing import Any
19+
from typing import Any, Optional
2020

2121

2222
def invoke_agent_span(context, agent, kwargs):
@@ -95,3 +95,13 @@ def update_invoke_agent_span(context, agent, output):
9595

9696
span.__exit__(None, None, None)
9797
delattr(context, "_sentry_agent_span")
98+
99+
100+
def end_invoke_agent_span(context_wrapper, agent, output=None):
101+
# type: (agents.RunContextWrapper, agents.Agent, Optional[Any]) -> None
102+
"""End the agent invocation span"""
103+
# Clear the stored agent
104+
if hasattr(context_wrapper, "_sentry_current_agent"):
105+
delattr(context_wrapper, "_sentry_current_agent")
106+
107+
update_invoke_agent_span(context_wrapper, agent, output)

sentry_sdk/integrations/openai_agents/utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from typing import Any
1919
from agents import Usage
2020

21+
from sentry_sdk.tracing import Span
22+
2123
try:
2224
import agents
2325

@@ -37,6 +39,20 @@ def _capture_exception(exc):
3739
sentry_sdk.capture_event(event, hint=hint)
3840

3941

42+
def _record_exception_on_span(span, error):
43+
# type: (Span, Exception) -> Any
44+
set_span_errored(span)
45+
span.set_data("span.status", "error")
46+
47+
# Optionally capture the error details if we have them
48+
if hasattr(error, "__class__"):
49+
span.set_data("error.type", error.__class__.__name__)
50+
if hasattr(error, "__str__"):
51+
error_message = str(error)
52+
if error_message:
53+
span.set_data("error.message", error_message)
54+
55+
4056
def _set_agent_data(span, agent):
4157
# type: (sentry_sdk.tracing.Span, agents.Agent) -> None
4258
span.set_data(

0 commit comments

Comments
 (0)