Repository navigation
Replay Anthropic web search and web fetch turns as server tool blocks - #178
Merged
Merged
Conversation
A restored transcript rebuilds the code-execution result blocks from the typed contents, but two payload shapes were rebuilt wrongly: - A payload variant this build does not model is preserved as unknown content on receive, yet the rebuild ignored it and invented an empty success payload (return_code 0) for code execution, bash and the text editor. It is now sent back exactly as it arrived. - An encrypted_code_execution_result surfaces as plain text, so the rebuild sent the ciphertext back as plaintext stdout. The text output now carries additionalProperties.encrypted, and the rebuild restores the encrypted_code_execution_result / encrypted_stdout shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A malformed code-execution file entry and an unmodelled payload variant both arrive as unknown content among the result's outputs, so a lone entry of an unexpected type was replayed after a restore as if it were the whole payload. The receive side now marks such entries with additionalProperties.fileEntry; the rebuild keeps them inside the rebuilt result and strips the marker from the wire block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unknown content carries the wire entry's fields, so a malformed entry that already had additionalProperties lost them when the file-entry marker replaced the record. The marker now joins the existing record, and the rebuild strips only the marker. An entry whose additionalProperties is not a non-empty record stays unmarked, since merging into it could not be undone exactly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The file-entry marker lived in the entry's own additionalProperties, which unknown content shares with the wire's fields, so an entry whose wire metadata used the same key lost it after a save and restore. The receive side now marks the code_interpreter_tool_result itself, which the framework owns, as additionalProperties.unmodelledPayload when its payload is a variant this build does not model; every other unknown output is a file entry and is replayed verbatim inside the result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The typed outputs of a code-execution, bash or text-editor result keep the outcome but not everything the wire said, so a restored transcript rebuilt some blocks wrongly: a failed run went back with return_code 0, an encrypted result with empty stdout went back as a plain one, a bash error with an unknown code went back as a successful run, and a text-editor view reading like a create result, or a str_replace with only the replaced span, went back as another variant. The receive side now records the wire payload's type (payloadType) and a code-execution run's non-zero return code (returnCode) on the result content, which the framework owns. The rebuild reads the variant from that record, falling back to the typed outputs for a transcript stored before it existed; this subsumes the separate encrypted-stdout and unmodelled-payload markers. Unknown bash error codes now degrade to unavailable, as on the code-execution path. A code-execution call that no result answers is now omitted once its turn has closed, in memory and after a restore, since the API refuses such a request; it is kept on the open turn, where a pause_turn continuation or a pending local tool result still needs it. The rebuilt server_tool_use helper also takes the call content type it is only ever given, dropping its dead empty-block branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The receive side types a web search or web fetch call as an informational function_call and its result as a function_result, and the conversion sent them back as a local tool_use / tool_result pair for a tool the request does not define. They now go back as server_tool_use and web_search_tool_result / web_fetch_tool_result on the assistant turn: the raw blocks while held, and blocks rebuilt from the typed contents after a restore, with the result type taken from the name of the call it answers. A server call is recognized by its raw server_tool_use block or, after a restore, by the srvtoolu_ id prefix, so an informational call reported by another provider keeps the local mapping. An unanswered server call follows the same open-turn rule as a code-execution call: kept on the open turn for a pause_turn continuation or a pending local tool result, omitted once the turn has closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The open-turn rule took the last assistant-role message as the open turn, so a local tool result carried in an assistant-role message, which goes to the user side as a tool_result, was mistaken for a later assistant turn and dropped the pending server call. The open turn is now the last message carrying anything but local tool results; a result answering a provider-executed call still counts as assistant content. A str_replace span with no replacement lines to quote carries no snippet, so a replaced-only span was rebuilt as an inserted one. Each str_replace line-span citation now records which span it is under additionalProperties.lineSpan, and the rebuild uses that record. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Takes the open-turn rule's move to emitted roles: a function_result answering a server tool call is part of the assistant turn, so it is not counted as a local result when deciding which turn is still open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The open-turn rule read turns off framework messages, but a framework message can split into several request turns and one that sends nothing is not a turn at all. A usage-only message after a paused call closed the turn and dropped the pending call, and a message bundling a server call, a local call, its result and final text exempted the whole message, keeping a call whose request turn had closed. The rule now runs on the converted request messages: the open turn is the trailing run of assistant messages followed only by local tool_result messages, and a server_tool_use outside it that no result block answers is omitted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The updated restored-replay branch decides abandoned server calls from the request messages rather than framework messages; that pass already covers web search and web fetch server_tool_use blocks, so the message-level server-call handling here goes away. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The abandoned-call pass recognized a provider call only as a server_tool_use block, so a code-execution call the receive side read from a tool_use block, replayed raw with that spelling, stayed in a closed turn in memory while its restored form was dropped. The blocks emitted for provider-executed calls are now collected during conversion and the pass filters by that identity, leaving local tool_use blocks alone. The synthetic Continue turn checked only the last request message, so an open turn split across assistant messages, with its pending call in an earlier one, was closed on that call. A pending call anywhere on the open turn now keeps Continue out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The abandoned-call pass now filters by the blocks emitted for provider-executed calls rather than by block type, so the server_tool_use blocks emitted for web search and web fetch calls are registered as provider calls when they are converted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from
replay-unmodelled-code-execution-payloads
to
master
October 20, 2026 08:01
Master now carries the squash-merged restored-replay change this branch was stacked on, plus the OTLP/gRPC peer range entry. The restored-replay code here already matches master's, so the conflicts resolve to this branch's version with both changelog entries kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Split-message result routing needs correction, and the deliberate reference divergence requires maintainer validation.
1 open issue
What changed in this PR
Updates Anthropic transcript replay to preserve web search and web fetch server-tool blocks in memory and after restoration.
Changes:
- Recognizes server calls and rebuilds their matching results.
- Preserves unanswered calls on open turns.
- Adds replay regression tests and documents the behavior change.
| File | Description |
|---|---|
| packages/anthropic/src/to-anthropic.ts | Recognizes and reconstructs web server-tool exchanges. |
| packages/anthropic/src/server-tool-replay.test.ts | Tests replay, restoration, and unanswered-call handling. |
| CHANGELOG.md | Documents replay changes and deliberate reference divergence. |
Review effort: Balanced
| // A server tool's result, reached without its raw block — a restored transcript. The | ||
| // receive side kept the block's `content` as the result, as the wire spelled it, so the | ||
| // block goes back around it on the assistant turn that produced it. | ||
| blocks.push({ type: serverResultType, tool_use_id: content.callId, content: content.result }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What this changes
A turn that used the web search or web fetch server tool now replays as the blocks the API sent. The receive side types such a call as an informational
function_calland its result as afunction_result, and the conversion sent them back as a localtool_use/tool_resultpair for a tool the request does not define. This happened both in memory and after a restore.They now go back as
server_tool_useandweb_search_tool_result/web_fetch_tool_resulton the assistant turn:contentis the result as the wire spelled it.How unanswered calls and other providers are handled:
server_tool_useblock or, after a restore, if its id has thesrvtoolu_prefix the API gives every server call. An informational call reported by another provider keeps the local mapping.pause_turncontinuation or a server call waiting on a local tool result needs; previously such a call was dropped by the local unanswered-call filter. Once the turn has closed, they are omitted.Parity
agent_framework_anthropic/_chat_client.py,_prepare_message_for_anthropic. Python sends an informationalfunction_callastool_useand itsfunction_resultastool_result, which is what this package did. This deliberately diverges. The Messages API server-tools documentation describes server calls asserver_tool_useblocks paired with their own result blocks on the assistant turn, re-sent as-is for apause_turncontinuation. A server call whose turn closed without its result is refused with… tool use with id … was found without a corresponding …_tool_result block. The receive-side mapping is unchanged and still matches Python.Checklist
pnpm checkpasses (lint, typecheck, build, test)CHANGELOG.md🤖 Generated with Claude Code