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

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions