Skip to content

fix(messageutil): preserve zero-valued fields in tool result ToParam (#317, #322)#345

Open
Zeffut wants to merge 1 commit into
anthropics:mainfrom
Zeffut:fix/toparam-preserves-zero-values
Open

fix(messageutil): preserve zero-valued fields in tool result ToParam (#317, #322)#345
Zeffut wants to merge 1 commit into
anthropics:mainfrom
Zeffut:fix/toparam-preserves-zero-values

Conversation

@Zeffut
Copy link
Copy Markdown

@Zeffut Zeffut commented May 23, 2026

Summary

The ToParam() methods on CodeExecutionToolResultBlock, BashCodeExecutionToolResultBlock, ToolSearchToolResultBlock and their Beta counterparts re-marshal server response blocks through struct fields tagged json:"...,omitzero". Zero-valued but required fields (return_code: 0, empty stdout/stderr, empty error_code) are silently dropped, and the next API call returns 400 on the now-malformed content block.

Fixes #317 and #322.

Root cause

Two stacked bugs in the manual messageutil.go / betamessageutil.go files (not Stainless-generated):

  1. omitzero strips required zero values. *ResultBlockParam fields are tagged omitzero, so when ToParam() round-trips a server response into params, return_code: 0, stdout: "", stderr: "", and error_code: "" disappear. The API rejects the resulting block on the next turn.
  2. param.Override was given a Go string instead of json.RawMessage. In the existing TextEditorCodeExecutionToolResultBlock.ToParam (and its beta mirror), param.Override[T](r.Content.RawJSON()) was passing a string, which param.MarshalUnion -> shimjson.Marshal then encodes as a JSON string literal (double-encoded). The documented usage (packages/param/param.go) is json.RawMessage(...).

Fix

Use raw-JSON passthrough on the union content directly:

return ContentBlockParamOfXxxToolResult(
    param.Override[XxxToolResultBlockParamContentUnion](json.RawMessage(r.Content.RawJSON())),
    r.ToolUseID,
)

This forwards the exact server-validated payload unchanged, preserving zero-valued required fields and the variant discriminator without the SDK having to introspect each variant.

Affected functions:

  • messageutil.go: BashCodeExecutionToolResultBlock.ToParam, CodeExecutionToolResultBlock.ToParam, ToolSearchToolResultBlock.ToParam, and TextEditorCodeExecutionToolResultBlock.ToParam (existing call corrected).
  • betamessageutil.go: BetaBashCodeExecutionToolResultBlock.ToParam, BetaCodeExecutionToolResultBlock.ToParam, BetaToolSearchToolResultBlock.ToParam, BetaTextEditorCodeExecutionToolResultBlock.ToParam (existing call corrected).

Tests

Adds two regression sub-tests in messageutil_test.go (TestContentBlockUnionToParam):

Test plan

  • go vet ./... clean
  • go build ./... clean
  • go test -run TestContentBlockUnionToParam ./... passes (all sub-tests)
  • Maintainers: confirm Prism-backed integration tests (unrelated, pre-existing failures locally due to no mock server)

Notes

Closes #317.
Closes #322.

…nthropics#317, anthropics#322)

The ToParam() methods on CodeExecutionToolResultBlock,
BashCodeExecutionToolResultBlock, ToolSearchToolResultBlock and their
beta counterparts re-marshaled response blocks via struct fields tagged
`omitzero`. Zero values from the server (return_code:0, stdout:"",
error_code:"") were stripped and the API returned 400 on the next turn.

Switch to param.Override[ContentUnion](json.RawMessage(r.Content.RawJSON()))
so the exact server-validated payload is forwarded unchanged. This also
fixes a silent bug where param.Override was receiving a Go string instead
of json.RawMessage — leading to double-encoded JSON.

Adds 2 regression tests covering anthropics#317 and anthropics#322. go vet, go build, and
the new tests pass cleanly.

Closes anthropics#317. Closes anthropics#322.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Zeffut Zeffut requested a review from a team as a code owner May 23, 2026 00:09
Copilot AI review requested due to automatic review settings May 23, 2026 00:09
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR addresses regressions where tool result content fields were being dropped during marshaling due to omitzero, causing follow-up API requests to be rejected.

Changes:

  • Switch several ToParam() implementations to raw-JSON passthrough via param.Override(...json.RawMessage(...)) to preserve zero/empty-but-required fields.
  • Apply the same raw-JSON preservation behavior to both stable and beta message utilities.
  • Add regression tests ensuring required fields like return_code, stdout/stderr, and error_code remain present after round-tripping.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
messageutil_test.go Adds regression tests validating that required zero/empty fields are preserved in marshaled tool result content.
messageutil.go Replaces typed union construction with raw JSON passthrough in multiple ToParam() methods to avoid omitzero dropping fields.
betamessageutil.go Mirrors the stable fix in beta utilities by using raw JSON passthrough for the same tool result blocks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread messageutil_test.go
Comment on lines +51 to +55
for _, key := range []string{`"return_code"`, `"stdout"`, `"stderr"`} {
if !strings.Contains(got, key) {
t.Errorf("expected %s to be present in marshalled content (got %s)", key, got)
}
}
Comment thread messageutil_test.go
Comment on lines +70 to +72
if !strings.Contains(string(marshalled), `"error_code"`) {
t.Errorf("expected error_code to be present in marshalled content (got %s)", string(marshalled))
}
Comment thread messageutil.go
Comment on lines +319 to +322
// Use raw JSON passthrough to preserve fields that would otherwise be
// dropped by `omitzero` (e.g. zero ReturnCode, empty Stderr/Stdout, or
// empty ErrorCode), which the API requires on the next turn. See #322.
p.Content = param.Override[BashCodeExecutionToolResultBlockParamContentUnion](json.RawMessage(r.Content.RawJSON()))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants