Skip to content

feat(provider): versioned raw state and explicit policy for unreplayable thinking - #4668

Merged
aheritier merged 7 commits into
mainfrom
feat/claude5x-raw-state-contract
Oct 20, 2026
Merged

aheritier merged 7 commits into
mainfrom
feat/claude5x-raw-state-contract

Conversation

@aheritier

@aheritier aheritier commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #4653 (part b, its last remaining part). Part of #4583 and epic #4580. Part (a) merged in #4666; part (c), the error default, was dropped (see the decision in #4653).

What

  1. Versioned, attributed raw state. chat.ProviderState gains Version (ProviderStateVersion = 1) and Model (the model that produced the response). A state from a newer build is never replayed; states written before versioning (version 0) keep replaying.
  2. Seal after generated media. The content hash covers media parts, and the runtime sealed the state before attaching them, so every turn with generated media was unreplayable.
  3. An explicit policy when a turn's thinking can't be replayed. Today an Anthropic turn whose raw state no longer matches silently loses its thinking, and Bedrock replays a merged signature that can't verify. Now:
    • The thinking is intact but the rest of the turn changed (a sanitized tool name, or an argument rewritten by redact_secrets): the recorded blocks are replayed in their original order, with the current text, tool names and arguments substituted. Whether a turn holds thinking is decided from its recorded blocks (signed or redacted), not from editable fields.
    • The thinking itself changed: it is omitted, with a warning in the logs.
    • Inside the tool loop the next request continues, with thinking on: the API requires that thinking, so the request is refused before it is sent, with an error that says to start a new user turn.
    • No own content (states holding only request context, or another provider's state): the flattened replay is kept, as today.

Applies to Anthropic (standard and Beta Messages API) and Bedrock Converse.

Not in this PR

References

Vendor documentation behind each change (all links checked on 2026-10-20):

Change Documentation
Thinking in the current tool loop must be passed back unchanged; earlier turns must stay append-only Preserved thinking
Signed and redacted thinking blocks, and when thinking must be included with tool results Extended thinking
Thinking blocks from earlier turns stay in context; thinking is bound to the producing account (the API drops it from another account, so no 400); no requirement to omit thinking when it is off Haiku 5.5 migration guide

Tests

  • Version gating, the content-ownership and tool-loop helpers, and seal-after-media, each with a test that failed first.
  • Per transport (Anthropic standard, Anthropic Beta, Bedrock):
    • a turn whose thinking changed is omitted, and refused inside the loop;
    • parallel tool calls in the loop;
    • a content-less state keeps its flattened thinking;
    • thinking off is never refused;
    • a sanitized tool name and redacted arguments keep their thinking.

Validation

  • task build passes.
  • task test passes (exit 0).
  • golangci-lint v2.14.0 reports 0 issues, and go run ./lint . reports no offenses.
  • go test -race -shuffle=on ./pkg/chat/ ./pkg/runtime/ ./pkg/model/provider/... passes.
  • go test ./e2e -run '^TestWasmProviderDependencies$|^TestDependencies$' passes.
  • An independent review found 0 critical and 2 important issues: refusing normal sessions after tool-name sanitization or redact_secrets, and Bedrock dropping single-block thinking. Both are fixed in this PR. Five Copilot comments (split signatures, interleaved thinking, redacted-only and cleared-signature turns, newer-format states) are fixed in 4994e37.

Deferred minor findings:

  • Anthropic counts an invalid token budget (below 1024, or at least max_tokens) as "thinking on".
  • Bedrock only knows the nine table models when deciding whether thinking is on.
  • The error's advice ("start a new user turn") doesn't help in --exec, sub-agent or A2A runs.
  • Bedrock runs the replay decision twice for turns in the active loop.

@aheritier
aheritier requested a review from a team as a code owner October 20, 2026 09:42
Copilot AI balanced review requested due to automatic review settings October 20, 2026 09:42

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.

🟡 Changes recommended

Replay safeguards can permit invalid thinking history or reject intact thinking during tool continuations.

5 open issues
What changed in this PR

This advances the signed-thinking work from #4653 by versioning provider state and defining replay behavior for Anthropic and Bedrock.

Changes:

  • Record state versions and producing models.
  • Seal runtime state after generated media is attached.
  • Recover intact thinking or omit/refuse unreplayable thinking, with tests and documentation.
File Description
pkg/​runtime/​provider_state_test.go Tests sealing after media attachment.
pkg/​runtime/​loop.go Moves sealing after media attachment.
pkg/​model/​provider/​bedrock/​native_blocks.go Adds state metadata and reasoning recovery guards.
pkg/​model/​provider/​bedrock/​native_blocks_test.go Tests edited reasoning and runtime transforms.
pkg/​model/​provider/​bedrock/​convert.go Applies reasoning recovery and omission.
pkg/​model/​provider/​bedrock/​client.go Checks active-loop replay before requests.
pkg/​model/​provider/​bedrock/​adapter.go Records the producing model.
pkg/​model/​provider/​bedrock/​adapter_test.go Checks state metadata.
pkg/​model/​provider/​anthropic/​thinking.go Determines whether requests enable thinking.
pkg/​model/​provider/​anthropic/​response_content.go Adds metadata, recovery, and refusal logic.
pkg/​model/​provider/​anthropic/​response_content_test.go Checks state metadata.
pkg/​model/​provider/​anthropic/​features_integration_test.go Tests recovery and refusal behavior.
pkg/​model/​provider/​anthropic/​client.go Applies standard Messages replay policy.
pkg/​model/​provider/​anthropic/​beta_converter.go Applies Beta Messages replay policy.
pkg/​model/​provider/​anthropic/​beta_adapter.go Records the producing model.
pkg/​model/​provider/​anthropic/​adapter.go Records the producing model.
pkg/​chat/​provider_message.go Adds version, ownership, and tool-loop helpers.
pkg/​chat/​provider_message_test.go Tests shared state helpers.
docs/​providers/​bedrock/​index.md Documents reasoning replay policy.
docs/​providers/​anthropic/​index.md Documents thinking recovery and refusal.

Review effort: Balanced

Comment thread pkg/chat/provider_message.go Outdated
Comment on lines +105 to +106
if s == nil || s.Provider != provider || s.Version > ProviderStateVersion {
return false

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 4994e37. HasOwnRawContent no longer depends on the version, so a state from a newer format stays "own": it is never decoded, recordedThinking/recordedReasoning assume it holds thinking, and its thinking is omitted, or the request is refused inside the active tool loop. Covered by the "newer state version is refused in the loop" cases on Anthropic standard, Beta and Bedrock.

Comment on lines +253 to +254
if n == 0 || slices.ContainsFunc(views[n:], isThinking) {
return 0, false

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 4994e37. The prefix-only recovery is replaced by recoveredContent (Anthropic) and recoveredNativeBlocks (Bedrock): when the thinking text is unchanged, the recorded blocks are rebuilt in their original order with the current tool names, arguments and text substituted (tool calls matched by ID). Interleaved thinking between tool calls is covered on all three transports.

}
}
}
return n, thinking.String() == msg.ReasoningContent && signature == msg.ThinkingSignature

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 4994e37. Recovery no longer compares the editable flattened signature: it checks the thinking text and replays the recorded blocks, which carry the complete signature. A split signature (recorded s1s2, flattened s2) is recovered on all three transports.

