From db240f2bbeef9527866781d0f7243900caf2b21b Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Wed, 27 May 2026 08:44:09 -0400 Subject: [PATCH] fix(openai_agents): Handle starting_agent keyword argument in runner patches Runner.run() and Runner.run_streamed() accept starting_agent as either a positional or keyword argument. The patches were always reading args[0], which crashes when the caller uses the keyword form (e.g. Runner.run(starting_agent=agent, input=...)). Check for the keyword argument first; fall back to the positional arg. Fixes GH-6418 Fixes PY-2498 --- .../openai_agents/patches/runner.py | 21 ++- .../openai_agents/test_openai_agents.py | 162 ++++++++++++++++++ 2 files changed, 179 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/runner.py b/sentry_sdk/integrations/openai_agents/patches/runner.py index 6828ab4855..c5a4fa7131 100644 --- a/sentry_sdk/integrations/openai_agents/patches/runner.py +++ b/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -36,7 +36,10 @@ async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": # don't touch each other's scopes with sentry_sdk.isolation_scope(): # Clone agent because agent invocation spans are attached per run. - agent = args[0].clone() + if "starting_agent" in kwargs: + agent = kwargs["starting_agent"].clone() + else: + agent = args[0].clone() with agent_workflow_span(agent) as workflow_span: # Set conversation ID on workflow span early so it's captured even on errors @@ -47,7 +50,11 @@ async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id ) - args = (agent, *args[1:]) + if "starting_agent" in kwargs: + kwargs["starting_agent"] = agent + else: + args = (agent, *args[1:]) + try: run_result = await original_func(*args, **kwargs) except AgentsException as exc: @@ -122,7 +129,10 @@ def _create_run_streamed_wrapper( @wraps(original_func) def wrapper(*args: "Any", **kwargs: "Any") -> "Any": # Clone agent because agent invocation spans are attached per run. - agent = args[0].clone() + if "starting_agent" in kwargs: + agent = kwargs["starting_agent"].clone() + else: + agent = args[0].clone() # Capture conversation_id from kwargs if provided conversation_id = kwargs.get("conversation_id") @@ -140,7 +150,10 @@ def wrapper(*args: "Any", **kwargs: "Any") -> "Any": # Store span on agent for cleanup agent._sentry_workflow_span = workflow_span - args = (agent, *args[1:]) + if "starting_agent" in kwargs: + kwargs["starting_agent"] = agent + else: + args = (agent, *args[1:]) try: # Call original function to get RunResultStreaming diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index cfe23f922a..d581fb5bb9 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -4943,3 +4943,165 @@ async def test_no_conversation_id_when_not_provided( ) assert "gen_ai.conversation.id" not in invoke_agent_span.get("data", {}) assert "gen_ai.conversation.id" not in ai_client_span.get("data", {}) + + +@pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) +@pytest.mark.asyncio +async def test_runner_run_with_starting_agent_kwarg( + sentry_init, + capture_events, + test_agent, + nonstreaming_responses_model_response, + get_model_response, + stream_gen_ai_spans, +): + """Runner.run(starting_agent=agent, input=...) must not crash. + + Regression test for https://github.com/getsentry/sentry-python/issues/6418 + """ + client = AsyncOpenAI(api_key="test-key") + model = OpenAIResponsesModel(model="gpt-4", openai_client=client) + agent = test_agent.clone(model=model) + + response = get_model_response( + nonstreaming_responses_model_response, serialize_pydantic=True + ) + + with patch.object( + agent.model._client._client, + "send", + return_value=response, + ): + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + stream_gen_ai_spans=stream_gen_ai_spans, + ) + + events = capture_events() + + result = await agents.run.DEFAULT_AGENT_RUNNER.run( + starting_agent=agent, + input="Test input", + run_config=test_run_config, + ) + + assert result is not None + assert result.final_output == "Hello, how can I help you?" + + (transaction,) = events + assert transaction["transaction"] == "test_agent workflow" + + +@pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) +@pytest.mark.asyncio +async def test_runner_run_streamed_with_starting_agent_kwarg( + sentry_init, + capture_events, + test_agent, + async_iterator, + server_side_event_chunks, + get_model_response, + stream_gen_ai_spans, +): + """Runner.run_streamed(starting_agent=agent, input=...) must not crash. + + Regression test for https://github.com/getsentry/sentry-python/issues/6418 + """ + client = AsyncOpenAI(api_key="test-key") + model = OpenAIResponsesModel(model="gpt-4", openai_client=client) + agent = test_agent.clone(model=model) + + request_headers = {} + if parse_version(OPENAI_AGENTS_VERSION) >= (0, 10, 3) and hasattr( + agent.model._client.responses, "with_streaming_response" + ): + request_headers["X-Stainless-Raw-Response"] = "stream" + + response = get_model_response( + async_iterator( + server_side_event_chunks( + [ + ResponseCreatedEvent( + response=Response( + id="chat-id", + output=[], + parallel_tool_calls=False, + tool_choice="none", + tools=[], + created_at=10000000, + model="gpt-4", + object="response", + ), + type="response.created", + sequence_number=0, + ), + ResponseCompletedEvent( + response=Response( + id="chat-id", + output=[ + ResponseOutputMessage( + id="message-id", + content=[ + ResponseOutputText( + annotations=[], + text="Hello, how can I help you?", + type="output_text", + ), + ], + role="assistant", + status="completed", + type="message", + ), + ], + parallel_tool_calls=False, + tool_choice="none", + tools=[], + created_at=10000000, + model="gpt-4", + object="response", + usage=ResponseUsage( + input_tokens=10, + input_tokens_details=InputTokensDetails( + cached_tokens=0, + ), + output_tokens=20, + output_tokens_details=OutputTokensDetails( + reasoning_tokens=5, + ), + total_tokens=30, + ), + ), + type="response.completed", + sequence_number=1, + ), + ] + ) + ), + request_headers=request_headers, + ) + + with patch.object( + agent.model._client._client, + "send", + return_value=response, + ): + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + stream_gen_ai_spans=stream_gen_ai_spans, + ) + + events = capture_events() + + result = agents.run.DEFAULT_AGENT_RUNNER.run_streamed( + starting_agent=agent, + input="Test input", + run_config=test_run_config, + ) + + async for _event in result.stream_events(): + pass + + (transaction,) = events + assert transaction["transaction"] == "test_agent workflow"