Skip to content

Conversation

@alexander-alderman-webb
Copy link
Contributor

@alexander-alderman-webb alexander-alderman-webb commented Dec 1, 2025

Description

With #5165 we have a handle on the agent invocation span, as the span is now attached to the context wrapper returned from the AgentRunner.run() method.

Exceptions of type AgentsException are now caught on AgentRunner.run() instead of AgentRunner._run_single_turn(), because openai-agents attaches the context wrapper to these exceptions in the method.

Removing sentry_sdk.get_current_span() avoids double exit scenarios when the active span is not an agent invocation span. This is the case when the asyncio integration is used.

Previously, the script below results in a double span exit unhandled exception.

import asyncio

from unittest import mock

import agents
from agents import Agent, Runner

import sentry_sdk
from sentry_sdk.integrations.openai_agents import OpenAIAgentsIntegration
from sentry_sdk.integrations.asyncio import AsyncioIntegration

async def main():
    with mock.patch(
        "agents.models.openai_responses.OpenAIResponsesModel.get_response"
    ) as mock_get_response:
        mock_get_response.side_effect = Exception("Model Error")

        sentry_sdk.init(
            integrations=[OpenAIAgentsIntegration(), AsyncioIntegration()],
            traces_sample_rate=1.0,
            debug=True
        )

        agent = Agent(
            name="Bug Reproduction Agent",
            instructions="You are a test agent. Always use the broken_tool.",
            model="gpt-4o-mini",
        )

        try:
            await Runner.run(
                agent, "Test input", # run_config=test_run_config
            )
        except Exception:
            pass

if __name__ == "__main__":
    asyncio.run(main())

Issues

Reminders

@alexander-alderman-webb alexander-alderman-webb marked this pull request as ready for review December 1, 2025 09:10
@alexander-alderman-webb alexander-alderman-webb requested a review from a team as a code owner December 1, 2025 09:10
@alexander-alderman-webb alexander-alderman-webb marked this pull request as draft December 1, 2025 09:59
@alexander-alderman-webb alexander-alderman-webb marked this pull request as ready for review December 1, 2025 10:24
@codecov
Copy link

codecov bot commented Dec 1, 2025

Codecov Report

❌ Patch coverage is 74.54545% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.95%. Comparing base (9c9510d) to head (b3e5d01).
⚠️ Report is 1 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...y_sdk/integrations/openai_agents/patches/runner.py 59.09% 7 Missing and 2 partials ⚠️
sentry_sdk/integrations/openai_agents/utils.py 75.00% 0 Missing and 3 partials ⚠️
...dk/integrations/openai_agents/patches/agent_run.py 85.71% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5174      +/-   ##
==========================================
+ Coverage   83.94%   83.95%   +0.01%     
==========================================
  Files         181      181              
  Lines       18419    18451      +32     
  Branches     3281     3286       +5     
==========================================
+ Hits        15462    15491      +29     
- Misses       1944     1946       +2     
- Partials     1013     1014       +1     
Files with missing lines Coverage Δ
...ntegrations/openai_agents/patches/error_tracing.py 62.06% <100.00%> (+2.06%) ⬆️
...y_sdk/integrations/openai_agents/spans/__init__.py 100.00% <100.00%> (ø)
...k/integrations/openai_agents/spans/invoke_agent.py 88.37% <100.00%> (+3.75%) ⬆️
...dk/integrations/openai_agents/patches/agent_run.py 84.41% <85.71%> (+1.08%) ⬆️
sentry_sdk/integrations/openai_agents/utils.py 88.00% <75.00%> (-1.78%) ⬇️
...y_sdk/integrations/openai_agents/patches/runner.py 75.00% <59.09%> (-20.66%) ⬇️

... and 6 files with indirect coverage changes

@alexander-alderman-webb alexander-alderman-webb marked this pull request as draft December 1, 2025 11:52
@alexander-alderman-webb alexander-alderman-webb marked this pull request as ready for review December 1, 2025 13:24
# 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.

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

Base automatically changed from webb/store-span-on-openai-agents-context-wrapper to master December 1, 2025 16:19
_capture_exception(exc)
raise exc from None

end_invoke_agent_span(run_result.context_wrapper, agent)

This comment was marked as outdated.

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)

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

Choose a reason for hiding this comment

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

it's hard for me to follow the flow, but my recommendation would be this:

  • we should not call capture_exception here at all, just have a finally block that takes care of the span management
  • the exception should be captured only in the runner.py patch
  • the library internally already decides what to re-raise and what to swallow and we don't need to care about those, only what the library finally emits

Copy link
Member

Choose a reason for hiding this comment

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

sorry I meant that we should only have this one and not the one in agent_run

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason there are special cases for exceptions, and for handling exceptions at different levels is that

  • openai-agents only attaches the context wrapper when AgentsException is raised; and
  • we always have a handle on the context_wrapper in agent_run.py, and only sometimes in runner.py.

Regarding having a finally block, I did not go with that approach for different reasons in agent_run.py and runner.py.

In agent_run.py, one agent invocation can span multiple calls to the function we patch, so it's intentional to only finish the span in the except block. The broad Exception is caught here so we can finish the span even when an exception which does not subclass AgentsException is raised. In runner.py we do not have a handle on the span if it is not an AgentsException.

In runner.py, we do not have a finally block because you access the context wrapper from different places depending on the code path. In the non-exception case, the context wrapper is on the return value of the function, and in the exception case, the context wrapper is attached to the exception object.

Copy link
Member

Choose a reason for hiding this comment

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

so it's intentional to only finish the span in the except block.

in that case keeping the span management in the except block is fine.

But I still think the capture_exception should only be there on the outermost layer. We should not be coupling exception handling and span management this tightly.

Regarding handling missing state on our end, we should simply write logic with presence checks so we don't rely on the actual underlying implementation of the lib so much. Currently, much of the design is very brittle to internals changing.

Copy link
Member

Choose a reason for hiding this comment

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

the span context manager __exit__ already responds to exceptions, but instead whoever wrote this decided to pass (None, None, None) here which makes everything extra complicated.

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've moved capture_exception to the outer layer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also realized we don't need this _SingleTurnException stuff so that's removed now.
Hopefully that makes it slightly less brittle to internals changing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants