From 0d543f5df8213ba82303a335b4b99edb8814ac8f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 25 Aug 2025 14:57:26 +0200 Subject: [PATCH 1/5] fix(openai-agents): Isolate agent run --- .../openai_agents/patches/agent_run.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index 084100878c..b8c0abe09f 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -1,7 +1,7 @@ from functools import wraps +import sentry_sdk from sentry_sdk.integrations import DidNotEnable - from ..spans import invoke_agent_span, update_invoke_agent_span, handoff_span from typing import TYPE_CHECKING @@ -9,7 +9,6 @@ if TYPE_CHECKING: from typing import Any, Optional - try: import agents except ImportError: @@ -63,24 +62,25 @@ async def patched_run_single_turn(cls, *args, **kwargs): # type: (agents.Runner, *Any, **Any) -> Any """Patched _run_single_turn that creates agent invocation spans""" - agent = kwargs.get("agent") - context_wrapper = kwargs.get("context_wrapper") - should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks") + with sentry_sdk.isolation_scope(): + agent = kwargs.get("agent") + context_wrapper = kwargs.get("context_wrapper") + should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks") - # 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 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) + _start_invoke_agent_span(context_wrapper, agent) - # Call original method with all the correct parameters - result = await original_run_single_turn(*args, **kwargs) + # Call original method with all the correct parameters + result = await original_run_single_turn(*args, **kwargs) - return result + return result @wraps( original_execute_handoffs.__func__ From 09770fb1cf147082dee9cf41cb40ef883f6b311f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 25 Aug 2025 15:51:14 +0200 Subject: [PATCH 2/5] . --- .../openai_agents/patches/agent_run.py | 31 +++++++++---------- .../openai_agents/patches/runner.py | 31 ++++++++++--------- .../openai_agents/test_openai_agents.py | 30 ++++++++++++++++++ 3 files changed, 60 insertions(+), 32 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index b8c0abe09f..29002f6619 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -1,6 +1,5 @@ from functools import wraps -import sentry_sdk from sentry_sdk.integrations import DidNotEnable from ..spans import invoke_agent_span, update_invoke_agent_span, handoff_span @@ -61,26 +60,24 @@ def _get_current_agent(context_wrapper): async def patched_run_single_turn(cls, *args, **kwargs): # type: (agents.Runner, *Any, **Any) -> Any """Patched _run_single_turn that creates agent invocation spans""" + agent = kwargs.get("agent") + context_wrapper = kwargs.get("context_wrapper") + should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks") - with sentry_sdk.isolation_scope(): - agent = kwargs.get("agent") - context_wrapper = kwargs.get("context_wrapper") - should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks") - - # 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 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) + _start_invoke_agent_span(context_wrapper, agent) - # Call original method with all the correct parameters - result = await original_run_single_turn(*args, **kwargs) + # Call original method with all the correct parameters + result = await original_run_single_turn(*args, **kwargs) - return result + return result @wraps( original_execute_handoffs.__func__ diff --git a/sentry_sdk/integrations/openai_agents/patches/runner.py b/sentry_sdk/integrations/openai_agents/patches/runner.py index e1e9a3b50c..e6a098a070 100644 --- a/sentry_sdk/integrations/openai_agents/patches/runner.py +++ b/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -23,20 +23,21 @@ def _create_run_wrapper(original_func): @wraps(original_func) async def wrapper(*args, **kwargs): # type: (*Any, **Any) -> Any - 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 + 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 return wrapper diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index 3f64e5c45c..ba26698b6c 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -1,3 +1,4 @@ +import asyncio import re import pytest from unittest.mock import MagicMock, patch @@ -637,3 +638,32 @@ async def test_error_handling(sentry_init, capture_events, test_agent): assert ai_client_span["description"] == "chat gpt-4" assert ai_client_span["origin"] == "auto.ai.openai_agents" assert ai_client_span["tags"]["status"] == "internal_error" + + +@pytest.mark.asyncio +async def test_multiple_agents_asyncio( + sentry_init, capture_events, test_agent, mock_model_response +): + """ + Test that multiple agents can be run at the same time in asyncio tasks. + """ + + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + with patch( + "agents.models.openai_responses.OpenAIResponsesModel.get_response" + ) as mock_get_response: + mock_get_response.return_value = mock_model_response + + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + ) + + asyncio.gather( + *[ + agents.Runner.run( + test_agent, "Test input", run_config=test_run_config + ) + for _ in range(3) + ] + ) From 2ff6efecc22ba9d479ce424b8f2147c3967dedec Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 25 Aug 2025 16:43:57 +0200 Subject: [PATCH 3/5] . --- tests/integrations/openai_agents/test_openai_agents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index ba26698b6c..b143c8ff15 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -659,7 +659,7 @@ async def test_multiple_agents_asyncio( traces_sample_rate=1.0, ) - asyncio.gather( + await asyncio.gather( *[ agents.Runner.run( test_agent, "Test input", run_config=test_run_config From 4a52ccf431ca4fda47d2b95dd574dad4ee545f3a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 26 Aug 2025 08:41:18 +0200 Subject: [PATCH 4/5] better test --- .../openai_agents/test_openai_agents.py | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index b143c8ff15..09fca2fbf3 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -645,7 +645,8 @@ async def test_multiple_agents_asyncio( sentry_init, capture_events, test_agent, mock_model_response ): """ - Test that multiple agents can be run at the same time in asyncio tasks. + Test that multiple agents can be run at the same time in asyncio tasks + without interfering with each other. """ with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): @@ -659,11 +660,23 @@ async def test_multiple_agents_asyncio( traces_sample_rate=1.0, ) - await asyncio.gather( - *[ - agents.Runner.run( - test_agent, "Test input", run_config=test_run_config - ) - for _ in range(3) - ] - ) + events = capture_events() + + async def run(): + await agents.Runner.run( + starting_agent=test_agent, + input="Test input", + run_config=test_run_config, + ) + + await asyncio.gather(*[run() for _ in range(3)]) + + assert len(events) == 3 + txn1, txn2, txn3 = events + + assert txn1["type"] == "transaction" + assert txn1["transaction"] == "test_agent workflow" + assert txn2["type"] == "transaction" + assert txn2["transaction"] == "test_agent workflow" + assert txn3["type"] == "transaction" + assert txn3["transaction"] == "test_agent workflow" From 23db0c8f54396bb8d1d35326b188e3786a4199cf Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 26 Aug 2025 09:46:00 +0200 Subject: [PATCH 5/5] comment --- sentry_sdk/integrations/openai_agents/patches/runner.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry_sdk/integrations/openai_agents/patches/runner.py b/sentry_sdk/integrations/openai_agents/patches/runner.py index e6a098a070..745f30a38e 100644 --- a/sentry_sdk/integrations/openai_agents/patches/runner.py +++ b/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -23,6 +23,8 @@ def _create_run_wrapper(original_func): @wraps(original_func) async def wrapper(*args, **kwargs): # type: (*Any, **Any) -> Any + # Isolate each workflow so that when agents are run in asyncio tasks they + # don't touch each other's scopes with sentry_sdk.isolation_scope(): agent = args[0] with agent_workflow_span(agent):