Neo-Pawsburg, 2087. A cyberpunk cat RPG running on production-grade AWS infrastructure β demonstrating five reliability patterns with an AI Dungeon Master powered by Amazon Bedrock.
Built as a live conference demo for AWS LA Summit 2026. The contrast between "cyberpunk cat adventure" and "enterprise distributed systems" is intentional. That's the point.
A fully-deployed, playable RPG where every player action flows through a real AWS architecture: API Gateway β Lambda β Step Functions β Bedrock (Claude) β DynamoDB. The UI shows the AWS mechanics in real time alongside the game β Step Functions workflow trace, CloudWatch log events, token counts, and dice rolls.
The demo illustrates five reliability patterns you'd find in any serious AI agent system:
- Retrieval-Augmented Generation β lore retrieved and injected as context before every Bedrock call
- Step Functions retries β exponential backoff with full jitter on named errors (
DemoForcedFailure,DMOutputValidationError)- The DM uses Bedrock's native tool-use loop to call dice/combat/inventory tools and see real results before writing narrative
- Dead Letter Queue β DM invocation failures caught after all retries, failed state sent to SQS DLQ, safe narrative returned to the player via FormatResponse
- Idempotency β tool results cached in DynamoDB with TTL, scoped to
campaignId:turnId:toolName - Structured observability β every Lambda emits one JSON log line per request, queryable via CloudWatch Logs Insights
Player Action
β
βΌ
API Gateway REST API
β POST /action
βΌ
dungeon-controller (Lambda)
β publishes PlayerAction to EventBridge (audit trail)
β starts Express Workflow async β returns turnId immediately
β player polls GET /action/status?turnId=... for result
βΌ
Step Functions Express Workflow
β
βββ RetrieveLore β lore context injected before DM call (see modes below)
βββ InvokeDungeonMaster β Bedrock native tool-use agentic loop (Claude Sonnet 4)
β β DM calls game tools β sees real results β calls finalize-response
β βββ retry: DemoForcedFailure (MaxAttempts: 3, FULL jitter)
β βββ retry: DMOutputValidationError (MaxAttempts: 3, FULL jitter)
β βββ catch: States.ALL β SQS DLQ β safe narrative β FormatResponse
βββ PersistCampaign β DynamoDB put; summarise if history > 20 turns
βββ FormatResponse β typed FormattedResponse written to DynamoDB, turnId resolved
invoke-dungeon-master runs a multi-turn Bedrock conversation loop rather than a single-shot prompt. The DM is given 9 tools:
| Tool | Purpose |
|---|---|
roll-dice |
Roll dN (d4/d6/d8/d10/d12/d20) β d20 enforced for skill checks |
apply-damage |
Apply damage or healing to a combatant |
update-inventory |
Add or remove items from player inventory |
award-xp |
Grant XP and trigger level-up if threshold reached |
update-location |
Move player to a new location |
apply-effect |
Apply status effects with duration |
use-special-ability |
Activate class special ability |
update-quest-log |
Record quest progress |
finalize-response |
Required last call β carries the narrative, combat summary, and game state |
The loop runs until the DM calls finalize-response (max 12 tool iterations). All tool execution happens inside the Lambda via lambda/shared/tool-runner.ts, so the DM always sees real dice totals before writing narrative.
invoke-dungeon-master loop:
send message to Bedrock (tools array)
β
βΌ
stop_reason == "tool_use"?
β yes β execute each tool via tool-runner.ts
β write tool_result blocks
β send next message with tool results
β βββ repeat
β
βββ stop_reason == "end_turn" after finalize-response
β extract DMOutput from finalize-response args
β return to Step Functions
Default (useBedrockKnowledgeBase: false)
RetrieveLore Lambda
βββ keyword scoring against bundled JSON (locations + enemies + items + classes, ~16 KB)
returns top-5 matching lore entries + guaranteed current location entry
AOSS mode (useBedrockKnowledgeBase: true)
RetrieveLore Lambda
βββ bedrock.retrieve() β Bedrock Knowledge Base
βββ OpenSearch Serverless (VECTORSEARCH collection)
βββ kNN index (HNSW/faiss, titan-embed-text-v1, dim 1536)
EventBridge custom bus (neon-scratch-events)
βββ PlayerAction rule β SFN StartExecution (async, audit trail only)
DynamoDB (on-demand)
βββ neon-scratch-campaigns (campaignId PK, playerId GSI, TTL 30 days)
βββ neon-scratch-tool-results (idempotencyKey PK, TTL 1 hour)
βββ neon-scratch-turn-results (turnId PK, TTL)
SQS Dead Letter Queue
βββ neon-scratch-dungeon-dlq (14-day retention)
CloudWatch
βββ Log groups per Lambda (1-week retention)
βββ Metric filters (token usage, dice rolls, monsters defeated, active campaigns)
βββ Dashboard: NeonScratchLounge
βββ Alarms (DLQ depth, DM p99 latency, controller p99 latency, error rate)
API Gateway (REST, prod stage)
βββ POST /action β dungeon-controller
βββ POST /demo/inject-failure β sets FORCE_TOOL_FAILURE=true on invoke-dungeon-master
βββ POST /demo/clear-failure β clears the env var
βββ GET /demo/logs β CloudWatch Logs Insights query for a campaignId
- AWS CLI configured with deploy permissions
- CDK bootstrapped in your target account/region:
cdk bootstrap - Bedrock model access enabled in us-east-1:
us.anthropic.claude-sonnet-4-5-20250929-v1:0(cross-region inference profile)amazon.titan-embed-text-v1(only ifuseBedrockKnowledgeBase: true)
- OpenSearch Serverless service-linked role (only if
useBedrockKnowledgeBase: true):aws iam create-service-linked-role --aws-service-name observability.aoss.amazonaws.com
- Node 20+
npm install
cd infra
npx cdk deploy --allThe deploy outputs NeonScratchApi.ApiUrl β you'll need that for the UI.
All physical resource names are suffixed (-dev), so dev and prod can coexist in the same account without collision:
cd infra
npx cdk deploy --all -c envName=devStack names get a capitalised suffix (NeonScratchData-Dev, NeonScratchWorkflow-Dev, etc.). CloudWatch metrics use the NeonScratchDev namespace. To destroy the dev environment independently:
npx cdk destroy --all -c envName=devThe ui/ directory is a React + Vite app. Left panel is the game; right panel shows the AWS mechanics live (Step Functions trace, CloudWatch log stream, token counts).
cd ui
npm install
cp .env.local.example .env.local
# Set VITE_API_GATEWAY_URL to the ApiUrl output from cdk deploy
npm run devThe CloudWatch Logs panel polls GET /demo/logs?campaignId=<id> after each turn and displays the actual Lambda invocation records filtered to the active campaign.
For a production static hosting setup:
npm run build
aws s3 sync dist/ s3://your-bucketThe failure injection demo shows Step Functions retrying the InvokeDungeonMaster task after a forced error.
How it works:
POST /demo/inject-failureβ setsFORCE_TOOL_FAILURE=trueas an environment variable on theinvoke-dungeon-masterLambda.- On the next player action,
invoke-dungeon-masterthrows aDemoForcedFailurenamed error the first time it tries to execute a tool. - Step Functions catches
DemoForcedFailureand retries the task up to 3 times with exponential backoff and full jitter. POST /demo/clear-failureβ clears the env var so subsequent turns succeed.
The UI shows the retry animation in the workflow trace while the DM is being re-invoked.
| Class | HP | STR | AGI | ARC | STL | Gold | Special ability |
|---|---|---|---|---|---|---|---|
| TabbyWarrior | 120 | 8 | 5 | 2 | 4 | 10 | NineLifesPassive |
| SiameseMage | 70 | 3 | 6 | 9 | 5 | 15 | LaserFocusSpell |
| MaineCoonPaladin | 100 | 6 | 3 | 5 | 2 | 20 | HolyHairballShield |
| SphinxRogue | 80 | 4 | 9 | 4 | 9 | 25 | SandstormVanish |
- NineLifesPassive β survives one killing blow per campaign with 1 HP
- LaserFocusSpell β spend 10 HP for 3Γ arcane damage on next attack (declare before roll)
- HolyHairballShield β blocks up to 15 damage once per combat encounter
- SandstormVanish β all enemies miss for one turn; 3-turn cooldown
| Location | Danger | Notes |
|---|---|---|
| NeonScratchLounge | Safe | Resistance HQ, Madame Fluffington |
| ChromeAlley | High | RoombaCore patrols, laser graffiti |
| NightMarket | Low | Merchants, info brokers |
| SewersOfForgetfulness | Medium | Mutant rats, feral cats |
| RoombaCoreTower | Extreme | Final dungeon, CEO Roomba |
All tunable parameters are in infra/cdk.json under context.neonScratch:
{
"maxConversationHistory": 20,
"campaignTtlDays": 30,
"toolResultTtlSeconds": 3600,
"xpPerLevel": 100,
"historyTrimCount": 5,
"retryMaxAttempts": 3,
"retryIntervalSeconds": 2,
"retryBackoffRate": 2,
"bedrockRegion": "us-east-1",
"bedrockModelId": "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
"useBedrockKnowledgeBase": false
}Bundled JSON (default) β no extra cost, works immediately:
"useBedrockKnowledgeBase": falseBedrock Knowledge Base + AOSS β semantic vector search, ~$701/month idle (4 OCU minimum):
"useBedrockKnowledgeBase": trueRequires amazon.titan-embed-text-v1 model access. After deploy, wait 3β5 minutes for the ingestion job to finish before the KB returns results.
npm testtest/dice.test.tsβ dice ranges, stat bonuses, idempotency key formattest/idempotency.test.tsβ cache hit/miss, TTL expiry, cross-turn isolationtest/validate-tools.test.tsβ tool schema validation, unknown tool rejectiontest/campaign-summary.test.tsβ summary triggers at 20 turns, history trimmingtest/special-abilities.test.tsβ NineLifes, Shield, Vanish cooldown mechanicstest/integration/workflow.test.tsβ CDK synth assertions, class stat invariants
βββ infra/
β βββ bin/app.ts CDK entry point (reads envName from context)
β βββ stacks/
β β βββ data-stack.ts DynamoDB tables
β β βββ knowledge-base-stack.ts AOSS + Bedrock KB (only when useBedrockKnowledgeBase: true)
β β βββ workflow-stack.ts Lambdas + Step Functions + EventBridge
β β βββ api-stack.ts API Gateway + demo endpoints
β β βββ observability-stack.ts CloudWatch dashboard + alarms
β βββ cdk.json Config + context variables
βββ lambda/
β βββ shared/
β β βββ types.ts Shared TypeScript types
β β βββ logger.ts Structured JSON logger
β β βββ idempotency.ts DynamoDB idempotency cache
β β βββ tool-runner.ts All 8 game tool functions + runTool() dispatcher
β βββ dungeon-controller/index.ts API entry point, starts SFN async, returns turnId
β βββ demo/
β β βββ inject-failure.ts Injects/clears FORCE_TOOL_FAILURE on invoke-dungeon-master
β β βββ fetch-logs.ts CloudWatch Logs Insights query helper
β βββ workflow/
β βββ retrieve-lore.ts RAG or bundled JSON lore retrieval
β βββ invoke-dungeon-master.ts Bedrock agentic loop β calls tools, writes finalize-response
β βββ execute-tool.ts Thin wrapper over tool-runner (deployed, not in SFN chain)
β βββ persist-campaign.ts DynamoDB campaign state write + history summarisation
β βββ format-response.ts Shapes final typed response for the player
βββ lore/ locations.json, enemies.json, items.json, classes.json
βββ scripts/demo.ts CLI helper used during the live demo
βββ test/
βββ ui/ React + Vite + Tailwind game UI
βββ README.md
Estimates in USD/month. Claude Sonnet 4 at $3.00/1M input, $15.00/1M output.
The agentic loop runs 2β4 Bedrock turns per player action (tool calls + finalize-response), and input tokens accumulate across iterations because each call re-sends prior messages and tool results. Tool schemas (9 tools) and lore context add ~3,500β4,000 tokens of overhead per turn. Typical observed usage: ~9,000β12,000 input + 700β1,000 output tokens per turn.
| Service | Idle (no AOSS) | 500 req/mo | 5,000 req/mo |
|---|---|---|---|
| Amazon Bedrock | β | ~$20.00 | ~$200.00 |
| CloudWatch (dashboard + 4 alarms) | $3.50 | $3.50 | $3.50 |
| Lambda (6 fns Γ ~2s avg Γ 512 MB) | β | $0.02 | $0.25 |
| Step Functions Express | β | $0.04 | $0.40 |
| DynamoDB + API GW + EventBridge + SQS | β | $0.01 | $0.10 |
| Total | ~$3.52 | ~$23.57 | ~$204.25 |
Add ~$701/month for the AOSS mode (4 OCU minimum regardless of traffic).
Bedrock cost per request β $0.040 (~10,000 input + 850 output tokens across the agentic loop, plus an amortised summarise call every 20 turns). Verify current pricing at aws.amazon.com/bedrock/pricing.
cd infra
npx cdk destroy --all
# or for dev environment:
npx cdk destroy --all -c envName=devS3 buckets and DynamoDB tables use RemovalPolicy.DESTROY for easy teardown.
- Item stat bonuses β inventory is currently a flat string array; equipping gear (e.g.
HackingClaws) does not modifyplayerStats. Add a static item registry intool-runner.tsthat applies/reverses stat deltas on pickup/drop (auto-equip on pickup first, then explicit equip action).