Skip to content

[Bug]: ag_ui_langgraph drops Anthropic extended-thinking signature on Bedrock Converse streams #2174

Description

@nielsdebruin

Pre-flight Checklist

  • I have searched existing issues and this hasn't been reported yet.
  • I am using the latest version AG-UI.

Describe the Bug

ag_ui_langgraph drops Anthropic extended-thinking signature on Bedrock Converse streams

Summary

When ChatBedrockConverse streams Anthropic extended-thinking output, the reasoning block's cryptographic signature is silently dropped and the reasoning block is closed one chunk early. Downstream, no REASONING_ENCRYPTED_VALUE event is ever emitted, so the assistant message cannot be replayed to Bedrock on the next Converse turn.

Affected: ag_ui_langgraph/utils.py::resolve_reasoning_content and ag_ui_langgraph/agent.py::LangGraphAgent.handle_reasoning_event.

Bedrock Converse stream shape

Unlike Anthropic's native API, Bedrock splits a single thinking block across chunks and delivers the signature on its own chunk with no text field, preceded by an empty-text flush:

[{'type': 'reasoning_content', 'reasoning_content': {'text': 'The'},              'index': 0}]
[{'type': 'reasoning_content', 'reasoning_content': {'text': ' user wants ...'},  'index': 0}]
[{'type': 'reasoning_content', 'reasoning_content': {'text': ''},                 'index': 0}]   # empty-text flush
[{'type': 'reasoning_content', 'reasoning_content': {'signature': 'EpcCC...=='},  'index': 0}]   # signature-only
[]                                                                                                # reasoning block closed
[{'type': 'text', 'text': 'Streaming LLM responses is', 'index': 1}]
...

Core issues

  1. resolve_reasoning_content gates on truthy text — the signature-only chunk fails if rc.get("text"): and resolves to None:

    if block_type == "reasoning_content" and isinstance(block.get("reasoning_content"), dict):
        rc = block["reasoning_content"]
        if rc.get("text"):                    # signature-only chunk falls through
            result = LangGraphReasoning(text=rc["text"], ...)
            if rc.get("signature"):
                result["signature"] = rc["signature"]
            return result
  2. Empty-text chunk triggers premature close-out — the resolver also returns None for {"text": ""}. In agent.py, reasoning_data is None with an active reasoning_process fires REASONING_MESSAGE_END + REASONING_END immediately, one chunk before the signature arrives.

  3. handle_reasoning_event returns before signature accumulation — even if (1) were fixed, the method's early return discards the signature:

    if not reasoning_data["text"]:            # signature-only chunk has "" text
        if reasoning_data.get("id"):
            self.active_run["pending_reasoning_id"] = reasoning_data["id"]
        return                                # <-- signature never stored
    ...
    if reasoning_data.get("signature"):
        self.active_run["reasoning_process"]["signature"] = reasoning_data["signature"]

Net effect: REASONING_ENCRYPTED_VALUE is never emitted for Bedrock Converse; on the next turn, replaying the assistant message either silently degrades multi-turn thinking or, in strict-verification modes, is rejected by Bedrock.

Reproduction

Steps to Reproduce

import os
from langchain_aws import ChatBedrockConverse

llm = ChatBedrockConverse(
    model="eu.anthropic.claude-sonnet-4-6",
    region_name=os.environ.get("AWS_REGION", "eu-central-1"),
    provider="anthropic",
    max_tokens=128000,
    additional_model_request_fields={
        "thinking": {"type": "adaptive"},
        "output_config": {"effort": "medium"},
    },
    timeout=3600,
)

for chunk in llm.stream([
    ("system", "You are a helpful assistant."),
    ("human", "Explain streaming LLM responses in 2 sentences."),
]):
    print(chunk.content)

Observed: a {'reasoning_content': {'text': ''}} chunk followed by a {'reasoning_content': {'signature': '...'}} chunk, both index: 0.

Minimal isolation of the bug against the library:

from ag_ui_langgraph.utils import resolve_reasoning_content

class C:
    def __init__(self, content): self.content = content

# Text chunk — works
print(resolve_reasoning_content(C([
    {"type": "reasoning_content", "reasoning_content": {"text": "The"}, "index": 0}
])))
# -> {'text': 'The', 'type': 'text', 'index': 0}

# Signature-only chunk — BUG: returns None, signature lost
print(resolve_reasoning_content(C([
    {"type": "reasoning_content", "reasoning_content": {"signature": "EpcCC=="}, "index": 0}
])))
# -> None   (expected: LangGraphReasoning with signature="EpcCC==")

# Empty-text chunk — BUG: returns None, triggers premature reasoning close-out
print(resolve_reasoning_content(C([
    {"type": "reasoning_content", "reasoning_content": {"text": ""}, "index": 0}
])))
# -> None   (expected: pass-through no-op, keep reasoning open)

Expected Behavior

Correctly, parse

Environment

## Environment

- `langchain-aws` (`ChatBedrockConverse`) against Bedrock Converse
- Model: `eu.anthropic.claude-sonnet-4-6` with `additional_model_request_fields={"thinking": {"type": "adaptive"}}`
- `ag_ui_langgraph` —v0.0.42
- `ag_ui_protocol` —v0.1.19

Screenshots

No response

Logs & Errors

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions