feat(pydantic_ai): Add instrumentation for Agent.iter()#5607
feat(pydantic_ai): Add instrumentation for Agent.iter()#5607dfm88 wants to merge 1 commit intogetsentry:masterfrom
Conversation
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨
Bug Fixes 🐛
Documentation 📚
Internal Changes 🔧
🤖 This preview updates automatically when you update the PR. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| # AgentRun (from iter()) wraps the final result in .result; | ||
| # StreamedRunResult (from run_stream()) is used directly. | ||
| if isinstance(self._result, AgentRun): | ||
| result = self._result.result |
There was a problem hiding this comment.
Unprotected AgentRun.result access may raise in __aexit__
Medium Severity
Accessing self._result.result on an AgentRun in __aexit__ is unprotected. According to pydantic-ai docs, AgentRun.result is only available after the run reaches an End node. If a user exits the iter() context manager early (e.g., break in the iteration loop), __aexit__ is called with exc_type=None but the run hasn't completed, so .result may raise. This causes instrumentation code to introduce an unexpected exception to the user. The access needs a try/except guard, similar to how update_invoke_agent_span protects .usage() and .response access.
There was a problem hiding this comment.
The .result property on AgentRun safely returns None when the iteration hasn't ended — it does not raise. This has been the case since pydantic-ai v1.0.0, which is the minimum version supported by Sentry for this integration (docs).
Here's the implementation from the v1.0.0 tag: pydantic_ai/run.py#L122-L138


Description
When using
Agent.iter()to drive an agent step by step (common in streaming or WebSocket scenarios), no Sentry spans are created at all. Tool calls, model requests, and the agent invocation itself are completely invisible. Onlyrun()andrun_stream()were instrumented.sentry_sdk/integrations/pydantic_ai/patches/agent_run.pyPatched
Agent.iterusing the existing_create_streaming_wrapper, withis_streaming=Falsesinceiter()is node-by-node iteration, not token streaming.Added passthrough detection in
_StreamingContextManagerWrapper.__aenter__: if an agent is already on the contextvar stack (meaning we're inside arun()orrun_stream()call that already set up instrumentation), we skip creating a newinvoke_agentspan and just forward to the original context manager. This is needed because in pydantic-ai v1.x, bothrun()andrun_stream()internally calliter().Used
isinstance(self._result, AgentRun)to distinguish between the two result types that flow through the wrapper:AgentRun(fromiter()) wraps the final result in its.resultproperty, whileStreamedRunResult(fromrun_stream()) can be passed directly toupdate_invoke_agent_span. This follows the project's guideline of preferringisinstance()overhasattr().Parameterized
is_streamingin_create_streaming_wrapper— it was previously hardcoded toTrue, which was correct forrun_stream()but wrong foriter().Issues
Reminders
tox -e linters.feat:,fix:,ref:,meta:)