fix(anthropic): guard leading-assistant transcripts after double compaction - #69340
Merged
Conversation
…sistant message Anthropic extracts the system prompt into a separate `system` field and requires messages[0] to be role="user"; a leading assistant turn is rejected with HTTP 400. After a second context compaction the only surviving leading anchor is the system prompt, so a summary/handoff message emitted as role="assistant" becomes the first messages entry once the system prompt is extracted. Anthropic reports this with a misleading error — `messages.N: tool_use ids were found without tool_result blocks immediately after: toolu_...` — even when every tool_use/tool_result pair is adjacent and matched; the real structural defect is the leading assistant role (#52160). This is engine-agnostic: it fires for any producer of a leading-assistant transcript (built-in compressor, the DAG/LCM context engine, session truncation), unlike the compressor-scoped fix in #52167. The native Bedrock Converse adapter already guards the same invariant (convert_messages_to_converse); this mirrors it for the native Anthropic path. Add _ensure_leading_user_turn() to the convert_messages_to_anthropic post-processing chain, scoped to the system-extracted case (the production trigger) so bare assistant-only unit fixtures are unaffected. Adds regression tests (leading-assistant, leading-assistant-with-adjacent-tool_use, and a no-op negative control).
…ent path The salvaged guard from #52276 only fired when a system prompt was present in messages[] (system is not None after extraction). The live repro of #52160 is the auto path: the system prompt is passed outside messages[], so after the second compaction messages[0] is the assistant-role summary with system=None — and the guard never ran. Make _ensure_leading_user_turn unconditional, exactly mirroring the Bedrock Converse adapter ('Converse requires the first message to be from the user' — convert_messages_to_converse). Add a regression test building the exact post-double-compaction shape (no system in messages, messages[0]=assistant summary) asserting the converted payload leads with a user turn, and update existing fixtures that started with a bare assistant message to locate roles instead of indexing result[0].
Contributor
૮ >ﻌ< ა ci reviewrunning on 5e67adb CI timingsCI timings · View jobWall time 8m26s vs 7m46s (+8.6%). 7 job(s) slower, 11 faster, 2 unchanged.
|
Collaborator
This was referenced Jul 22, 2026
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.
Summary
The native Anthropic adapter now guarantees the converted payload leads with a user turn, so sessions survive a second context compaction instead of dying with HTTP 400. Root cause: after double compaction on the auto path, protect_head decays to 0 and the compaction summary (role=assistant) becomes messages[0]; Anthropic requires messages[0] to be role=user and rejects the request — often masked as a misleading
tool_use ids were found without tool_result blockserror (#52160).Changes
agent/anthropic_adapter.py: add_ensure_leading_user_turn()to theconvert_messages_to_anthropicpost-processing chain — when the first converted message is not role=user, prepend a minimal user text turn, exactly mirroring the existing Bedrock Converse adapter guard ("Converse requires the first message to be from the user"). Follow-up commit makes the guard unconditional (the fix(anthropic): prepend user turn when compaction leaves a leading assistant message #52276 version only fired when a system prompt was present inside messages[]; the live bug(adapter): HTTP 400 after double context compression — first message is assistant, not user #52160 repro has the system prompt outside messages[], so system=None).tests/agent/test_anthropic_adapter.py: regression tests — leading-assistant-with-system, exact post-double-compaction shape (no system in messages, messages[0]=assistant summary → payload leads with user), leading-assistant with adjacent tool_use pair preserved, and a no-op negative control; existing fixtures that indexedresult[0]on bare assistant-first transcripts now locate roles explicitly.Validation
messages: first message must be user/ misleading tool_use error; session deadTargeted tests:
tests/agent/test_anthropic_adapter.py182 passed, 0 failed;tests/agent/ -k anthropic471 passed across 275 files, 0 failed.Credit
Salvaged from #52276 by @fesalfayed — commit cherry-picked with original authorship preserved. Complements the compressor-side fix #56197 (which only covers the system-in-messages gateway /compress path); this adapter-level guard is engine-agnostic and backstops any producer of a leading-assistant transcript.
Fixes #52160.
Infographic