Repository navigation
feat(provider): versioned raw state and explicit policy for unreplayable thinking - #4668
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Replay safeguards can permit invalid thinking history or reject intact thinking during tool continuations.
5 open issues
Unsupported provider state is incorrectly rebuilt as valid thinking · New Interleaved thinking recovery fails after non-thinking transformations · New Split signature deltas cause false thinking replay rejection · New Cleared signatures bypass required thinking validation · New Edited signatures skip replay validation for signed or redacted thinking · New
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
| if s == nil || s.Provider != provider || s.Version > ProviderStateVersion { | ||
| return false |
There was a problem hiding this comment.
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.
| if n == 0 || slices.ContainsFunc(views[n:], isThinking) { | ||
| return 0, false |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
| if msg.ThinkingSignature == "" && msg.ReasoningContent == "" { | ||
| return false, nil | ||
| } | ||
| if i >= loopStart && msg.ThinkingSignature != "" && c.requestThinks() { |
There was a problem hiding this comment.
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.
| if msg.Role != chat.MessageRoleAssistant || msg.ThinkingSignature == "" || !msg.HasOwnRawContent(providerName) { | ||
| continue | ||
| } | ||
| if _, ok := replayNativeBlocks(msg); ok { | ||
| continue | ||
| } |
There was a problem hiding this comment.
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.
|
👋 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. |
|
👋 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 |
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>
4037299 to
4994e37
Compare

Closes #4653 (part b, its last remaining part). Part of #4583 and epic #4580. Part (a) merged in #4666; part (c), the
errordefault, was dropped (see the decision in #4653).What
chat.ProviderStategainsVersion(ProviderStateVersion = 1) andModel(the model that produced the response). A state from a newer build is never replayed; states written before versioning (version 0) keep replaying.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.Applies to Anthropic (standard and Beta Messages API) and Bedrock Converse.
Not in this PR
errordefault (part c) was dropped. Automatic compaction and agent handoffs change earlier history mid-session, and undererrorevery later request would be rejected.drop_blockstays the default, and the docs now explain why (4037299).References
Vendor documentation behind each change (all links checked on 2026-10-20):
Tests
Validation
task buildpasses.task testpasses (exit 0).golangci-lintv2.14.0 reports 0 issues, andgo 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.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 in4994e37.Deferred minor findings:
max_tokens) as "thinking on".--exec, sub-agent or A2A runs.