ref(openai-agents): Use first class agent hooks when available - #7029
ref(openai-agents): Use first class agent hooks when available#7029alexander-alderman-webb wants to merge 15 commits into
Conversation
Codecov Results 📊✅ 95803 passed | ⏭️ 6239 skipped | Total: 102042 | Pass Rate: 93.89% | Execution Time: 350m 33s 📊 Comparison with Base Branch
All tests are passing successfully. ✅ Patch coverage is 94.12%. Project has 2486 uncovered lines. Files with missing lines (3)
Coverage diff@@ Coverage Diff @@
## main #PR +/-##
==========================================
+ Coverage 89.90% 90.02% +0.12%
==========================================
Files 193 193 —
Lines 24825 24919 +94
Branches 8922 8954 +32
==========================================
+ Hits 22319 22433 +114
- Misses 2506 2486 -20
- Partials 1415 1421 +6Generated by Codecov Action |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 01fd3b1. Configure here.
| context: "AgentHookContext[TContext]", | ||
| agent: "Agent[TContext]", | ||
| ) -> "None": | ||
| self._sentry_invoke_agent_span = invoke_agent_span(agent, context.turn_input) |
There was a problem hiding this comment.
Handoffs leak invoke agent spans
High Severity
With use_run_hooks, invoke agent spans live on the hooks instance and are finished in on_agent_end, which only runs on final output. Handoffs never close the prior span: on_agent_start overwrites _sentry_invoke_agent_span, and _execute_handoffs still looks for context_wrapper._sentry_agent_span, which is never set on this path. Each handoff therefore leaves an unfinished invoke agent span.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 01fd3b1. Configure here.
| 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: |
There was a problem hiding this comment.
Invoke agent span leaks on exception when user-provided hooks are used
The exception handler retrieves _sentry_invoke_agent_span from run_hooks, but when the user provides a RunHooks instance _patch_run_hooks stores the span on a separate internal _SentryRunHooks object, so the span is never finished and leaks on every exception.
Evidence
run_hooksis retrieved fromargs[2]/kwargs.get("hooks"), which is the user-providedRunHooksinstance._patch_run_hooksinrunner.py:129creates a separate_SentryRunHooks()instance (sentry_hooks) whoseon_agent_startstores_sentry_invoke_agent_spanon that private object.- The patched
on_agent_startdelegates tosentry_hooks.on_agent_start, but the user's hooks object never receives the_sentry_invoke_agent_spanattribute. getattr(run_hooks, "_sentry_invoke_agent_span", None)therefore always resolves toNoneon patched user hooks.- Because the span is never updated or exited, it leaks on every exception path when custom hooks are present.
Also found at 2 additional locations
sentry_sdk/integrations/openai_agents/patches/agent_run.py:139sentry_sdk/integrations/openai_agents/patches/runner.py:131-131
Identified by Warden · find-bugs · XD7-X2Q
|
|
||
| def update_invoke_agent_span( | ||
| span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", | ||
| context: "agents.RunContextWrapper", | ||
| usage: "Optional[Usage]", | ||
| agent: "agents.Agent", | ||
| output: "Any" = None, | ||
| ) -> None: | ||
| # Add aggregated usage data from context_wrapper | ||
| if hasattr(context, "usage"): | ||
| _set_usage_data(span, context.usage) | ||
| if usage is not None: | ||
| _set_usage_data(span, usage) | ||
|
|
There was a problem hiding this comment.
update_invoke_agent_span lost defensive usage guard, callers crash on missing usage attribute
The update_invoke_agent_span signature changed from taking context (with a hasattr(context, "usage") guard) to taking usage directly. Callers now pass context_wrapper.usage or context.usage without defensive guards. If the context object lacks a usage attribute, callers raise AttributeError inside capture_internal_exceptions(), which suppresses the error but skips span cleanup—leaving spans unfinished.
Evidence
update_invoke_agent_spanpreviously checkedhasattr(context, "usage")before accessing.usage; the new signature only checksif usage is not None.- Callers in
patches/agent_run.pyandpatches/runner.pypasscontext_wrapper.usageorcontext.usagedirectly withoutgetattrorhasattrguards (e.g.,agent_run.py:71,agent_run.py:141,runner.py:68). - Exception handlers in
_run_single_turn(agent_run.py:136-146) and_run_single_turn_streamed(agent_run.py:238-248) wrap these calls insidecapture_internal_exceptions(), whose__exit__returnsTrue, swallowing anyAttributeErrorand blocking thedel/span.__exit__cleanup that follows. - The same pattern exists in
_SentryRunHooks.on_agent_end(runner.py:66-70), which is executed through a_patch_run_hookswrapper that also usescapture_internal_exceptions(), so a missing.usagethere silently leaks the invoke-agent span on the hooks instance.
Identified by Warden · find-bugs · JK8-BDR


Description
Add
on_agent_start()andon_agent_end()hooks.Unlike tool hooks, the Invoke Agent span is stored on the
RunHooksinstance.If the hooks are active, start and finish Invoke Agent spans in the hooks in successful executions. If an exception bubbles up,
on_agent_end()does not run. Modify monkey-patches so thatRunHooksspan is finished if the hooks are present.Make the signatures of
invoke_agent_span(),update_invoke_agent_span()and_maybe_start_agent_span()more minimal so that they can be re-used in theon_agent_start()andon_agent_end()hooks.Issues
Closes #6989
Reminders
uv run ruff.feat:,fix:,ref:,meta:)