Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
12c28ac
ref(openai-agents): Use first class tool hooks when available
alexander-alderman-webb Aug 3, 2026
d982006
support streamed responses as well
alexander-alderman-webb Aug 3, 2026
0fa5385
fix langchain tests
alexander-alderman-webb Aug 3, 2026
4501d34
make mypy happy
alexander-alderman-webb Aug 3, 2026
47a1914
fix inheritance
alexander-alderman-webb Aug 3, 2026
b9c3cb0
make idempotent
alexander-alderman-webb Aug 3, 2026
1f1e5d3
some reordering
alexander-alderman-webb Aug 3, 2026
881ff72
early return for pii
alexander-alderman-webb Aug 3, 2026
9822982
update callsite
alexander-alderman-webb Aug 3, 2026
537e996
simplify test
alexander-alderman-webb Aug 3, 2026
78d5d6f
remove two more assertions
alexander-alderman-webb Aug 3, 2026
364b172
add a comment
alexander-alderman-webb Aug 3, 2026
bb76a15
gracefully fail
alexander-alderman-webb Aug 3, 2026
4fd1a4f
ref(openai-agents): Stop setting gen_ai.response.model on Invoke Agen…
alexander-alderman-webb Aug 4, 2026
3cef85d
ref(openai-agents): Use first class agent hooks when available
alexander-alderman-webb Aug 4, 2026
83de0ca
renamings and adding a docstring
alexander-alderman-webb Aug 4, 2026
ed6b6d2
add another docstring
alexander-alderman-webb Aug 4, 2026
dffd885
Merge branch 'webb/use-tool-hooks' into webb/use-agent-hooks
alexander-alderman-webb Aug 4, 2026
eb9bd68
merge
alexander-alderman-webb Aug 4, 2026
f60881e
delattr before span is closed
alexander-alderman-webb Aug 4, 2026
0e7155a
make context_wrapper available again
alexander-alderman-webb Aug 4, 2026
b273a1d
pass usage instead of context to update_invoke_agent_span
alexander-alderman-webb Aug 4, 2026
b5ae6b3
update all use_agent_hooks references to use_run_hooks
alexander-alderman-webb Aug 4, 2026
9c8074d
simplify
alexander-alderman-webb Aug 4, 2026
573480b
.
alexander-alderman-webb Aug 4, 2026
9210726
make mypy happy
alexander-alderman-webb Aug 4, 2026
d555e9c
add missing argument
alexander-alderman-webb Aug 4, 2026
517d837
.
alexander-alderman-webb Aug 4, 2026
8eb6c80
fix(openai-agents): Patch correct run_single_turn_streamed reference
alexander-alderman-webb Aug 4, 2026
002ec0a
merge
alexander-alderman-webb Aug 4, 2026
bca6ba6
make mypy happy
alexander-alderman-webb Aug 4, 2026
9fe9910
Merge branch 'webb/fix-run_single_turn_streamed-patch' into webb/use-…
alexander-alderman-webb Aug 4, 2026
01fd3b1
.
alexander-alderman-webb Aug 4, 2026
0759b8b
close streaming workflow span in all cases
alexander-alderman-webb Aug 4, 2026
a0adffd
handle handoffs
alexander-alderman-webb Aug 5, 2026
8343310
.
alexander-alderman-webb Aug 5, 2026
c9a3196
address review
alexander-alderman-webb Aug 5, 2026
327a4d8
merge master
alexander-alderman-webb Aug 6, 2026
2f01eb7
populate hooks variable
alexander-alderman-webb Aug 6, 2026
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
15 changes: 10 additions & 5 deletions sentry_sdk/integrations/openai_agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ async def new_wrapped_run_single_turn(
*args: "Any", **kwargs: "Any"
) -> "SingleStepResult":
return await _run_single_turn(
run_loop.run_single_turn, *args, **kwargs
run_loop.run_single_turn, use_run_hooks, *args, **kwargs
)

