ag-ui-protocol / ag-ui-protocol/ag-ui

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

Abierto
#2,174 4 comentarios 1 reacción 0 asignados Ver en GitHub
bug
Lenguaje dominante
Python
Estrellas
15.9k
Forks
1.4k
Merge medio
1 d 17 h
PR fusionados (30 d)
163

Descripción

### 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_

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.