Skip to content

Python: Keep pending Anthropic server tool calls as server_tool_use - #9513

Open
Shivam Sharma (ShivamSharma43) wants to merge 2 commits into
microsoft:mainfrom
ShivamSharma43:fix/9478-anthropic-server-tool-use
Open

Shivam Sharma (ShivamSharma43) wants to merge 2 commits into
microsoft:mainfrom
ShivamSharma43:fix/9478-anthropic-server-tool-use

Conversation

@ShivamSharma43

Copy link
Copy Markdown
Contributor

Motivation & Context

When Claude calls a local tool and a server tool (for example get_web_search_tool()) in the same turn, the response ends with a local tool_use plus a pending server_tool_use. The parser keeps the hosted call as an informational function_call with hosted=True, but _prepare_message_for_anthropic sends every function_call back as tool_use. Anthropic then expects a local tool_result for the server call and rejects the next request with HTTP 400 (tool_use ids were found without tool_result blocks immediately after: srvtoolu_...). Per Anthropic's server-tool docs, the client returns the local result and the API runs the pending server tool on the next request, which needs the block to stay server_tool_use.

Description & Review Guide

  • What are the major changes? _prepare_messages_for_anthropic collects the hosted calls that have no function_result anywhere in the conversation and passes them down to _prepare_message_for_anthropic, which sends those calls as server_tool_use instead of tool_use. The hosted call is found from the call's own hosted flag, which survives to_dict/from_dict, so a reloaded session behaves the same.
  • What is the impact of these changes? The parallel local plus server tool turn from the issue now produces assistant [tool_use(local), server_tool_use(srv)] followed by user [tool_result(local)]. Server tool calls that already have a result in the conversation keep today's tool_use/tool_result pairing. I left those alone on purpose: the parsed web_search_tool_result content is stored stringified on the Content, so a native result block (with encrypted_content) can't be rebuilt from history. Switching only the call type would leave the result without a matching tool_use.
  • What do you want reviewers to focus on? Whether keeping completed server tool calls on the existing pairing is the right scope, or whether you'd rather round-trip them natively in a follow-up (which would need the structured result kept on the content).

Tests: test_prepare_messages_preserves_parallel_server_tool_use (in memory and after a to_dict/from_dict reload, from real BetaToolUseBlock/BetaServerToolUseBlock parsing) fails on main and passes with the change. test_prepare_messages_keeps_completed_server_tool_call_paired_with_its_result pins the unchanged pairing once the search result arrives. The anthropic package unit tests pass (204), ruff check/format are clean, and mypy/pyright report no new errors in agent_framework_anthropic compared with main. I did not run it against the live Anthropic API.

Related Issue

Fixes #9478

The issue was reproduced by the repository's triage workflow (reproduced label, assigned to Eduard van Valkenburg (@eavanvalkenburg)). It has no maintainer comment on the direction yet, and no open PR references it. Happy to adjust or close if you'd rather handle it differently.

AI Assistance

  • No material AI assistance was used.
  • This is an AI-assisted contribution. I reviewed, understood, and verified all submitted content and accept responsibility for it.

AI assistance details: implementation, tests and this PR description were drafted with Claude Code and reviewed before submitting.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR links to an agreed issue with no competing open PR, or the Related Issue section documents a trivial-change or repository-automation exception.
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

🤖 Generated with Claude Code

When Claude calls a local tool and a server tool (such as web search) in
parallel, the response ends with a pending server_tool_use. The hosted call
was sent back as a plain tool_use, so Anthropic expected a local tool_result
for it and rejected the next request with HTTP 400. Hosted calls that have
no result in the conversation are now sent back as server_tool_use, so the
API runs the server tool on the next request.

Fixes microsoft#9478

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@agent-framework-automation agent-framework-automation Bot added the python Usage: [Issues, PRs], Target: Python label Oct 18, 2026
if content.type == "function_call"
and content.hosted
and content.call_id
and content.call_id not in result_call_ids

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.

The global result_call_ids set treats call_id as unique across the transcript. Spec 004 permits later occurrences to reuse an ID, so an earlier result causes a later pending hosted call to serialize as tool_use, recreating the missing-result 400. Please correlate occurrences in transcript order—using Content.id when available—and add a reused-ID regression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, fixed in a03b285. _pending_hosted_function_calls now walks the transcript in order: each function_result settles the open call occurrence whose Content.id it carries, otherwise the earliest open occurrence with that call_id. It never settles a call that comes after it, which matches how _replace_approval_contents_with_results closes occurrences in core. Pending hosted calls are tracked by content identity rather than call_id, so an earlier completed call with the same ID still goes out as tool_use + tool_result, and the later pending one goes out as server_tool_use. Added regressions for a reused ID across turns (in memory and reloaded), a result that names a specific occurrence through Content.id, and a result that appears before the call.

…ipt order

A call_id can be reused by a later occurrence, so a global set of result
call IDs let an earlier result mark a later pending server tool call as
settled. It was then sent as tool_use and Anthropic rejected the request
for a missing tool_result.

Walk the transcript in order instead: each result settles the open call
occurrence named by its Content.id, otherwise the earliest open occurrence
with that call_id, and never a call that appears after it. Pending hosted
calls are tracked by content identity rather than call_id.

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

This branch was successfully deployed

1 active deployment
github-app-auth — a03b285b Deployed Oct 23, 2026 by ShivamSharma43 via add_label #26019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: Anthropic parallel hosted web search and local tool calls lose server_tool_use identity

3 participants