Skip to content

fix(anthropic): guard leading-assistant transcripts after double compaction - #69340

Merged
teknium1 merged 2 commits into
mainfrom
salvage/52276-leading-user-guard
Jul 22, 2026
Merged

fix(anthropic): guard leading-assistant transcripts after double compaction#69340
teknium1 merged 2 commits into
mainfrom
salvage/52276-leading-user-guard

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

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 blocks error (#52160).

Changes

  • agent/anthropic_adapter.py: add _ensure_leading_user_turn() to the convert_messages_to_anthropic post-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 indexed result[0] on bare assistant-first transcripts now locate roles explicitly.

Validation

Before After
2nd auto-compaction on Anthropic path HTTP 400 messages: first message must be user / misleading tool_use error; session dead Request accepted; conversation continues
Converted payload, messages[0]=assistant summary leads with assistant leads with synthesized user turn (mirrors Bedrock)

Targeted tests: tests/agent/test_anthropic_adapter.py 182 passed, 0 failed; tests/agent/ -k anthropic 471 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

leading-user-guard

fesalfayed and others added 2 commits July 22, 2026 05:36
…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].
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

running on 5e67adb

CI timings

CI timings · View job

Wall time 8m26s vs 7m46s (+8.6%). 7 job(s) slower, 11 faster, 2 unchanged.

  • Python tests / Run tests slice 2/8: -12.0s
  • Build&Test Docker image / build (arm64, ubuntu-24.04-arm, linux/arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope...: +12.0s
  • Python tests / Run tests slice 3/8: +11.0s
  • OSV scan / Scan lockfiles / osv-scan: -10.0s
  • Python tests / Run tests slice 4/8: +7.0s

@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/anthropic Anthropic native Messages API area/compression Context compression and continuation sessions P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 22, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to #52276 and #52160: this patch removes the older guard's dependency on an extracted system prompt, covering the no-system double-compaction transcript as well.

@teknium1
teknium1 merged commit b6a2b70 into main Jul 22, 2026
42 checks passed
@teknium1
teknium1 deleted the salvage/52276-leading-user-guard branch July 22, 2026 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/compression Context compression and continuation sessions comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/anthropic Anthropic native Messages API sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(adapter): HTTP 400 after double context compression — first message is assistant, not user

3 participants