getsentry / getsentry/sentry-python

openai_agents: tool wrapper is re-applied on every turn

Đang mở
#7,301 2 bình luận 0 reaction 1 người được giao Được @alexander-alderman-webb nhận Xem trên GitHub
Bug Python Spans
Ngôn ngữ chính
Python
Star
2.2k
Fork
669
Merge trung bình
1 ngày 1 giờ
Pull request đã merge (30 ngày)
213

Mô tả

### How do you use Sentry?

Sentry SaaS (sentry.io)

### Version

2.68.1

### Steps to Reproduce

We ran into the tool-rebuild problem that #6961 fixed. Our agent crash-looped on `UserError: ToolSearchTool() requires at least one searchable Responses surface`, because `defer_loading` came back `False` on every tool. Moving to 2.67.0 fixed it for us, and the switch to `RunHooks` in #6994 looks like the right call.

While reading that fix I noticed one thing still worth reporting in `patches/tools.py`. It now sets `on_invoke_tool` in place:

```python
tool.on_invoke_tool = create_wrapped_invoke(tool, original_on_invoke)
```

`get_all_tools` returns the agent's own tool objects and runs once per turn, so the wrapper goes on again on each turn and the previous one stays underneath it.

That file is only wired up for `openai-agents < 0.3.2`, so this repro calls `_get_all_tools` directly with a stub standing in for the original:

```python
import asyncio, sentry_sdk
sentry_sdk.init(dsn="https://a@b.ingest.sentry.io/1", traces_sample_rate=1.0)
from sentry_sdk.integrations.openai_agents.patches.tools import _get_all_tools
from agents import Agent, FunctionTool

async def invoke(ctx, args): return "ok"

tool = FunctionTool(
name="t", description="d",
params_json_schema={"type": "object", "properties": {}, "additionalProperties": False},
on_invoke_tool=invoke,
)
agent = Agent(name="a", tools=[tool])

async def original(a, c): return list(a.tools) # stands in for get_all_tools

async def main():
for turn in range(1, 6):
await _get_all_tools(original, agent, None)
f, depth = agent.tools[0].on_invoke_tool, 0
while hasattr(f, "__wrapped__"):
f, depth = f.__wrapped__, depth + 1
print(f"after turn {turn}: wrapper depth = {depth}")

asyncio.run(main())
```

### Expected Result

A tool gets wrapped once, however many turns a run takes, and one tool call produces one `gen_ai.execute_tool` span.

### Actual Result

```
after turn 1: wrapper depth = 1
after turn 2: wrapper depth = 2
after turn 3: wrapper depth = 3
after turn 4: wrapper depth = 4
after turn 5: wrapper depth = 5
```

A tool call on turn N opens N nested `gen_ai.execute_tool` spans, and the wrapping stays on the caller's `Agent` after the run.

`_patch_run_hooks` in `patches/runner.py` already guards against this with `_sentry_is_patched`. The same flag on the tool would cover it.

Tested on Python 3.13 with `openai-agents` 0.17.5.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.