Comment on lines +278 to +281
if msg.ThinkingSignature == "" && msg.ReasoningContent == "" {
return false, nil
}
if i >= loopStart && msg.ThinkingSignature != "" && c.requestThinks() {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 4994e37. Whether a turn holds thinking is now decided from the recorded blocks (signed thinking or redacted_thinking), not from the flattened fields. A cleared signature with unchanged thinking text is recovered; redacted-only thinking that can't be recovered is refused inside the active tool loop. Both cases are tested on standard and Beta.

Comment on lines +264 to +269
if msg.Role != chat.MessageRoleAssistant || msg.ThinkingSignature == "" || !msg.HasOwnRawContent(providerName) {
continue
}
if _, ok := replayNativeBlocks(msg); ok {
continue
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 4994e37. checkReplayableThinking now checks every assistant turn in the active loop whose recorded blocks hold signed or redacted reasoning (recordedReasoning), instead of relying on ThinkingSignature. Redacted-only reasoning that can't be recovered is refused, and a cleared signature with intact reasoning is recovered; both are tested.

@aheritier aheritier added area/docs Documentation changes area/providers/anthropic For features/issues/fixes related to the usage of Anthropic models area/providers/bedrock AWS Bedrock provider support area/runtime Runtime engine, agent loop execution, tool dispatch, loop detection kind/feat PR adds a new feature (maps to feat:). Use on PRs only. status/needs-rebase PR has merge conflicts or is out of date with main labels Oct 20, 2026
@aheritier

Copy link
Copy Markdown
Collaborator Author

👋 This PR has merge conflicts with the base branch. Please rebase or merge the latest base branch and resolve them. I've moved it to draft and added ; it'll be picked back up automatically once the conflicts are cleared.

@aheritier

Copy link
Copy Markdown
Collaborator Author

👋 This PR has merge conflicts with the base branch. Please rebase or merge the latest base branch and resolve them. I\x27ve moved it to draft and added status/needs-rebase; it\x27ll be picked back up automatically once the conflicts are cleared.

Raw state now records a format version and the model that produced it.
A state from a newer build is never replayed; states written before
versioning (version 0) keep replaying.

Signed-off-by: Arnaud Héritier <aheritier@apache.org>
The content hash covers media parts, so sealing before they were attached
made every turn with generated media unreplayable.

Signed-off-by: Arnaud Héritier <aheritier@apache.org>
Signed-off-by: Arnaud Héritier <aheritier@apache.org>
An assistant turn edited since its raw state was recorded used to lose
its thinking silently on Anthropic, or replay a merged signature on
Bedrock. Its thinking is now omitted with a warning, and a thinking
request whose active tool loop needs it is refused with an actionable
error. States without content and other providers' states keep the
flattened replay.

Signed-off-by: Arnaud Héritier <aheritier@apache.org>
Tool-name sanitization and redact_secrets change a turn's tool calls
before or after sealing without touching its signed thinking. Such turns
were treated as edited: their thinking was dropped, and inside the active
tool loop the request was refused, breaking normal sessions. Replay the
recorded leading thinking when it still matches the flattened fields;
omit or refuse only when the thinking itself changed.

Signed-off-by: Arnaud Héritier <aheritier@apache.org>
Signed-off-by: Arnaud Héritier <aheritier@apache.org>
Recovery of a transformed turn only handled leading thinking, compared
the editable flattened signature (split signature deltas and cleared
signatures failed), and decided whether a turn had thinking from those
editable fields (redacted-only thinking bypassed the refusal). Rebuild
the recorded blocks in their original order with the current text, tool
names and arguments substituted when the thinking text is unchanged, and
decide from the recorded blocks whether a turn holds signed or redacted
thinking. A state from a newer format is never decoded: its thinking is
omitted, or refused inside the active tool loop.

Signed-off-by: Arnaud Héritier <aheritier@apache.org>
@aheritier
aheritier force-pushed the feat/claude5x-raw-state-contract branch from 4037299 to 4994e37 Compare October 20, 2026 14:08
@aheritier
aheritier added this pull request to the merge queue Oct 20, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 20, 2026
@aheritier aheritier removed the status/needs-rebase PR has merge conflicts or is out of date with main label Oct 20, 2026
@aheritier
aheritier added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit 144787f Oct 20, 2026
22 checks passed
@aheritier
aheritier deleted the feat/claude5x-raw-state-contract branch October 20, 2026 17:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation changes area/providers/anthropic For features/issues/fixes related to the usage of Anthropic models area/providers/bedrock AWS Bedrock provider support area/runtime Runtime engine, agent loop execution, tool dispatch, loop detection kind/feat PR adds a new feature (maps to feat:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(provider): exact signed-thinking capture, persistence and replay (incl. Bedrock)

3 participants