Sponsored Content
Skip to content

Repository files navigation

The Neon Scratch Lounge (Demo for AWS Summit Los Angeles 2026)

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.


What this is

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:

  1. Retrieval-Augmented Generation β€” lore retrieved and injected as context before every Bedrock call
  2. 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
  3. Dead Letter Queue β€” DM invocation failures caught after all retries, failed state sent to SQS DLQ, safe narrative returned to the player via FormatResponse
  4. Idempotency β€” tool results cached in DynamoDB with TTL, scoped to campaignId:turnId:toolName
  5. Structured observability β€” every Lambda emits one JSON log line per request, queryable via CloudWatch Logs Insights

Architecture

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

Agentic tool-use loop in InvokeDungeonMaster

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

RetrieveLore β€” two modes

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)

Supporting infrastructure

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

Prerequisites

  • 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 if useBedrockKnowledgeBase: 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+

Deploy

Production

npm install
cd infra
npx cdk deploy --all

The deploy outputs NeonScratchApi.ApiUrl β€” you'll need that for the UI.

Dev environment

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=dev

Stack 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=dev

UI

The 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 dev

The 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-bucket

Demo: failure injection

The failure injection demo shows Step Functions retrying the InvokeDungeonMaster task after a forced error.

How it works:

  1. POST /demo/inject-failure β€” sets FORCE_TOOL_FAILURE=true as an environment variable on the invoke-dungeon-master Lambda.
  2. On the next player action, invoke-dungeon-master throws a DemoForcedFailure named error the first time it tries to execute a tool.
  3. Step Functions catches DemoForcedFailure and retries the task up to 3 times with exponential backoff and full jitter.
  4. 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.


Character classes

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

Locations in Neo-Pawsburg

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

Configuration

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
}

Switching lore retrieval modes

Bundled JSON (default) β€” no extra cost, works immediately:

"useBedrockKnowledgeBase": false

Bedrock Knowledge Base + AOSS β€” semantic vector search, ~$701/month idle (4 OCU minimum):

"useBedrockKnowledgeBase": true

Requires amazon.titan-embed-text-v1 model access. After deploy, wait 3–5 minutes for the ingestion job to finish before the KB returns results.


Tests

npm test
  • test/dice.test.ts β€” dice ranges, stat bonuses, idempotency key format
  • test/idempotency.test.ts β€” cache hit/miss, TTL expiry, cross-turn isolation
  • test/validate-tools.test.ts β€” tool schema validation, unknown tool rejection
  • test/campaign-summary.test.ts β€” summary triggers at 20 turns, history trimming
  • test/special-abilities.test.ts β€” NineLifes, Shield, Vanish cooldown mechanics
  • test/integration/workflow.test.ts β€” CDK synth assertions, class stat invariants

File structure

β”œβ”€β”€ 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

Cost estimate

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.


Cleanup

cd infra
npx cdk destroy --all
# or for dev environment:
npx cdk destroy --all -c envName=dev

S3 buckets and DynamoDB tables use RemovalPolicy.DESTROY for easy teardown.


TODO

  • Item stat bonuses β€” inventory is currently a flat string array; equipping gear (e.g. HackingClaws) does not modify playerStats. Add a static item registry in tool-runner.ts that applies/reverses stat deltas on pickup/drop (auto-equip on pickup first, then explicit equip action).

About

No description, website, or topics provided.

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages