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

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

Aperta
#2,174 4 commenti 1 reazione 0 assegnatari Vedi su GitHub
bug
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.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.