Skip to content

Commit 19c81f2

Browse files
committed
Better hooking into agent invokation
1 parent 145653e commit 19c81f2

File tree

6 files changed

+170
-89
lines changed

6 files changed

+170
-89
lines changed

sentry_sdk/integrations/openai_agents/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
_create_get_model_wrapper,
55
_create_get_all_tools_wrapper,
66
_create_run_wrapper,
7+
_patch_agent_run,
78
)
89

910
try:
@@ -15,15 +16,16 @@
1516

1617
def _patch_runner():
1718
# type: () -> None
18-
19-
# Creating agent workflow spans
19+
# Create the root span for one full agent run (including eventual handoffs)
2020
# Note agents.run.DEFAULT_AGENT_RUNNER.run_sync is a wrapper around
2121
# agents.run.DEFAULT_AGENT_RUNNER.run. It does not need to be wrapped separately.
22+
# TODO-anton: Also patch streaming runner: agents.Runner.run_streamed
2223
agents.run.DEFAULT_AGENT_RUNNER.run = _create_run_wrapper(
2324
agents.run.DEFAULT_AGENT_RUNNER.run
2425
)
2526

26-
# TODO-anton: Also patch streaming runner: agents.Runner.run_streamed
27+
# Creating the actual spans for each agent run.
28+
_patch_agent_run()
2729

2830

2931
def _patch_model():
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .models import _create_get_model_wrapper # noqa: F401
22
from .tools import _create_get_all_tools_wrapper # noqa: F401
33
from .runner import _create_run_wrapper # noqa: F401
4+
from .agent_run import _patch_agent_run # noqa: F401
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
from functools import wraps
2+
3+
from sentry_sdk.integrations import DidNotEnable
4+
5+
from ..spans import invoke_agent_span, update_invoke_agent_span
6+
7+
8+
try:
9+
import agents
10+
except ImportError:
11+
raise DidNotEnable("OpenAI Agents not installed")
12+
13+
14+
def _patch_agent_run():
15+
"""
16+
Patches AgentRunner methods to create agent invocation spans without using RunHooks.
17+
This directly patches the execution flow to track when agents start and stop.
18+
"""
19+
20+
# Store original methods
21+
original_run_single_turn = agents.run.AgentRunner._run_single_turn
22+
original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs
23+
original_execute_final_output = agents._run_impl.RunImpl.execute_final_output
24+
25+
def _start_invoke_agent_span(context_wrapper, agent):
26+
"""Start an agent invocation span"""
27+
# Store the agent on the context wrapper so we can access it later
28+
context_wrapper._sentry_current_agent = agent
29+
invoke_agent_span(context_wrapper, agent)
30+
31+
def _end_invoke_agent_span(context_wrapper, agent, output):
32+
"""End the agent invocation span"""
33+
# Clear the stored agent
34+
if hasattr(context_wrapper, "_sentry_current_agent"):
35+
delattr(context_wrapper, "_sentry_current_agent")
36+
37+
update_invoke_agent_span(context_wrapper, agent, output)
38+
39+
def _has_active_agent_span(context_wrapper):
40+
"""Check if there's an active agent span for this context"""
41+
return hasattr(context_wrapper, "_sentry_current_agent")
42+
43+
def _get_current_agent(context_wrapper):
44+
"""Get the current agent from context wrapper"""
45+
return getattr(context_wrapper, "_sentry_current_agent", None)
46+
47+
@wraps(original_run_single_turn)
48+
async def patched_run_single_turn(
49+
cls,
50+
*,
51+
agent,
52+
all_tools,
53+
original_input,
54+
generated_items,
55+
hooks,
56+
context_wrapper,
57+
run_config,
58+
should_run_agent_start_hooks,
59+
tool_use_tracker,
60+
previous_response_id,
61+
):
62+
"""Patched _run_single_turn that creates agent invocation spans"""
63+
64+
# Start agent span when agent starts (but only once per agent)
65+
if should_run_agent_start_hooks and agent and context_wrapper:
66+
# End any existing span for a different agent
67+
if _has_active_agent_span(context_wrapper):
68+
current_agent = _get_current_agent(context_wrapper)
69+
if current_agent and current_agent != agent:
70+
_end_invoke_agent_span(context_wrapper, current_agent, None)
71+
72+
_start_invoke_agent_span(context_wrapper, agent)
73+
74+
# Call original method with all the correct parameters
75+
result = await original_run_single_turn(
76+
agent=agent,
77+
all_tools=all_tools,
78+
original_input=original_input,
79+
generated_items=generated_items,
80+
hooks=hooks,
81+
context_wrapper=context_wrapper,
82+
run_config=run_config,
83+
should_run_agent_start_hooks=should_run_agent_start_hooks,
84+
tool_use_tracker=tool_use_tracker,
85+
previous_response_id=previous_response_id,
86+
)
87+
88+
return result
89+
90+
@wraps(original_execute_handoffs)
91+
async def patched_execute_handoffs(
92+
cls,
93+
*,
94+
agent,
95+
original_input,
96+
pre_step_items,
97+
new_step_items,
98+
new_response,
99+
run_handoffs,
100+
hooks,
101+
context_wrapper,
102+
run_config,
103+
):
104+
"""Patched execute_handoffs that ends agent span for handoffs"""
105+
106+
# Call original method with all parameters
107+
result = await original_execute_handoffs(
108+
agent=agent,
109+
original_input=original_input,
110+
pre_step_items=pre_step_items,
111+
new_step_items=new_step_items,
112+
new_response=new_response,
113+
run_handoffs=run_handoffs,
114+
hooks=hooks,
115+
context_wrapper=context_wrapper,
116+
run_config=run_config,
117+
)
118+
119+
# End span for current agent after handoff processing is complete
120+
if agent and context_wrapper and _has_active_agent_span(context_wrapper):
121+
_end_invoke_agent_span(context_wrapper, agent, None)
122+
123+
return result
124+
125+
@wraps(original_execute_final_output)
126+
async def patched_execute_final_output(
127+
cls,
128+
*,
129+
agent,
130+
original_input,
131+
new_response,
132+
pre_step_items,
133+
new_step_items,
134+
final_output,
135+
hooks,
136+
context_wrapper,
137+
):
138+
"""Patched execute_final_output that ends agent span for final outputs"""
139+
140+
# Call original method with all parameters
141+
result = await original_execute_final_output(
142+
agent=agent,
143+
original_input=original_input,
144+
new_response=new_response,
145+
pre_step_items=pre_step_items,
146+
new_step_items=new_step_items,
147+
final_output=final_output,
148+
hooks=hooks,
149+
context_wrapper=context_wrapper,
150+
)
151+
152+
# End span for current agent after final output processing is complete
153+
if agent and context_wrapper and _has_active_agent_span(context_wrapper):
154+
_end_invoke_agent_span(context_wrapper, agent, final_output)
155+
156+
return result
157+
158+
# Apply patches
159+
agents.run.AgentRunner._run_single_turn = classmethod(patched_run_single_turn)
160+
agents._run_impl.RunImpl.execute_handoffs = classmethod(patched_execute_handoffs)
161+
agents._run_impl.RunImpl.execute_final_output = classmethod(
162+
patched_execute_final_output
163+
)

