Skip to content

fix(anthropic): synthesized leading user turn must be non-whitespace - #70081

Open
psolakovballisticcell wants to merge 1 commit into
NousResearch:mainfrom
psolakovballisticcell:fix/anthropic-leading-user-nonwhitespace
Open

fix(anthropic): synthesized leading user turn must be non-whitespace#70081
psolakovballisticcell wants to merge 1 commit into
NousResearch:mainfrom
psolakovballisticcell:fix/anthropic-leading-user-nonwhitespace

Conversation

@psolakovballisticcell

Copy link
Copy Markdown

Problem

Resuming a session whose active window opens on a non-user role (e.g. a resumed or double-compacted session whose first live message is an assistant tool-call turn) fails on the very first request with:

HTTP 400: messages: text content blocks must contain non-whitespace text
Provider: anthropic  Model: claude-opus-4-8

Non-retryable — the whole request is rejected before any tokens are generated, so the session becomes unresumable.

Root cause

_ensure_leading_user_turn() (in agent/anthropic_adapter.py) backstops Anthropic's requirement that messages[0].role == "user" by prepending a filler user turn. That filler used a single space as its text:

result.insert(0, {"role": "user", "content": [{"type": "text", "text": " "}]})

A single space is whitespace-only, which the Messages API rejects with the 400 above. The bug is latent: it only fires when the converted message list actually opens on a non-user role, which happens on resumed/compacted sessions whose active head is an assistant turn.

Two existing tests (test_leading_assistant_after_compaction_gets_user_turn_prepended, test_double_compaction_no_system_in_messages_leads_with_user) asserted the " " value, but only checked output shape — never that Anthropic would accept it. That's why it shipped.

Fix

Use the "(empty)" sentinel already used by every other empty-content path in this same adapter (lines ~2061 / 2136 / 2325 / 2368) and by the Bedrock adapter's _EMPTY_TEXT_PLACEHOLDER = "(empty)". The synthesized leading user turn now always carries printable text.

Tests

  • Updated the two existing assertions from " " to "(empty)" and added a .strip() != "" invariant so a future whitespace regression fails loudly.
  • Added test_synthesized_leading_user_turn_is_non_whitespace reproducing the real-world shape (assistant tool-call turn as messages[0]), asserting the property that actually matters to the API: non-whitespace filler text.

Verification

Reproduced against a real affected session by rebuilding its active window and running it through convert_messages_to_anthropic(): before the fix, one {"type":"text","text":" "} block was emitted as messages[0]; after, zero empty/whitespace text blocks across the full converted payload. The three relevant tests pass against the patched adapter.

_ensure_leading_user_turn() prepends a filler user message when the
converted message list opens on a non-user role (e.g. a resumed or
double-compacted session whose active window starts on an assistant
tool-call turn). The filler used a single space (" ") as its text.

Anthropic's Messages API rejects any text content block whose text is
empty or whitespace-only:

  HTTP 400: messages: text content blocks must contain non-whitespace text

so every such resume 400s before a single token is generated. Two
existing tests asserted the " " value but only checked output *shape*,
never that the API would accept it — which is why this shipped.

Fix: use the "(empty)" sentinel already used by every other
empty-content path in this adapter (lines ~2061/2136/2325/2368) and by
the Bedrock adapter's _EMPTY_TEXT_PLACEHOLDER. Update the two existing
tests to assert non-whitespace, and add a regression test for the
assistant-tool-call-leading shape that triggered it in the wild.
@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 P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 23, 2026

@teknium1 teknium1 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.

Thanks for the focused fix. Current main still inserts the whitespace-only synthesized user block in agent/anthropic_adapter.py:2613, and the conversion path invokes that helper at agent/anthropic_adapter.py:2674, so the reported defect remains present.

Suggested changes

  • Prefer the existing _EMPTY_TEXT_PLACEHOLDER constant from agent/anthropic_adapter.py:1931 instead of repeating the "(empty)" literal. The proposed .strip() != "" assertions are the useful behavioral contract; current main still freezes the invalid literal at tests/agent/test_anthropic_adapter.py:883.

The patch is narrow, preserves role ordering, and introduces no tool, configuration, or prompt-cache surface. This is an automated hermes-sweeper review.

@@ -2427,7 +2427,15 @@ def _ensure_leading_user_turn(result: List[Dict[str, Any]]) -> None:
(convert_messages_to_converse).

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.

Prefer _EMPTY_TEXT_PLACEHOLDER here instead of duplicating its current "(empty)" value. The module defines the shared sentinel for this provider's non-whitespace fallback paths, so using it prevents this synthesized-turn path from drifting.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 30, 2026
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

Four open PRs address Issue #70909's Anthropic request-assembly failure. #70081, #70159, and #71935 fix only the whitespace-only synthesized leading-user block; #70910 includes that source fix plus a final guard for blank text blocks elsewhere in the assembled system prompt and messages.

Related pull requests

Duplicates

#70081, #70159, and #71935 duplicate the synthesized leading-user fix contained in #70910. #70081 and #71935 are source-level equivalents using the canonical behavior, while #70159 differs mainly by introducing "." instead of _EMPTY_TEXT_PLACEHOLDER; #70910 additionally covers the broader request-assembly paths.

Suggested consolidation

Keep open with a salvage path: #70910, preserving its canonical synthesis-site fix, final assembled-request text-block guard, and focused regressions while reconciling tests with current main. Close #70081, #70159, and #71935 as duplicates of #70910; this recommendation explicitly departs from the COMMENTED keep_open reviews on #70081 and #70159 and the MAINTAINER-BOT keep_open verdict on #71935 because their diffs contain only the narrower synthesis-site change already included in #70910. Do not merge any PR from this consolidation.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    I70909(["issue #70909 (open)"])
    subgraph Dup70081 ["PRs duplicating each other"]
        P70081["PR #70081 (open)"]
        P70159["PR #70159 (open)"]
        P71935["PR #71935 (open)"]
    end
    P70081 -->|fixes| I70909
    class I70909 open
    class P70081 open
    class P70159 open
    class P71935 open
    class P70081 target
    click I70909 "https://github.com/NousResearch/hermes-agent/issues/70909"
    click P70081 "https://github.com/NousResearch/hermes-agent/pull/70081"
    click P70159 "https://github.com/NousResearch/hermes-agent/pull/70159"
    click P71935 "https://github.com/NousResearch/hermes-agent/pull/71935"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 4 pull requests and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 21 kB of PR diffs, 13 kB of issue/PR text, 4 kB of discussion (8 comments), 8 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform 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.

4 participants