Skip to content

feat(server-utils): Emit execute_tool spans for Anthropic's beta.messages.toolRunner() - #25320

Open
RulaKhaled wants to merge 7 commits into
developfrom
feat/anthropic-tool-runner-execute-tool-spans
Open

RulaKhaled wants to merge 7 commits into
developfrom
feat/anthropic-tool-runner-execute-tool-spans

Conversation

@RulaKhaled

Copy link
Copy Markdown
Collaborator

The tool runner runs the user's tools, but Sentry only recorded its chat calls. Tool calls got no span, and when a tool threw, the runner turned the error into an is_error result for the model, so nobody saw it.

This wraps each tool's run() in place with fill(), which the runner calls on every version since 0.63. When a tool throws, its span is marked as an error and the exception is captured. Intentional ToolError results and aborted calls are marked but not captured.

Fixes #25288

🤖 Generated with Claude Code

RulaKhaled and others added 3 commits October 13, 2026 13:33
…essages.toolRunner()`

Wraps each runnable tool's `run()` in place with `fill()`, covering tools
passed to `toolRunner()`, `addTools()` and `setMessagesParams()`, on both the
orchestrion and `instrumentAnthropicAiClient` paths. A tool that throws gets an
error span and a captured event, since the runner hides the error from the app.

Fixes #25288

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… setting them to undefined

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…echanism type

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 29.83 kB - -
@sentry/browser - with treeshaking flags 27.96 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.87 kB - -
@sentry/browser (incl. Tracing) 52.02 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 52.02 kB - -
@sentry/browser (incl. Tracing, Profiling) 54.96 kB - -
@sentry/browser (incl. Tracing, Replay) 91.75 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 80.62 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 96.47 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 109.43 kB - -
@sentry/browser (incl. Feedback) 47.35 kB - -
@sentry/browser (incl. sendFeedback) 34.89 kB - -
@sentry/browser (incl. FeedbackAsync) 40 kB - -
@sentry/browser (incl. Metrics) 30.85 kB - -
@sentry/browser (incl. Logs) 31.13 kB - -
@sentry/browser (incl. Metrics & Logs) 31.78 kB - -
@sentry/react 31.64 kB - -
@sentry/react (incl. Tracing) 54.34 kB - -
@sentry/vue 37.98 kB - -
@sentry/vue (incl. Tracing) 54.95 kB - -
@sentry/svelte 29.86 kB - -
@sentry/remix (Remix 3 client bundle) 57.02 kB - -
CDN Bundle 31.56 kB - -
CDN Bundle (incl. Tracing) 52.52 kB - -
CDN Bundle (incl. Logs, Metrics) 33.77 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 54.46 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 74.7 kB - -
CDN Bundle (incl. Tracing, Replay) 90.19 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 92.11 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 96.34 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 98.28 kB - -
CDN Bundle - uncompressed 92.98 kB - -
CDN Bundle (incl. Tracing) - uncompressed 155.83 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 99.52 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 161.79 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 229.56 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 276 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 281.94 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 289.7 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 295.63 kB - -
@sentry/nextjs (client) 56.75 kB - -
@sentry/sveltekit (client) 52.41 kB - -
@sentry/core/server 41.12 kB - -
@sentry/core/browser 13.81 kB - -
@sentry/node 152.79 kB +0.25% +368 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 83.83 kB +0.03% +20 B 🔺
@sentry/node - without tracing 94.59 kB +0.03% +24 B 🔺
@sentry/node - without channel injection 131.02 kB +0.29% +376 B 🔺
@sentry/aws-serverless 102.75 kB +0.03% +25 B 🔺
@sentry/cloudflare (withSentry) - minified 210.38 kB - -
@sentry/cloudflare (withSentry) 521.61 kB - -
@sentry/nextjs/cloudflare (withSentry) - minified 228.06 kB - -

View base workflow run

@RulaKhaled

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RulaKhaled
RulaKhaled marked this pull request as ready for review October 13, 2026 12:16
@RulaKhaled
RulaKhaled requested a review from a team as a code owner October 13, 2026 12:16
@RulaKhaled
RulaKhaled requested review from andreiborza, isaacs, mydea and nicohrubec and removed request for a team October 13, 2026 12:16
Comment thread packages/server-utils/src/ai/anthropic-ai/tool-runner.ts Outdated

@isaacs isaacs left a comment

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.

Wrapipng tool objects in place rather than wrapping the runner or proxying is the right choice for this SDK: the runner uses private class fields, the client code already avoids Proxies for that reason, and tool.run is the one call point that is stable from 0.63 to 0.133.

Capturing the swallowed error is the useful part of the change, since otherwise those errors disappear.

Open questions are what counts as "handled", and keeping the run contract narrow enough that new kinds of tools entries, like toolsets, don't get wrapped by accident. Otherwise LGTM!

Comment thread packages/server-utils/src/ai/anthropic-ai/tool-runner.ts Outdated
Comment thread packages/server-utils/src/ai/anthropic-ai/tool-runner.ts Outdated
Comment thread packages/server-utils/src/ai/anthropic-ai/tool-runner.ts
Comment thread packages/server-utils/src/ai/anthropic-ai/tool-runner.ts Outdated
RulaKhaled and others added 2 commits October 14, 2026 13:05
…rors as handled

Toolsets are nameless `tools` entries whose `run` takes `(ctx, toolUse)`, so
only entries with a string `name` are wrapped now. Tool errors the runner hands
back to the model are captured with `handled: true`, as Flue and Mastra do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5e0cfc3. Configure here.

Comment thread packages/server-utils/src/ai/anthropic-ai/tool-runner.ts
…on older SDKs

SDK 0.85 to 0.96 pass only `toolUseBlock` to a tool's `run()`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@isaacs isaacs left a comment

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.

This looks great!

Anthropic SDK 0.133 adds client.beta.sessions.events.toolRunner(sessionID, { tools }) (lib/tools/SessionToolRunner.ts), which also runs BetaRunnableTools. Those tools get no spans unless the same objects were also given to a beta.messages.toolRunner() (in which case they get spans with the wrong framing, see 1). I think that's out of scope for this PR, but worth a follow-up issue so the PR body saying "the tool runner" isn't read as full coverage.

})),
...['resources/beta/messages/messages.js', 'resources/beta/messages/messages.mjs'].map(filePath => ({
channelName: 'tool-runner',
module: { name: '@anthropic-ai/sdk', versionRange: '>=0.19.2 <1', filePath },

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.

toolRunner only exists from 0.63, so we can tighten this up a little bit :)

Suggested change
module: { name: '@anthropic-ai/sdk', versionRange: '>=0.19.2 <1', filePath },
module: { name: '@anthropic-ai/sdk', versionRange: '>=0.63.0 <1', filePath },

// Handled: the runner turns the throw into an error result for the model, so the app never sees it.
// A `ToolError` is the SDK's way to send that result on purpose.
if ((error as Error | undefined)?.name !== 'ToolError') {
captureException(error, { mechanism: { handled: true, type: ANTHROPIC_AI_ORIGIN } });

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.

So, fill replaces run on the user's own tool object for the life of the process. Tool objects are often module-level constants, reused in other places: a direct call in app code or tests, an MCP server, another agent framework, or client.beta.sessions.events.toolRunner().

Every such call now gets an execute_tool span with origin: auto.ai.anthropic, and any throw gets captured with mechanism: { handled: true, type: 'auto.ai.anthropic' }. In those contexts the caller does see the error, so "handled by the Anthropic runner" is wrong. The __sentry_captured__ guard stops a second capture when the app also reports it, but the first (wrong) mechanism wins.

There is no clean way to tell a runner call from a direct call on 0.63 to 0.80 (no context arg). On 0.85+ the runner always passes a context object with toolUseBlock.

I don't think we really need to change the code here, but we might want to document this in the PR body and/or a code comment, since it's a deliberate tradeoff. Maybe we could only capture when context?.toolUse ?? context?.toolUseBlock is present, which is true for every runner call on 0.85+ and false for most direct calls. That would keep the span everywhere but limit the "handled" capture to calls we know came from the runner. Older SDKs would then lose capture, which may be acceptable, or keep capture there unconditionally.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(server-utils): Emit execute_tool spans for Anthropic's beta.messages.toolRunner()

3 participants