Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 30 additions & 5 deletions sentry_sdk/integrations/openai_agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
_execute_handoffs,
_create_run_wrapper,
_create_run_streamed_wrapper,
_patch_agent_run,
_execute_final_output,
_patch_error_tracing,
)

Expand Down Expand Up @@ -60,9 +60,6 @@ def _patch_runner() -> None:
agents.run.DEFAULT_AGENT_RUNNER.run_streamed
)

# Creating the actual spans for each agent run (works for both streaming and non-streaming).
_patch_agent_run()


class OpenAIAgentsIntegration(Integration):
"""
Expand All @@ -82,7 +79,7 @@ class OpenAIAgentsIntegration(Integration):
- A Model instance is created at the start of the loop by calling the `Runner._get_model()`. We patch the Model instance using `patches._get_model()`.
- Available tools are also deteremined at the start of the loop, with `Runner._get_all_tools()`. We patch Tool instances by iterating through the returned tools in `patches._get_all_tools()`.
- In each loop iteration, `run_single_turn()` or `run_single_turn_streamed()` is responsible for calling the Responses API, patched with `patches._run_single_turn()` and `patches._run_single_turn_streamed()`.
4. On loop termination, `RunImpl.execute_final_output()` is called. The function is patched with `patched_execute_final_output()`.
4. On loop termination, `RunImpl.execute_final_output()` is called. The function is patched with `patches._execute_final_output()`.

Local tools are run based on the return value from the Responses API as a post-API call step in the above loop.
Hosted MCP Tools are run as part of the Responses API call, and involve OpenAI reaching out to an external MCP server.
Expand Down Expand Up @@ -155,6 +152,20 @@ async def new_wrapped_execute_handoffs(
new_wrapped_execute_handoffs
)

original_execute_final_output = turn_resolution.execute_final_output

@wraps(turn_resolution.execute_final_output)
async def new_wrapped_final_output(
*args: "Any", **kwargs: "Any"
) -> "SingleStepResult":
return await _execute_final_output(
original_execute_final_output, *args, **kwargs
)

agents.run_internal.turn_resolution.execute_final_output = (
new_wrapped_final_output
)

return

original_get_all_tools = AgentRunner._get_all_tools
Expand Down Expand Up @@ -216,3 +227,17 @@ async def old_wrapped_execute_handoffs(
agents._run_impl.RunImpl.execute_handoffs = classmethod(
old_wrapped_execute_handoffs
)

original_execute_final_output = agents._run_impl.RunImpl.execute_final_output

@wraps(agents._run_impl.RunImpl.execute_final_output.__func__)
async def old_wrapped_final_output(
cls: "agents.Runner", *args: "Any", **kwargs: "Any"
) -> "SingleStepResult":
return await _execute_final_output(
original_execute_final_output, *args, **kwargs
)

agents._run_impl.RunImpl.execute_final_output = classmethod(
old_wrapped_final_output
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
_run_single_turn,
_run_single_turn_streamed,
_execute_handoffs,
_patch_agent_run,
_execute_final_output,
) # noqa: F401
from .error_tracing import _patch_error_tracing # noqa: F401
63 changes: 21 additions & 42 deletions sentry_sdk/integrations/openai_agents/patches/agent_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,49 +204,28 @@ async def _execute_handoffs(
return result


def _patch_agent_run() -> None:
async def _execute_final_output(
original_execute_final_output: "Callable[..., SingleStepResult]",
*args: "Any",
**kwargs: "Any",
) -> "SingleStepResult":
"""
Patches AgentRunner methods to create agent invocation spans.
This directly patches the execution flow to track when agents start and stop.
Patched execute_final_output that
- ends the agent invocation span.
- ends the workflow span if the response is streamed.
"""

# Store original methods
original_execute_final_output = agents._run_impl.RunImpl.execute_final_output
agent = kwargs.get("agent")
context_wrapper = kwargs.get("context_wrapper")
final_output = kwargs.get("final_output")

@wraps(
original_execute_final_output.__func__
if hasattr(original_execute_final_output, "__func__")
else original_execute_final_output
)
async def patched_execute_final_output(
cls: "agents.Runner", *args: "Any", **kwargs: "Any"
) -> "Any":
"""
Patched execute_final_output that
- ends the agent invocation span.
- ends the workflow span if the response is streamed.
"""

agent = kwargs.get("agent")
context_wrapper = kwargs.get("context_wrapper")
final_output = kwargs.get("final_output")

try:
result = await original_execute_final_output(*args, **kwargs)
finally:
with capture_internal_exceptions():
if (
agent
and context_wrapper
and _has_active_agent_span(context_wrapper)
):
end_invoke_agent_span(context_wrapper, agent, final_output)
# For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper)
_close_streaming_workflow_span(agent)

return result

# Apply patches
agents._run_impl.RunImpl.execute_final_output = classmethod(
patched_execute_final_output
)
try:
result = await original_execute_final_output(*args, **kwargs)
finally:
with capture_internal_exceptions():
if agent and context_wrapper and _has_active_agent_span(context_wrapper):
end_invoke_agent_span(context_wrapper, agent, final_output)
# For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper)
_close_streaming_workflow_span(agent)

return result
Loading