# CDS agent event normalizer (reference)

Companion artifact for [Building Production Agentic AI with AWS Communication Developer Services and Amazon Bedrock AgentCore](https://www.factualminds.com/blog/aws-cds-agentic-ai-architecture/), part 6 of the **AWS Communication Developer Services** series (October 2026).

This is a reference sketch. It is not a FactualMinds production component and it has not run against live traffic.

## Files

| File | What it is |
| --- | --- |
| `normalize.py` | Maps SMS/RCS two-way messages, RCS suggestion taps, SMS/RCS delivery events and WhatsApp events into one contract, and decides whether an event may start an agent turn |
| `inbound-event.schema.json` | JSON Schema (draft 2020-12) for that contract. SES is not mapped |
| `sample-events/*.json` | Five synthetic events. Shapes follow the AWS documentation read on 28 October 2026. Numbers are 555 test numbers and the account ID is the AWS docs placeholder |
| `test_normalize.py` | Assert-based self-check |
| `architecture.drawio` | The diagram in the post |

## Run it

Python 3.12 or later, no dependencies:

```bash
python3 -I test_normalize.py
# ok: 5 sample events, 3 reach the model, 2 receipts kept from the model, duplicate key stable, 5 malformed events raise
```

## What it shows

- Delivery and read receipts never start a turn.
- Malformed events raise (a broken RCS tap payload, a WhatsApp entry with no messages or statuses, an empty SNS batch) so they land in a dead-letter queue instead of being dropped quietly.
- The same SNS message delivered twice produces the same `dedup_key`, so a conditional write can drop the second one.
- An RCS button tap is routed by `postbackData`, not display text.
- A WhatsApp message asking for a refund still reaches the model. The protection is that the agent has no refund tool, not a filter on the words.

## Limits

- SMS versus RCS is inferred from whether `destinationNumber` is an E.164 number. If you run one SNS topic per channel, pass the channel from the topic instead.
- WhatsApp media, location and reaction messages are classified with `text: null` and do not reach the model. The caller must route `kind == customer_message` events where `reaches_model()` is false to a person or a media handler; the sketch does not.
- Configuration-set delivery events come out with `channel: "sms_or_rcs"`, because the payload does not say which channel delivered the message.
- SES bounce and complaint events are not mapped here.

## Sources

- [Two-way SMS payload](https://docs.aws.amazon.com/sms-voice/latest/userguide/two-way-sms-payload.html)
- [Receiving inbound RCS messages](https://docs.aws.amazon.com/sms-voice/latest/userguide/rcs-inbound.html)
- [Messaging events (RCS suggestion taps)](https://docs.aws.amazon.com/end-user-messaging/latest/userguide/nx-features-messaging-events.html)
- [SMS event format](https://docs.aws.amazon.com/sms-voice/latest/userguide/configuration-sets-event-format.html)
- [End User Messaging Social event format](https://docs.aws.amazon.com/social-messaging/latest/userguide/managing-event-destination-dlrs.html)
