Repository navigation
Python: Keep pending Anthropic server tool calls as server_tool_use - #9513
Shivam Sharma (ShivamSharma43) wants to merge 2 commits into
Conversation
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>
| if content.type == "function_call" | ||
| and content.hosted | ||
| and content.call_id | ||
| and content.call_id not in result_call_ids |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
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 localtool_useplus a pendingserver_tool_use. The parser keeps the hosted call as an informationalfunction_callwithhosted=True, but_prepare_message_for_anthropicsends everyfunction_callback astool_use. Anthropic then expects a localtool_resultfor 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 stayserver_tool_use.Description & Review Guide
_prepare_messages_for_anthropiccollects the hosted calls that have nofunction_resultanywhere in the conversation and passes them down to_prepare_message_for_anthropic, which sends those calls asserver_tool_useinstead oftool_use. The hosted call is found from the call's ownhostedflag, which survivesto_dict/from_dict, so a reloaded session behaves the same.assistant [tool_use(local), server_tool_use(srv)]followed byuser [tool_result(local)]. Server tool calls that already have a result in the conversation keep today'stool_use/tool_resultpairing. I left those alone on purpose: the parsedweb_search_tool_resultcontent is stored stringified on theContent, so a native result block (withencrypted_content) can't be rebuilt from history. Switching only the call type would leave the result without a matchingtool_use.Tests:
test_prepare_messages_preserves_parallel_server_tool_use(in memory and after ato_dict/from_dictreload, from realBetaToolUseBlock/BetaServerToolUseBlockparsing) fails on main and passes with the change.test_prepare_messages_keeps_completed_server_tool_call_paired_with_its_resultpins 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 inagent_framework_anthropiccompared 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 (
reproducedlabel, 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
AI assistance details: implementation, tests and this PR description were drafted with Claude Code and reviewed before submitting.
Contribution Checklist
breaking changelabel (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