ag-ui-protocol / ag-ui-protocol/ag-ui
[Bug]: `ag_ui_langgraph` drops Anthropic extended-thinking signature on Bedrock Converse streams
- Lingua principale
- Python
- Stelle
- 15.9k
- Fork
- 1.4k
- Merge medio
- 1g 17h
- PR unite (30g)
- 163
Descrizione
### Pre-flight Checklist
- [x] I have searched [existing issues](https://github.com/ag-ui-protocol/ag-ui/issues) and this hasn't been reported yet.
- [x] 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`:
```python
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:
```python
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
```python
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:
```python
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
```text
## 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
```shell
```
### Additional Context
_No response_
Guida per i contributori
Apri la guida per i contributori
Valutazione
Questa issue non è ancora stata valutata.