agents.run.run_single_turn = new_wrapped_run_single_turn
Expand All @@ -125,6 +125,7 @@ async def new_wrapped_run_single_turn_streamed(
) -> "SingleStepResult":
return await _run_single_turn_streamed(
original_run_single_turn_streamed,
use_run_hooks,
*args,
**kwargs,
)
Expand All @@ -151,7 +152,7 @@ async def new_wrapped_execute_handoffs(
*args: "Any", **kwargs: "Any"
) -> "SingleStepResult":
return await _execute_handoffs(
original_execute_handoffs, *args, **kwargs
original_execute_handoffs, use_run_hooks, *args, **kwargs
)

agents.run_internal.turn_resolution.execute_handoffs = (
Expand Down Expand Up @@ -207,7 +208,9 @@ def old_wrapped_get_model(
async def old_wrapped_run_single_turn(
cls: "agents.Runner", *args: "Any", **kwargs: "Any"
) -> "SingleStepResult":
return await _run_single_turn(original_run_single_turn, *args, **kwargs)
return await _run_single_turn(
original_run_single_turn, use_run_hooks, *args, **kwargs
)

agents.run.AgentRunner._run_single_turn = classmethod(
old_wrapped_run_single_turn
Expand All @@ -220,7 +223,7 @@ async def old_wrapped_run_single_turn_streamed(
cls: "agents.Runner", *args: "Any", **kwargs: "Any"
) -> "SingleStepResult":
return await _run_single_turn_streamed(
original_run_single_turn_streamed, *args, **kwargs
original_run_single_turn_streamed, use_run_hooks, *args, **kwargs
)

agents.run.AgentRunner._run_single_turn_streamed = classmethod(
Expand All @@ -233,7 +236,9 @@ async def old_wrapped_run_single_turn_streamed(
async def old_wrapped_execute_handoffs(
cls: "agents.Runner", *args: "Any", **kwargs: "Any"
) -> "SingleStepResult":
return await _execute_handoffs(original_execute_handoffs, *args, **kwargs)
return await _execute_handoffs(
original_execute_handoffs, use_run_hooks, *args, **kwargs
Comment thread
sentry-warden[bot] marked this conversation as resolved.
)

agents._run_impl.RunImpl.execute_handoffs = classmethod(
old_wrapped_execute_handoffs
Expand Down
151 changes: 98 additions & 53 deletions sentry_sdk/integrations/openai_agents/patches/agent_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
if TYPE_CHECKING:
from typing import Any, Awaitable, Callable, Optional, Union

from agents import TResponseInputItem
from agents.run_internal.run_steps import SingleStepResult

from sentry_sdk.tracing import Span
Expand Down Expand Up @@ -49,7 +50,7 @@
context_wrapper: "agents.RunContextWrapper",
agent: "agents.Agent",
should_run_agent_start_hooks: bool,
span_kwargs: "dict[str, Any]",
turn_input: "Optional[list[TResponseInputItem]]",
is_streaming: bool = False,
) -> "Optional[Union[Span, StreamedSpan]]":
"""
Expand All @@ -68,14 +69,14 @@
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(
span=span, context=context_wrapper, agent=agent
span=span, usage=context_wrapper.usage, agent=agent
)
span.__exit__(None, None, None)
delattr(context_wrapper, "_sentry_agent_span")

# Store the agent on the context wrapper so we can access it later
context_wrapper._sentry_current_agent = agent
span = invoke_agent_span(context_wrapper, agent, span_kwargs)
span = invoke_agent_span(agent, turn_input)
context_wrapper._sentry_agent_span = span
agent._sentry_agent_span = span

Expand All @@ -92,6 +93,7 @@

async def _run_single_turn(
original_run_single_turn: "Callable[..., Awaitable[SingleStepResult]]",
use_run_hooks: "bool",
*args: "Any",
**kwargs: "Any",
) -> "SingleStepResult":
Expand All @@ -107,39 +109,56 @@
if bindings is not None
else kwargs.get("agent")
)
context_wrapper = kwargs.get("context_wrapper")
should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks", False)

span = _maybe_start_agent_span(
context_wrapper, agent, should_run_agent_start_hooks, kwargs
)
context_wrapper: "agents.RunContextWrapper[Any]" = kwargs.get("context_wrapper")
if not use_run_hooks:
should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks", False)

if (
span is None
or (isinstance(span, StreamedSpan) and span.end_timestamp is not None)
or (not isinstance(span, StreamedSpan) and span.timestamp is not None)
):
return await original_run_single_turn(*args, **kwargs)
span = _maybe_start_agent_span(
context_wrapper,
agent,
should_run_agent_start_hooks,
kwargs.get("input"),
)

if (
span is None
or (isinstance(span, StreamedSpan) and span.end_timestamp is not None)
or (not isinstance(span, StreamedSpan) and span.timestamp is not None)
):
return await original_run_single_turn(*args, **kwargs)

try:
result = await original_run_single_turn(*args, **kwargs)
except Exception:
exc_info = sys.exc_info()
with capture_internal_exceptions():
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(
span=span, context=context_wrapper, agent=agent
)
span.__exit__(*exc_info)
delattr(context_wrapper, "_sentry_agent_span")
if use_run_hooks:
run_hooks = kwargs.get("hooks")
if run_hooks is not None:
Comment on lines +136 to +138

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

To confirm my understanding of what's happening here - are run hooks something that are passed in by users (which is why they could potentially be None)?

span = getattr(run_hooks, "_sentry_invoke_agent_span", None)
Comment thread
alexander-alderman-webb marked this conversation as resolved.
if span is not None:
update_invoke_agent_span(
span=span, usage=context_wrapper.usage, agent=agent
Comment thread
alexander-alderman-webb marked this conversation as resolved.
)
del run_hooks._sentry_invoke_agent_span
span.__exit__(*exc_info)
else:
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(
span=span, usage=context_wrapper.usage, agent=agent
)
span.__exit__(*exc_info)
delattr(context_wrapper, "_sentry_agent_span")
reraise(*exc_info)

return result


async def _run_single_turn_streamed(
original_run_single_turn_streamed: "Callable[..., Awaitable[SingleStepResult]]",
use_run_hooks: "bool",
*args: "Any",
**kwargs: "Any",
) -> "SingleStepResult":
Expand Down Expand Up @@ -181,42 +200,59 @@
args[1] if len(args) > 1 else kwargs.get("bindings", kwargs.get("agent"))
)
agent = getattr(agent_or_bindings, "public_agent", agent_or_bindings)
context_wrapper = args[3] if len(args) > 3 else kwargs.get("context_wrapper")
should_run_agent_start_hooks = bool(
args[5] if len(args) > 5 else kwargs.get("should_run_agent_start_hooks", False)

context_wrapper: "agents.RunContextWrapper[Any]" = (
args[3] if len(args) > 3 else kwargs.get("context_wrapper")
)
if not use_run_hooks:
should_run_agent_start_hooks = bool(
args[5]
if len(args) > 5
else kwargs.get("should_run_agent_start_hooks", False)
)

span_kwargs: "dict[str, Any]" = {}
if streamed_result and hasattr(streamed_result, "input"):
span_kwargs["original_input"] = streamed_result.input
span_kwargs: "dict[str, Any]" = {}
if streamed_result and hasattr(streamed_result, "input"):
span_kwargs["original_input"] = streamed_result.input

span = _maybe_start_agent_span(
context_wrapper,
agent,
should_run_agent_start_hooks,
span_kwargs,
is_streaming=True,
)
span = _maybe_start_agent_span(
context_wrapper,
agent,
should_run_agent_start_hooks,
getattr(streamed_result, "input", None),
is_streaming=True,
)

if (
span is None
or (isinstance(span, StreamedSpan) and span.end_timestamp is not None)
or (not isinstance(span, StreamedSpan) and span.timestamp is not None)
):
return await original_run_single_turn_streamed(*args, **kwargs)
if (
span is None
or (isinstance(span, StreamedSpan) and span.end_timestamp is not None)
or (not isinstance(span, StreamedSpan) and span.timestamp is not None)
):
return await original_run_single_turn_streamed(*args, **kwargs)

try:
result = await original_run_single_turn_streamed(*args, **kwargs)
except Exception:
exc_info = sys.exc_info()
with capture_internal_exceptions():
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(
span=span, context=context_wrapper, agent=agent
)
span.__exit__(*exc_info)
delattr(context_wrapper, "_sentry_agent_span")
if use_run_hooks:
run_hooks = args[2] if len(args) > 2 else kwargs.get("hooks")
if run_hooks is not None:
span = getattr(run_hooks, "_sentry_invoke_agent_span", None)

Check warning on line 241 in sentry_sdk/integrations/openai_agents/patches/agent_run.py

View check run for this annotation

@sentry/warden / warden: code-review

Invoke Agent span leaks when user hooks are present and an exception occurs

The exception handler in `_run_single_turn_streamed` looks for `_sentry_invoke_agent_span` on the `run_hooks` argument, but when a user supplies custom hooks the span is stored on a separate internal instance created by `_patch_run_hooks`, so the lookup returns `None` and the span is never finished.
if span is not None:
update_invoke_agent_span(
span=span, usage=context_wrapper.usage, agent=agent
)
del run_hooks._sentry_invoke_agent_span
span.__exit__(*exc_info)
else:
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
Comment thread
alexander-alderman-webb marked this conversation as resolved.
update_invoke_agent_span(
span=span, usage=context_wrapper.usage, agent=agent
)
span.__exit__(*exc_info)
delattr(context_wrapper, "_sentry_agent_span")
_close_streaming_workflow_span(agent)
reraise(*exc_info)

Expand All @@ -225,6 +261,7 @@

async def _execute_handoffs(
original_execute_handoffs: "Callable[..., SingleStepResult]",
use_run_hooks: "bool",
*args: "Any",
**kwargs: "Any",
) -> "SingleStepResult":
Expand All @@ -241,12 +278,17 @@
agent = kwargs.get("public_agent", kwargs.get("agent"))

# Create Sentry handoff span for the first handoff (agents library only processes the first one)
if run_handoffs:
if not use_run_hooks and run_handoffs:
first_handoff = run_handoffs[0]
handoff_agent_name = first_handoff.handoff.agent_name
handoff_span(context_wrapper, agent, handoff_agent_name)

if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper):
if (
use_run_hooks
or not agent
or not context_wrapper
or not _has_active_agent_span(context_wrapper)
):
Comment thread
sentry[bot] marked this conversation as resolved.
Comment thread
alexander-alderman-webb marked this conversation as resolved.
# Call original method with all parameters
try:
return await original_execute_handoffs(*args, **kwargs)
Expand All @@ -266,15 +308,15 @@
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(
span=span, context=context_wrapper, agent=agent
span=span, usage=context_wrapper.usage, agent=agent
)
span.__exit__(*exc_info)
delattr(context_wrapper, "_sentry_agent_span")
reraise(*exc_info)

span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(span=span, context=context_wrapper, agent=agent)
update_invoke_agent_span(span=span, usage=context_wrapper.usage, agent=agent)
span.__exit__(None, None, None)
delattr(context_wrapper, "_sentry_agent_span")

Expand Down Expand Up @@ -315,7 +357,10 @@
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(
span=span, context=context_wrapper, agent=agent, output=final_output
span=span,
usage=context_wrapper.usage,
agent=agent,
output=final_output,
)
span.__exit__(*exc_info)
delattr(context_wrapper, "_sentry_agent_span")
Expand All @@ -324,7 +369,7 @@
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(
span=span, context=context_wrapper, agent=agent, output=final_output
span=span, usage=context_wrapper.usage, agent=agent, output=final_output
)
span.__exit__(None, None, None)
delattr(context_wrapper, "_sentry_agent_span")
Expand Down
Loading
Loading