Rebuild provider-executed Anthropic blocks for a restored session - #162
Conversation
Content describing a call the provider ran itself - code execution, bash, the text editor - was replayed only from rawRepresentation, which serialization deliberately strips. A transcript restored from persistence therefore dropped those blocks, and the text editor's function_result went out as a tool_result answering a call the request no longer contained, which the Messages API rejects. The conversion now rebuilds the blocks from the typed contents when the raw block is gone. The server_tool_use family is inferred from the kind of content answering the call, since the typed call folds all three code-execution tools into one kind; the result payloads follow the request schemas, with the bash files gathered from the hosted_file siblings the receive side places before the shell result. Error outputs parsed from code-execution and text-editor failures now also carry the wire's code under ErrorContent.errorCode, without which a restored error turn cannot name the code the schema requires. For every fixture whose typed form captures what the wire said, the rebuilt request is asserted identical to the in-memory replay; a transcript still holding rawRepresentation replays the exact bytes, and rawRepresentation is still never serialized. Closes #126 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes Anthropic “provider-executed” (code execution / bash / text editor) turn replay after restoring a persisted session by rebuilding the missing Messages API blocks when rawRepresentation has been stripped during serialization.
Changes:
- Reconstruct
server_tool_use+ provider tool-result blocks from typedContentwhenrawRepresentationis absent (restored sessions). - Preserve error codes through persistence by additionally storing Anthropic tool error codes in
ErrorContent.errorCode. - Add replay/round-trip tests to assert restored replay matches in-memory replay for provider-executed turns, and document the fix in the changelog.
File summaries
| File | Description |
|---|---|
| packages/anthropic/src/to-anthropic.ts | Adds restored-session reconstruction logic for provider-executed tool call/result blocks. |
| packages/anthropic/src/from-anthropic.ts | Captures tool error codes into ErrorContent.errorCode to enable faithful rebuild after persistence. |
| packages/anthropic/src/code-execution.test.ts | Updates expectations for new errorCode field on error outputs. |
| packages/anthropic/src/code-execution-replay.test.ts | New fixtures asserting restored replay is byte-identical to in-memory replay for provider-executed turns. |
| CHANGELOG.md | Documents restored-session replay behavior fix for the Anthropic package. |
Review details
Suppressed comments (2)
packages/anthropic/src/to-anthropic.ts:407
- These restored-session cases should not emit blocks with missing/empty ids (
callIdis optional on hosted tool contents). As written,callId ?? ''can produceid: ''/tool_use_id: '', which is not pairable and can cause Messages API rejection.
case 'code_interpreter_tool_call': {
// Reached only without a raw block — a restored transcript — since the raw path above
// replays the exact bytes otherwise. The same holds for the result cases below.
blocks.push(rebuiltServerToolUse(content, ctx.families.get(content.callId ?? '') ?? 'code_execution'));
return;
packages/anthropic/src/to-anthropic.ts:421
- For bash replays, malformed file entries are preserved by the receive side as
unknowncontent items (unknownTypebash_code_execution_output) placed immediately before theshell_tool_result. The rebuild currently only gathershosted_filesiblings, so restored replays drop those entries from the rebuilt payload and may incorrectly emit them as standalone unknown blocks.
case 'shell_tool_result': {
// The files a bash run produced sit as `hosted_file` siblings *before* the shell result —
// that is where the receive side put them — so the rebuilt payload gathers the contiguous
// run immediately preceding this content.
const files: Content[] = [];
- Files reviewed: 5/5 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Review caught three fidelity gaps in the restored-session rebuild. A
string server_tool_use input - which the receive side records as
itself - was collapsed to {}; the rebuild now parses the recorded text
when it can and sends it back as the string it was otherwise, which is
type-valid because the request schema types input as unknown. A
malformed file entry the receive side preserves as unknown content was
dropped from the rebuilt result payloads; it now goes back in place for
both the code and bash lists. And the bash siblings walk now carries
those preserved entries too, while the unknown case skips a file entry
that folds into the shell result that follows it - previously it leaked
as a standalone top-level block on both the raw and the rebuilt path.
Each case is pinned by a new round-trip equivalence fixture.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The restored-session reconstruction path can mis-rebuild code_execution_tool_result_error into a success-shaped code_execution_result when ErrorContent.errorCode is present but not in the known enum, which changes the payload variant/semantics instead of degrading to a valid error payload.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
A stored errorCode marks the error-payload variant unambiguously, but the rebuild only produced an error when the code was one the schema enum knows - an unrecognized or future code fell through to a success-shaped code_execution_result with return_code 0, inverting what the turn reported. It now degrades to the unavailable code the same way the text-editor path does, while a legacy prose-only lone error still reads back as the stderr-only success it was. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
codeExecutionFamilies currently correlates missing callId values via '', which can mis-infer tool families for keyless hosted-tool contents and produce mismatched rebuilt server_tool_use.name in degenerate transcripts.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟢 Approval recommended
The rebuild behavior is well-scoped, preserves raw replay when present, and is backed by comprehensive round-trip equivalence tests covering key provider-executed variants and edge cases.
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Lite
An empty callId is not an identity two contents can share - the rule the unanswered-call filter already applies - but the family inference keyed missing ids as '', letting a keyless shell result flip a different keyless call's rebuilt server_tool_use name. Families are now tracked only for non-empty ids, and a keyless call keeps the code_execution default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The reconstruction behavior is well-scoped, preserves exact raw replay when available, and is backed by byte-equivalence replay tests covering the restored-session path.
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Lite
What this changes
A session restored from persistence can replay a turn that used the code-execution beta: when
rawRepresentationis gone — serialization deliberately strips it — the Anthropic conversion rebuildsserver_tool_useand the code-execution result blocks from the typed contents, so calls stay paired with their results instead of dropping out and drawing a 400. Closes #126.Parity
@anthropic-ai/sdkrequest param schemas (v0.122:BetaServerToolUseBlockParam,BetaCodeExecutionToolResultBlockParam,BetaBashCodeExecution…,BetaTextEditorCodeExecution…), and the receive-side mapping being inverted is this package's own (from-anthropic.ts), which itself matches Python's_parse_tool_result_from_mcp-era handling. Python does not rebuild these blocks — its transcripts hit the same loss — so the reconstruction rules are this repository's own, pinned by round-trip equivalence: for every fixture (code success and error, bash success and timeout, text-editor view/str_replace/create/error), receive → serialize → restore → send is asserted byte-identical to the in-memory raw replay.The
server_tool_usefamily is inferred from the kind of content answering the call, since the typedcode_interpreter_tool_callfolds all three code-execution tools into one kind; bash files are gathered from thehosted_filesiblings the receive side places before the shell result;return_codeandfile_typeare not in the typed model and default to0/'text'. Error outputs parsed from code-execution and text-editor failures now also carry the wire's code underErrorContent.errorCode(additive; both layers proven individually necessary by negative control). A transcript still holdingrawRepresentationreplays the exact bytes — a raw block with an unmodelled extra field is pinned to survive — andrawRepresentationis still never serialized. Afunction_resultanswering a local call keeps the localtool_resultmapping after a restore. Restoring web_search/web_fetch turns stays out of scope, per the issue.Checklist
pnpm checkpasses (lint, typecheck, build, test)CHANGELOG.md🤖 Generated with Claude Code