Skip to content

Implement and configure the Story agent

The Story Agent is a Strands agent generated with --protocol=AG-UI in Module 1, so the UI can stream from it over the Agent-User Interaction protocol via CopilotKit. It uses the Inventory MCP Server to manage the player’s items, and Strands’ built-in S3SessionManager to persist conversation history into the sessions bucket we provisioned in Module 2.

Update the following files in packages/story/dungeon_adventure_story/agent:

import logging
import os
import uuid
from functools import cache
from typing import Any, cast
from ag_ui.core import RunAgentInput
from ag_ui_strands import StrandsAgent, StrandsAgentConfig, create_strands_app
from aws_lambda_powertools.utilities import parameters
from dungeon_adventure_agent_connection import session_id_context
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from strands.session import FileSessionManager, S3SessionManager, SessionManager
from .agent import get_agent
logging.basicConfig(level=logging.INFO)
SESSION_ID_HEADER = "x-amzn-bedrock-agentcore-runtime-session-id"
@cache
def _resolve_sessions_bucket() -> str:
"""Read the conversation-history bucket name from runtime config.
Resolved lazily (and memoised) so `fastapi dev` can import this module
before ``RUNTIME_CONFIG_APP_ID`` is in the environment.
"""
application = os.environ.get("RUNTIME_CONFIG_APP_ID")
if not application:
raise RuntimeError("RUNTIME_CONFIG_APP_ID is not set — cannot resolve the StorySessions bucket.")
provider = parameters.AppConfigProvider(environment="default", application=application)
buckets = cast(dict[str, Any], provider.get("buckets", transform="json"))
return buckets["StorySessions"]["bucketName"]
def _session_manager_provider(input_data: RunAgentInput) -> SessionManager:
"""Create a session manager keyed by the AG-UI thread_id.
- In AgentCore (and `agent-serve`), persist to the shared ``StorySessions``
S3 bucket — the same bucket the Game API reads from to rebuild
conversation history on revisit.
- In `agent-serve-local` (`SERVE_LOCAL=true`), persist to a temp
directory so the agent can run fully offline against the local MCP
server without any AWS calls.
"""
session_id = input_data.thread_id or "default"
if os.environ.get("SERVE_LOCAL") == "true":
return FileSessionManager(session_id=session_id, storage_dir="/tmp/strands-sessions")
return S3SessionManager(session_id=session_id, bucket=_resolve_sessions_bucket())
# The template Agent is cloned per thread_id by ``StrandsAgent`` — we plug in
# a ``session_manager_provider`` so each thread gets its own session manager
# and conversation history is replayed on subsequent turns and survives agent
# restarts.
_agent_ctx = get_agent()
_agent = _agent_ctx.__enter__()
agui_agent = StrandsAgent(
agent=_agent,
name="StoryAgent",
description="A Strands Agent exposed via the AG-UI protocol.",
config=StrandsAgentConfig(session_manager_provider=_session_manager_provider),
)
class _SessionIdMiddleware(BaseHTTPMiddleware):
"""Bind the session ID for this request so downstream MCP / A2A clients forward it on outbound calls."""
async def dispatch(self, request: Request, call_next):
session_id = request.headers.get(SESSION_ID_HEADER) or str(uuid.uuid4())
with session_id_context(session_id):
return await call_next(request)
app = create_strands_app(agui_agent, path="/invocations")
app.add_middleware(_SessionIdMiddleware)

The changes are:

  • main.py adds a session_manager_provider that creates an S3SessionManager per thread_id when deployed, and falls back to an on-disk FileSessionManager under /tmp/strands-sessions when running under agent-serve-local (SERVE_LOCAL=true). Deployed, the S3 bucket is the same one the Game API’s queryActions reads from, so the browser can rebuild transcripts on revisit; locally the agent runs fully offline against the local MCP server with no AWS calls.
  • agent.py drops the sample subtract tool and swaps the system prompt for a dungeon-master one that invites the first user message to state the player’s name and genre, and uses the Inventory MCP Server’s tools.

To build the code:

Terminal window
pnpm build

To deploy your application, run the following command:

Terminal window
pnpm nx deploy infra "dungeon-adventure-infra-sandbox/*"

This deployment will take around 2 minutes to complete.

Once the deployment completes, you will see outputs similar to the following (some values have been redacted):

Terminal window
dungeon-adventure-infra-sandbox-Application
dungeon-adventure-infra-sandbox-Application: deploying... [2/2]
dungeon-adventure-infra-sandbox-Application
Deployment time: 354s
Outputs:
dungeon-adventure-infra-sandbox-Application.ElectroDbTableTableNameXXX = dungeon-adventure-infra-sandbox-Application-ElectroDbTableXXX-YYY
dungeon-adventure-infra-sandbox-Application.GameApiEndpointXXX = https://xxx.execute-api.region.amazonaws.com/prod/
dungeon-adventure-infra-sandbox-Application.GameUIDistributionDomainNameXXX = xxx.cloudfront.net
dungeon-adventure-infra-sandbox-Application.InventoryMcpArn = arn:aws:bedrock-agentcore:region:xxxxxxx:runtime/dungeonadventureventoryMcpServerXXXX-YYYY
dungeon-adventure-infra-sandbox-Application.RuntimeConfigApplicationId = xxxx
dungeon-adventure-infra-sandbox-Application.StoryAgentArn = arn:aws:bedrock-agentcore:region:xxxxxxx:runtime/dungeonadventurecationStoryAgentXXXX-YYYY
dungeon-adventure-infra-sandbox-Application.UserIdentityUserIdentityIdentityPoolIdXXX = region:xxx
dungeon-adventure-infra-sandbox-Application.UserIdentityUserIdentityUserPoolIdXXX = region_xxx

You can test your Agent by either:

  • Starting a local instance of the Agent server and chatting with it via the generated agent-chat target, or
  • Calling the deployed API using curl with a JWT token.

Open an interactive REPL against a locally-served copy of the AG-UI agent using the generated agent-chat target:

Terminal window
pnpm nx agent-chat story

agent-chat has dependsOn: ['agent-serve-local'], so there’s no need to start the agent server separately — Nx will spawn agent-serve-local (which in turn boots the Inventory MCP server locally), then launch the agent-chat-cli AG-UI REPL against http://localhost:8081/invocations. Your first message should tell the agent your hero’s name and the genre (for example: My name is Alice. Start my zombie adventure.) and the story will stream back.

If the command runs successfully, you should start to see the story being streamed back as an AG-UI event stream (Server-Sent Events) — RUN_STARTED / TEXT_MESSAGE_START / TEXT_MESSAGE_CONTENT chunks wrap the actual story tokens:

data: {"type":"RUN_STARTED","threadId":"Alice-zombie00000000000000000000000",...}
data: {"type":"TEXT_MESSAGE_START","messageId":"...","role":"assistant"}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"...","delta":"Greetings, Alice. "}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"...","delta":"The moans grow louder beyond the barricaded door..."}

Congratulations. You have built and deployed your first Strands Agent on Bedrock AgentCore Runtime! 🎉🎉🎉