sentry_sdk/integrations/openai_agents/patches/runner.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
agent_workflow_span,
99
update_agent_workflow_span,
1010
)
11-
from ..utils import _capture_exception, _wrap_hooks
11+
from ..utils import _capture_exception
1212

1313
from typing import TYPE_CHECKING
1414

@@ -28,7 +28,6 @@ async def async_wrapper(*args, **kwargs):
2828
# type: (*Any, **Any) -> Any
2929
agent = args[0]
3030
with agent_workflow_span(agent) as span:
31-
kwargs["hooks"] = _wrap_hooks(kwargs.get("hooks"))
3231
result = None
3332
try:
3433
result = await original_func(*args, **kwargs)

sentry_sdk/integrations/openai_agents/run_hooks.py

Lines changed: 0 additions & 37 deletions
This file was deleted.

sentry_sdk/integrations/openai_agents/utils.py

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from functools import wraps
21
import sentry_sdk
32
from sentry_sdk.consts import SPANDATA
43
from sentry_sdk.integrations import DidNotEnable
@@ -156,49 +155,3 @@ def _set_output_data(span, result):
156155

157156
if len(output_messages["response"]) > 0:
158157
span.set_data(SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"])
159-
160-
161-
def _create_hook_wrapper(original_hook, sentry_hook):
162-
# type: (Callable[..., Any], Callable[..., Any]) -> Callable[..., Any]
163-
@wraps(original_hook)
164-
async def async_wrapper(*args, **kwargs):
165-
# type: (*Any, **Any) -> Any
166-
await sentry_hook(*args, **kwargs)
167-
return await original_hook(*args, **kwargs)
168-
169-
return async_wrapper
170-
171-
172-
def _wrap_hooks(hooks):
173-
# type: (agents.RunHooks) -> agents.RunHooks
174-
"""
175-
Our integration uses RunHooks to create spans. This function will either
176-
enable our SentryRunHooks or if the users has given custom RunHooks wrap
177-
them so the Sentry hooks and the users hooks are both called
178-
"""
179-
from .run_hooks import SentryRunHooks
180-
181-
sentry_hooks = SentryRunHooks()
182-
183-
if hooks is None:
184-
return sentry_hooks
185-
186-
wrapped_hooks = type("SentryWrappedHooks", (hooks.__class__,), {})
187-
188-
# Wrap all methods from RunHooks
189-
for method_name in dir(agents.RunHooks):
190-
if method_name.startswith("on_"):
191-
original_method = getattr(hooks, method_name)
192-
# Only wrap if the method exists in SentryRunHooks
193-
try:
194-
sentry_method = getattr(sentry_hooks, method_name)
195-
setattr(
196-
wrapped_hooks,
197-
method_name,
198-
_create_hook_wrapper(original_method, sentry_method),
199-
)
200-
except AttributeError:
201-
# If method doesn't exist in SentryRunHooks, just use the original method
202-
setattr(wrapped_hooks, method_name, original_method)
203-
204-
return wrapped_hooks()

0 commit comments

Comments
 (0)