Skip to content

Bug: converse-stream Event Stream payloads are double-wrapped, causing KeyError in AWS SDK #162

@KMiya84377

Description

@KMiya84377

Environment

  • aimock: 1.19.1
  • strands-agents: 1.38.0
  • Python: 3.13
  • boto3 / botocore: 1.43.6

Steps to reproduce

1. Create a fixture file

// fixtures/agent.json
{
  "fixtures": [
    {
      "match": { "userMessage": "hello" },
      "response": { "content": "Hello! How can I help you today?" }
    }
  ]
}

2. Start aimock

npx -p @copilotkit/aimock llmock -f ./fixtures --port 4010

3. Run the agent

# main.py
from strands import Agent
from strands.models import BedrockModel

model = BedrockModel(endpoint_url="http://localhost:4010")
agent = Agent(model=model)
agent("hello")
uv run main.py

Actual output:

Traceback (most recent call last):
  ...
  File ".../strands/event_loop/streaming.py", line 436, in process_stream
    state["message"] = handle_message_start(chunk["messageStart"], state["message"])
  File ".../strands/event_loop/streaming.py", line 169, in handle_message_start
    message["role"] = event["role"]
KeyError: 'role'

Expected output:

The agent runs without error and returns a response.

Root cause

In src/bedrock-converse.ts, Converse stream event payloads are double-wrapped:

// current (incorrect) — buildBedrockStreamTextEvents
{ eventType: "messageStart", payload: { messageStart: { role: "assistant" } } }
//                                      ^^^^^^^^^^^^ redundant outer key

Strands Agents uses boto3 internally to call Bedrock. The problem unfolds as follows:

  1. aimock encodes a binary Event Stream frame with :event-type: messageStart header and JSON payload {"messageStart": {"role": "assistant"}}
  2. botocore receives the frame and reads the :event-type header → uses "messageStart" as the dict key
  3. botocore tries to map the JSON payload to the MessageStartEvent shape, which expects {"role": "assistant"} at the top level
  4. The actual payload is {"messageStart": {"role": "assistant"}} — no fields match the shape → botocore silently returns {}
  5. boto3 returns {"messageStart": {}} to Strands — this is botocore's BaseEventStreamParser._do_parse behavior (botocore/parsers.py):
    event_type = response['headers'].get(':event-type')  # "messageStart"
    event_shape = shape.members.get(event_type)          # MessageStartEvent shape
    final_parsed[event_type] = self._do_parse(response, event_shape)
    # payload {"messageStart": {"role": "assistant"}} doesn't match the shape → {}
  6. Strands' process_stream function (via handle_message_start) in src/strands/event_loop/streaming.py reads chunk["messageStart"]["role"]{}["role"]KeyError: 'role'

Suggested fix

The payload should not be wrapped with the event type name — the :event-type header already carries that information:

// before (incorrect)
{ eventType: "messageStart", payload: { messageStart: { role: "assistant" } } }

// after (correct)
{ eventType: "messageStart", payload: { role: "assistant" } }

The same double-wrapping exists in all Converse stream event builders:

Event type Affected function
messageStart, contentBlockStart, contentBlockDelta, contentBlockStop, messageStop, metadata buildBedrockStreamTextEvents
same + tool events buildBedrockStreamToolCallEvents, buildBedrockStreamContentWithToolCallsEvents

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions