Configurar un monorepo
Tarea 1: Crear un monorepo
Sección titulada «Tarea 1: Crear un monorepo»Para crear un nuevo monorepo, desde el directorio deseado, ejecuta el siguiente comando:
pnpm create @aws/nx-workspace dungeon-adventure --iac=cdkyarn create @aws/nx-workspace dungeon-adventure --iac=cdknpm create @aws/nx-workspace -- dungeon-adventure --iac=cdkbun create @aws/nx-workspace dungeon-adventure --iac=cdkEsto configurará un monorepo NX dentro del directorio dungeon-adventure. Cuando abras el directorio en VSCode, verás esta estructura de archivos:
Directorio.nx/
- …
Directorio.vscode/
- …
Directorionode_modules/
- …
Directoriopackages/ aquí es donde residirán tus sub-proyectos
- …
- .gitignore
- biome.json configura Biome para linting y formateo
- nx.json configura el CLI de Nx y los valores predeterminados del monorepo
- package.json todas las dependencias de node se definen aquí
- pnpm-lock.yaml o bun.lock, yarn.lock, package-lock.json dependiendo del gestor de paquetes
- pnpm-workspace.yaml si usas pnpm
- README.md
- tsconfig.base.json todos los sub-proyectos basados en node extienden esto
- tsconfig.json
- aws-nx-plugin.config.mts configuración para el Nx Plugin for AWS
Tarea 2: Estructurar el Dungeon Adventure Game
Sección titulada «Tarea 2: Estructurar el Dungeon Adventure Game»Con el espacio de trabajo listo, estructuramos los sub-proyectos del juego: la Game API, el Story Agent, el servidor MCP de inventario, la base de datos del juego y el sitio web, junto con las conexiones que los unen. Hay dos formas de hacerlo:
- Rápido — copia los comandos directamente del diagrama a continuación y ejecútalos. La forma más rápida de llegar al mismo punto de partida.
- Paso a paso — expande la sección a continuación para ejecutar cada generador tú mismo y examinar exactamente lo que produce cada uno.
El diagrama a continuación es el espacio de trabajo de Dungeon Adventure: cada proyecto, componente y conexión que construirás en este módulo. Haz clic en Copy commands para tomar toda la serie y luego ejecútalos desde dentro del directorio dungeon-adventure que creaste en la Tarea 1.
Paso a paso
En lugar de copiar todos los comandos a la vez, puedes ejecutar cada generador individualmente. Esta es la mejor manera de entender qué agrega cada generador a tu espacio de trabajo. Ejecuta cada generador en orden, desde dentro del directorio dungeon-adventure que creaste en la Tarea 1.
Crear una Game API
Sección titulada «Crear una Game API»Primero, creemos nuestra Game API. Para hacerlo, crea una API tRPC llamada GameApi siguiendo estos pasos:
pnpm nx g @aws/nx-plugin:ts#api --name=GameApi --framework=trpc --no-interactiveyarn nx g @aws/nx-plugin:ts#api --name=GameApi --framework=trpc --no-interactivenpx nx g @aws/nx-plugin:ts#api --name=GameApi --framework=trpc --no-interactivebunx nx g @aws/nx-plugin:ts#api --name=GameApi --framework=trpc --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#api --name=GameApi --framework=trpc --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#api --name=GameApi --framework=trpc --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#api --name=GameApi --framework=trpc --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#api --name=GameApi --framework=trpc --no-interactive --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#api - Complete los parámetros requeridos
- name: GameApi
- framework: trpc
- Haga clic en
Generate
Verás algunos archivos nuevos aparecer en tu árbol de archivos.
Examinar los archivos generados por ts#api en detalle
A continuación se muestra una lista de todos los archivos que han sido generados por el generador ts#api. Vamos a examinar algunos de los archivos clave resaltados en el árbol de archivos:
Directoriopackages/
Directoriocommon/
Directorioconstructs/
Directoriosrc/
Directorioapp/ constructs CDK específicos de la aplicación
Directorioapis/
- game-api.ts construct CDK para crear tu API tRPC
- index.ts
- …
- index.ts
Directoriocore/ constructs CDK genéricos
Directorioapi/
- rest-api.ts construct base CDK para API Gateway Rest API
- trpc-utils.ts utilidades para constructs CDK de API tRPC
- utils.ts utilidades para constructs de API
- index.ts
- runtime-config.ts
- index.ts
- project.json
- …
Directoriogame-api/ API tRPC
Directoriosrc/
Directorioclient/ cliente vanilla típicamente usado para llamadas máquina a máquina en TS
- index.ts
Directoriomiddleware/ instrumentación con powertools
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
Directorioschema/ definiciones de entradas y salidas para tu API
- index.ts
- echo.ts esquema de entrada y salida de ejemplo
- z-async-iterable.ts esquema Zod envolvente para salida de suscripción tRPC
Directorioprocedures/ implementaciones específicas de tus procedimientos/rutas de la API
- echo.ts implementación de procedimiento de ejemplo
- index.ts
- init.ts configura el contexto y middleware
- handler.ts punto de entrada del Lambda handler (usa streaming de respuesta para REST APIs)
- local-server.ts usado al ejecutar el servidor tRPC localmente
- router.ts define el router tRPC y todos los procedimientos
- project.json
- …
- vitest.workspace.ts
Veamos estos archivos clave:
import { echo } from './procedures/echo.js';import { t } from './init.js';
export const router = t.router;
export const appRouter = router({ echo,});
export type AppRouter = typeof appRouter;El router define el router tRPC para tu API y es donde declararás todos tus métodos de API. Como puedes ver arriba, tenemos un método llamado echo con su implementación en el archivo ./procedures/echo.ts. El punto de entrada del Lambda handler está en handler.ts, que se configura automáticamente por el generador.
import { publicProcedure } from '../init.js';import { EchoInputSchema, EchoOutputSchema,} from '../schema/echo.js';
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ message: opts.input.message }));Este archivo es la implementación del método echo y como puedes ver está fuertemente tipado declarando sus estructuras de datos de entrada y salida.
import { z } from 'zod';
export const EchoInputSchema = z.object({ message: z.string().max(1024),});
export type IEchoInput = z.TypeOf<typeof EchoInputSchema>;
export const EchoOutputSchema = z.object({ message: z.string().max(1024),});
export type IEchoOutput = z.TypeOf<typeof EchoOutputSchema>;Todas las definiciones de esquema tRPC se definen usando Zod y se exportan como tipos TypeScript mediante la sintaxis z.TypeOf.
import { Construct } from 'constructs';import * as url from 'url';import { Distribution } from 'aws-cdk-lib/aws-cloudfront';import { Code, Runtime, Function, FunctionProps, Tracing,} from 'aws-cdk-lib/aws-lambda';import { RuntimeConfig } from '../../core/runtime-config.js';import { AuthorizationType, LambdaIntegration, ResponseTransferMode,} from 'aws-cdk-lib/aws-apigateway';import { Aspects, Duration } from 'aws-cdk-lib';import { PolicyDocument, PolicyStatement, Effect, AnyPrincipal, IGrantable, Grant,} from 'aws-cdk-lib/aws-iam';import { ApiIntegrations, IntegrationBuilder, RestApiIntegration,} from '../../core/api/utils.js';import { findCloudFrontDomainNames } from '../../core/cloudfront.js';import { AddCorsPreflightAspect, RestApi } from '../../core/api/rest-api.js';import { Procedures, routerToOperations } from '../../core/api/trpc-utils.js';import { AppRouter, appRouter } from '@dungeon-adventure/game-api';
// String union type for all API operation namestype Operations = Procedures<AppRouter>;
/** * Properties for creating a GameApi construct * * @template TIntegrations - Map of operation names to their integrations */export interface GameApiProps< TIntegrations extends ApiIntegrations<Operations, RestApiIntegration>,> { /** * Map of operation names to their API Gateway integrations */ integrations: TIntegrations; /** * Whether to enable AWS WAFv2 with the default managed ruleset on the API's default stage. * * @default true */ enableWaf?: boolean;}
/** * A CDK construct that creates and configures an AWS API Gateway REST API * specifically for GameApi. * @template TIntegrations - Map of operation names to their integrations */export class GameApi< TIntegrations extends ApiIntegrations<Operations, RestApiIntegration>,> extends RestApi<Operations, TIntegrations> { private allowedOrigins: readonly string[] = ['*'];
/** * Creates default integrations for all operations, which implement each operation as * its own individual lambda function. * * @param scope - The CDK construct scope * @returns An IntegrationBuilder with default lambda integrations */ public static defaultIntegrations = (scope: Construct) => { const rc = RuntimeConfig.ensure(scope); return IntegrationBuilder.rest({ pattern: 'isolated', operations: routerToOperations(appRouter), defaultIntegrationOptions: { runtime: Runtime.NODEJS_LATEST, handler: 'index.handler', code: Code.fromAsset( url.fileURLToPath( new URL( '../../../../../../dist/packages/game-api/bundle', import.meta.url, ), ), ), timeout: Duration.seconds(30), tracing: Tracing.ACTIVE, } as FunctionProps, buildDefaultIntegration: (op, props: FunctionProps) => { const handler = new Function(scope, `GameApi${op}Handler`, props); handler.addEnvironment('RUNTIME_CONFIG_APP_ID', rc.appConfigApplicationId); rc.grantReadAppConfig(handler); return { handler, integration: new LambdaIntegration(handler, { responseTransferMode: ResponseTransferMode.STREAM, }), }; }, }); };
constructor( scope: Construct, id: string, props: GameApiProps<TIntegrations>, ) { super(scope, id, { apiName: 'GameApi', defaultMethodOptions: { authorizationType: AuthorizationType.IAM, }, deployOptions: { tracingEnabled: true, }, policy: new PolicyDocument({ statements: [ // Open up OPTIONS to allow browsers to make unauthenticated preflight requests new PolicyStatement({ effect: Effect.ALLOW, principals: [new AnyPrincipal()], actions: ['execute-api:Invoke'], resources: ['execute-api:/*/OPTIONS/*'], }), ], }), operations: routerToOperations(appRouter), ...props, }); Aspects.of(this).add(new AddCorsPreflightAspect(() => this.allowedOrigins)); }
/** * Restricts CORS to the provided origins * * Configures the CloudFront distribution domains or origin strings * as the only permitted CORS origins in API Gateway preflight responses and the AWS * Lambda integrations. Any custom domain names (aliases) configured on a CloudFront * distribution are included automatically alongside its default `*.cloudfront.net` * domain. * * @param origins - The origin strings, CloudFront distributions, or objects containing a CloudFront distribution to grant CORS from */ public restrictCorsTo( ...origins: (string | Distribution | { cloudFrontDistribution: Distribution })[] ) { const allowedOrigins = origins.flatMap((origin) => typeof origin === 'string' ? [origin] : findCloudFrontDomainNames( 'cloudFrontDistribution' in origin ? origin.cloudFrontDistribution : origin, ).map((domain) => `https://${domain}`), );
this.allowedOrigins = allowedOrigins;
// Set ALLOWED_ORIGINS environment variable for all Lambda integrations Object.values(this.integrations).forEach((integration) => { if ('handler' in integration && integration.handler instanceof Function) { integration.handler.addEnvironment( 'ALLOWED_ORIGINS', allowedOrigins.join(','), ); } }); }
/** * Grants IAM permissions to invoke any method on this API. * * @param grantee - The IAM principal to grant permissions to */ public grantInvokeAccess(grantee: IGrantable) { // Here we grant grantee permission to call the api. // Machine to machine fine-grained access can be defined here using more specific principals (eg roles or // users) and resources (eg which api paths may be invoked by which principal) if required. this.api.addToResourcePolicy( new PolicyStatement({ effect: Effect.ALLOW, principals: [grantee.grantPrincipal], actions: ['execute-api:Invoke'], resources: ['execute-api:/*'], }), );
Grant.addToPrincipal({ grantee, actions: ['execute-api:Invoke'], resourceArns: [this.api.arnForExecuteApi('*', '/*', '*')], }); }}Este es el construct CDK que define nuestro GameApi. Proporciona un método defaultIntegrations que automáticamente crea una función Lambda para cada procedimiento en nuestra API tRPC, apuntando a la implementación de la API empaquetada. Esto significa que en el momento de cdk synth, no ocurre empaquetado (a diferencia de usar NodeJsFunction) ya que ya lo hemos empaquetado como parte del target de build del proyecto backend.
Crear el Story Agent
Sección titulada «Crear el Story Agent»Ahora creemos nuestro Story Agent.
Story agent: proyecto Python
Sección titulada «Story agent: proyecto Python»Para crear un proyecto Python:
pnpm nx g @aws/nx-plugin:py#project --name=story --no-interactiveyarn nx g @aws/nx-plugin:py#project --name=story --no-interactivenpx nx g @aws/nx-plugin:py#project --name=story --no-interactivebunx nx g @aws/nx-plugin:py#project --name=story --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runyarn nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runnpx nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runbunx nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - py#project - Complete los parámetros requeridos
- name: story
- Haga clic en
Generate
Verás algunos archivos nuevos aparecer en tu árbol de archivos.
Examinar los archivos generados por py#project en detalle
El py#project genera estos archivos:
Directorio.venv/ entorno virtual único para el monorepo
- …
Directoriopackages/
Directoriostory/
Directoriodungeon_adventure_story/ módulo python
- …
Directoriotests/
- …
- .python-version
- pyproject.toml
- project.json
- .python-version versión de python de uv fijada
- pyproject.toml
- uv.lock
Esto ha configurado un proyecto Python y un UV Workspace con entorno virtual compartido.
Story agent
Sección titulada «Story agent»Para agregar un agente Strands al proyecto con el generador py#agent:
pnpm nx g @aws/nx-plugin:py#agent --project=story --auth=cognito --protocol=ag-ui --no-interactiveyarn nx g @aws/nx-plugin:py#agent --project=story --auth=cognito --protocol=ag-ui --no-interactivenpx nx g @aws/nx-plugin:py#agent --project=story --auth=cognito --protocol=ag-ui --no-interactivebunx nx g @aws/nx-plugin:py#agent --project=story --auth=cognito --protocol=ag-ui --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:py#agent --project=story --auth=cognito --protocol=ag-ui --no-interactive --dry-runyarn nx g @aws/nx-plugin:py#agent --project=story --auth=cognito --protocol=ag-ui --no-interactive --dry-runnpx nx g @aws/nx-plugin:py#agent --project=story --auth=cognito --protocol=ag-ui --no-interactive --dry-runbunx nx g @aws/nx-plugin:py#agent --project=story --auth=cognito --protocol=ag-ui --no-interactive --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - py#agent - Complete los parámetros requeridos
- project: story
- auth: cognito
- protocol: ag-ui
- Haga clic en
Generate
Verás algunos archivos nuevos aparecer en tu árbol de archivos.
Examinar los archivos generados por py#agent en detalle
El py#agent genera estos archivos:
Directoriopackages/
Directoriostory/
Directoriodungeon_adventure_story/ módulo python
Directorioagent/
- main.py punto de entrada para tu agente en Bedrock AgentCore Runtime
- agent.py define un agente y herramientas de ejemplo
- session.py resuelve un SessionManager para persistir el estado de la conversación
Directoriomiddleware/
- session_id_middleware.py vincula el ID de sesión de AgentCore entrante para la solicitud
- Dockerfile define la imagen docker para el despliegue en AgentCore Runtime
Directoriocommon/constructs/
Directoriosrc
Directorioapp/agents/story-agent/
- story-agent.ts construct para desplegar tu agente Story en AgentCore Runtime
Veamos algunos de los archivos en detalle:
from contextlib import contextmanager
from strands import Agent, toolfrom strands.hooks import HookCallback, HookProviderfrom strands_tools import current_timefrom dungeon_adventure_agent_connection import log_model_errors, log_tool_errors
@tooldef subtract(a: int, b: int) -> int: return a - b
AGENT_HOOKS: list[HookProvider | HookCallback] = [log_model_errors, log_tool_errors]
@contextmanagerdef get_agent(): yield Agent( name="StoryAgent", description="StoryAgent Strands Agent", system_prompt="""You are a mathematical wizard.Use your tools for mathematical tasks.Refer to tools as your 'spellbook'.""", tools=[subtract, current_time], hooks=AGENT_HOOKS, )Esto crea un agente Strands de ejemplo y define una herramienta de sustracción. log_model_errors y log_tool_errors son hooks del proyecto compartido dungeon_adventure_agent_connection que registran fallos de modelo/herramienta en lugar de dejarlos fallar silenciosamente.
import uuid
from fastapi import Requestfrom starlette.middleware.base import BaseHTTPMiddleware
from dungeon_adventure_agent_connection import session_id_context
SESSION_ID_HEADER = "x-amzn-bedrock-agentcore-runtime-session-id"
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)El SessionIdMiddleware vincula el ID de sesión del runtime de AgentCore entrante a un ContextVar durante la duración de la solicitud.
import loggingimport uuidfrom contextlib import asynccontextmanager
from ag_ui.core import EventType, RunAgentInput, RunErrorEventfrom ag_ui.encoder import EventEncoderfrom ag_ui_strands import StrandsAgent, StrandsAgentConfigfrom dungeon_adventure_agent_connection import get_current_session_id, session_id_contextfrom fastapi import FastAPI, Requestfrom fastapi.middleware.cors import CORSMiddlewarefrom fastapi.responses import StreamingResponse
from .agent import AGENT_HOOKS, get_agentfrom .middleware.session_id_middleware import SESSION_ID_HEADER, SessionIdMiddlewarefrom .session import get_session_manager
logging.basicConfig(level=logging.INFO)
@asynccontextmanagerasync def lifespan(app: FastAPI): with get_agent() as agent: app.state.agui_agent = StrandsAgent( agent=agent, name="StoryAgent", description="A Strands Agent exposed via the AG-UI protocol.", # A per-thread session manager, not the template Agent's own, since # AG-UI caches one Strands agent per thread_id. config=StrandsAgentConfig(session_manager_provider=lambda _input_data: get_session_manager()), # Required as well as on the template Agent: AG-UI keeps only the # built HookRegistry, so hooks it can't read back are never # registered and model/tool failures go unreported. hooks=AGENT_HOOKS, ) yield
app = FastAPI(title="AWS Strands - StoryAgent", lifespan=lifespan)app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"],)app.add_middleware(SessionIdMiddleware)
@app.post("/invocations")async def invocations(request: Request): # Validate the body manually since AgentCore may omit Content-Type. encoder = EventEncoder(accept=request.headers.get("accept") or "") raw = await request.body() try: input_data = RunAgentInput.model_validate_json(raw) except Exception as exc: message = f"Invalid RunAgentInput: {str(exc)[:200]}"
async def _bad(): yield encoder.encode(RunErrorEvent(type=EventType.RUN_ERROR, message=message, code="BAD_REQUEST"))
return StreamingResponse(_bad(), media_type=encoder.get_content_type())
session_id = request.headers.get(SESSION_ID_HEADER) or get_current_session_id()
async def event_generator(): # Re-bind the session: the streaming body runs outside the middleware. with session_id_context(session_id or str(uuid.uuid4())): async for event in request.app.state.agui_agent.run(input_data): try: yield encoder.encode(event) except Exception as e: error_event = RunErrorEvent( type=EventType.RUN_ERROR, message=f"Encoding error: {e}", code="ENCODING_ERROR", ) yield encoder.encode(error_event) break
return StreamingResponse(event_generator(), media_type=encoder.get_content_type())
@app.get("/ping")async def ping(): return {"status": "healthy"}Este es el punto de entrada para el agente. Dado que seleccionamos --protocol=ag-ui, el generador envuelve nuestro Agent de Strands con StrandsAgent de ag_ui_strands y lo monta en una aplicación FastAPI que habla el protocolo AG-UI — esto es lo que CopilotKit usará para comunicarse desde el sitio web React. El agente se construye dentro de un manejador lifespan — no en el momento de la importación — por lo que el inicio del contenedor, no la importación del módulo, controla la construcción, y cada sesión de AgentCore obtiene su propio contenedor. El SessionIdMiddleware que vimos arriba reenvía el ID de sesión del runtime de AgentCore entrante para que cualquier cliente MCP/A2A que conectemos más adelante (por ejemplo, el servidor MCP de inventario en el Módulo 2) lo reenvíe automáticamente en sus llamadas salientes. Dado que AG-UI almacena en caché un agente Strands por thread_id, conectamos un session_manager_provider en lugar del propio gestor de sesiones del agente plantilla — esto le da a cada hilo su propio SessionManager para que el historial de conversación persista entre turnos. Esa función get_session_manager() proviene de un session.py generado adyacente: cuando está desplegado, devuelve un strands.session.S3SessionManager respaldado por un bucket S3 que el generador aprovisiona automáticamente; bajo agent-dev (LOCAL_DEV=true) siempre devuelve un FileSessionManager que escribe en un directorio temporal local, independientemente de la configuración desplegada.
import { Fn, Lazy, Names, RemovalPolicy, Stack } from 'aws-cdk-lib';import { Platform } from 'aws-cdk-lib/aws-ecr-assets';import { Connections, IConnectable } from 'aws-cdk-lib/aws-ec2';import { BlockPublicAccess, Bucket, BucketEncryption,} from 'aws-cdk-lib/aws-s3';import { Key } from 'aws-cdk-lib/aws-kms';import { CfnDelivery, CfnDeliveryDestination, CfnDeliverySource, LogGroup, RetentionDays,} from 'aws-cdk-lib/aws-logs';import { Construct } from 'constructs';import * as path from 'path';import * as url from 'url';import { AgentRuntimeArtifact, ProtocolType, Runtime, RuntimeProps, RuntimeAuthorizerConfiguration,} from 'aws-cdk-lib/aws-bedrockagentcore';import { PolicyStatement, Effect, ServicePrincipal, IGrantable, IPrincipal,} from 'aws-cdk-lib/aws-iam';import { IUserPool, IUserPoolClient } from 'aws-cdk-lib/aws-cognito';import { suppressRules } from '../../../core/checkov.js';import { RuntimeConfig } from '../../../core/runtime-config.js';import { findWorkspaceRoot } from '../../../core/workspace.js';
export type StoryAgentProps = Omit< RuntimeProps, | 'runtimeName' | 'protocolConfiguration' | 'agentRuntimeArtifact' | 'authorizerConfiguration'> & { /** * Identity details for Cognito Authentication */ identity: { userPool: IUserPool; userPoolClient: IUserPoolClient; }; /** * Removal policy for the session bucket holding the agent's conversation * history. Defaults to retaining it so a stack `destroy` doesn't silently * delete session data — set to `RemovalPolicy.DESTROY` for sandbox/CI teardown. * * @default RemovalPolicy.RETAIN */ readonly sessionBucketRemovalPolicy?: RemovalPolicy;};
export class StoryAgent extends Construct implements IGrantable, IConnectable { public readonly dockerImage: AgentRuntimeArtifact; public readonly agentCoreRuntime: Runtime; /** Default Gateway target name for this agent. */ public readonly agentName = 'story-agent'; /** Inbound auth — a fronting Gateway uses this to pick its outbound credential. */ public readonly auth = 'cognito';
constructor(scope: Construct, id: string, props: StoryAgentProps) { super(scope, id);
const rc = RuntimeConfig.ensure(this);
// Resolve the bundle output directory containing the Dockerfile and built artifacts const bundleDir = path.join( findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))), 'dist/packages/story/docker/story-agent', );
this.dockerImage = AgentRuntimeArtifact.fromAsset(bundleDir, { platform: Platform.LINUX_ARM64, });
const { identity, sessionBucketRemovalPolicy = RemovalPolicy.RETAIN, ...restProps } = props ?? {};
const sessionKey = new Key(this, 'SessionKey', { enableKeyRotation: true, });
// Allow CloudWatch Logs to use the session key for server access log delivery. const stack = Stack.of(this); sessionKey.addToResourcePolicy( new PolicyStatement({ effect: Effect.ALLOW, principals: [ new ServicePrincipal(`logs.${stack.region}.amazonaws.com`), ], actions: [ 'kms:Encrypt', 'kms:Decrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey', ], resources: ['*'], conditions: { ArnLike: { 'kms:EncryptionContext:aws:logs:arn': `arn:aws:logs:${stack.region}:${stack.account}:log-group:*`, }, }, }), );
const sessionAccessLogs = new LogGroup(this, 'SessionAccessLogs', { retention: RetentionDays.ONE_YEAR, encryptionKey: sessionKey, removalPolicy: RemovalPolicy.DESTROY, });
const sessionBucket = new Bucket(this, 'SessionBucket', { enforceSSL: true, removalPolicy: sessionBucketRemovalPolicy, encryption: BucketEncryption.KMS, encryptionKey: sessionKey, blockPublicAccess: BlockPublicAccess.BLOCK_ALL, }); suppressRules( sessionBucket, ['CKV_AWS_21'], 'Session data does not need versioning enabled', ); suppressRules( sessionBucket, ['CKV2_AWS_61'], 'Lifecycle configuration not required for session data', ); suppressRules( sessionBucket, ['CKV_AWS_144'], 'Cross-region replication not required for session data', ); suppressRules( sessionBucket, ['CKV2_AWS_62'], 'Event notifications not required for session data', ); suppressRules( sessionBucket, ['CKV_AWS_18'], 'Server access logs are delivered to CloudWatch Logs', );
const sessionAccessLogsSource: CfnDeliverySource = new CfnDeliverySource( this, 'SessionAccessLogsSource', { name: Lazy.string({ produce: () => Names.uniqueResourceName(sessionAccessLogsSource, { maxLength: 60, }), }), logType: 'S3_SERVER_ACCESS_LOGS', resourceArn: sessionBucket.bucketArn, }, ); const sessionBucketPolicy = sessionBucket.policy; if (sessionBucketPolicy) { sessionAccessLogsSource.node.addDependency(sessionBucketPolicy); } const sessionAccessLogsDestination: CfnDeliveryDestination = new CfnDeliveryDestination(this, 'SessionAccessLogsDestination', { name: Lazy.string({ produce: () => Names.uniqueResourceName(sessionAccessLogsDestination, { maxLength: 60, }), }), destinationResourceArn: sessionAccessLogs.logGroupArn, }); const sessionAccessLogsDelivery = new CfnDelivery( this, 'SessionAccessLogsDelivery', { deliverySourceName: sessionAccessLogsSource.name, deliveryDestinationArn: sessionAccessLogsDestination.attrArn, }, ); sessionAccessLogsDelivery.addDependency(sessionAccessLogsSource);
this.agentCoreRuntime = new Runtime(this, 'StoryAgent', { runtimeName: Lazy.string({ produce: () => Names.uniqueResourceName(this.agentCoreRuntime, { maxLength: 40 }), }), protocolConfiguration: ProtocolType.HTTP, agentRuntimeArtifact: this.dockerImage, authorizerConfiguration: RuntimeAuthorizerConfiguration.usingCognito( identity.userPool, [identity.userPoolClient], ), // Receive the caller's Authorization header (validated by the authorizer). requestHeaderConfiguration: { allowlistedHeaders: ['Authorization'], }, ...restProps, environmentVariables: { RUNTIME_CONFIG_APP_ID: rc.appConfigApplicationId, ...restProps?.environmentVariables, }, });
// Grant access for the agent to invoke bedrock models this.agentCoreRuntime.addToRolePolicy( new PolicyStatement({ actions: [ 'bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream', ], resources: [ 'arn:aws:bedrock:*:*:foundation-model/*', 'arn:aws:bedrock:*:*:inference-profile/*', ], }), );
sessionBucket.grantReadWrite(this.agentCoreRuntime);
rc.grantReadAppConfig(this.agentCoreRuntime);
rc.set('agentcore', 'agentRuntimes', { ...rc.get('agentcore').agentRuntimes, StoryAgent: { arn: this.agentCoreRuntime.agentRuntimeArn, session: { bucketName: sessionBucket.bucketName, }, }, });
rc.set('connection', 'agentRuntimes', { ...rc.get('connection').agentRuntimes, StoryAgent: this.agentCoreRuntime.agentRuntimeArn, }); }
/** * The principal to grant permissions to. */ public get grantPrincipal(): IPrincipal { return this.agentCoreRuntime.grantPrincipal; }
/** * Network connections for this agent runtime. */ public get connections(): Connections { return this.agentCoreRuntime.connections; }
/** * The HTTPS invocation URL of the runtime. */ public get invocationUrl(): string { // The URL must URL-encode the runtime ARN (':' -> '%3A', '/' -> '%2F'). // The ARN is a CDK token, so encode at deploy time via Fn.join/Fn.split. const encodedArn = Fn.join( '%2F', Fn.split( '/', Fn.join('%3A', Fn.split(':', this.agentCoreRuntime.agentRuntimeArn)), ), ); return `https://bedrock-agentcore.${Stack.of(this).region}.amazonaws.com/runtimes/${encodedArn}/invocations?qualifier=DEFAULT`; }}Esto configura un AgentRuntimeArtifact de CDK que sube tu imagen Docker del agente a ECR, y la aloja usando AgentCore Runtime. Debido a que elegimos --auth=cognito, el construct requiere la identidad del user pool/client y autoriza las invocaciones de AgentCore Runtime a través de Cognito, reenviando el encabezado Authorization del llamador. También aprovisiona el bucket de sesión que el session.py del Story Agent lee de vuelta en tiempo de ejecución — un bucket S3 cifrado con KMS con registros de acceso del servidor entregados a CloudWatch Logs — otorga al agente acceso de lectura/escritura a él, le otorga acceso para invocar modelos de Bedrock, y registra su ARN y nombre de bucket en RuntimeConfig para que tanto el agente (en tiempo de ejecución, a través de AppConfig) como la Game API (en tiempo de synth, a través de invocationUrl) puedan encontrarlo.
Puedes notar un Dockerfile adicional, que referencia la imagen Docker del proyecto story, permitiéndonos ubicar conjuntamente el Dockerfile y el código fuente del agente.
Configurar las herramientas de inventario
Sección titulada «Configurar las herramientas de inventario»Inventario: proyecto TypeScript
Sección titulada «Inventario: proyecto TypeScript»Creemos un servidor MCP para proporcionar herramientas a nuestro Story Agent para gestionar el inventario de un jugador.
Primero, creamos un proyecto TypeScript:
pnpm nx g @aws/nx-plugin:ts#project --name=inventory --no-interactiveyarn nx g @aws/nx-plugin:ts#project --name=inventory --no-interactivenpx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactivebunx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#project - Complete los parámetros requeridos
- name: inventory
- Haga clic en
Generate
Esto creará un proyecto TypeScript vacío.
Examinar los archivos generados por ts#project en detalle
El generador ts#project genera estos archivos.
Directoriopackages/
Directorioinventory/
Directoriosrc/
- index.ts punto de entrada con función de ejemplo
- project.json configuración del proyecto
- vitest.config.mts configuración de pruebas
- tsconfig.json configuración base de TypeScript para el proyecto
- tsconfig.lib.json configuración de TypeScript para el proyecto orientada a compilación y empaquetado
- tsconfig.spec.json configuración de TypeScript para pruebas
- tsconfig.base.json actualizado para configurar un alias para que otros proyectos hagan referencia a este
Inventario: servidor MCP
Sección titulada «Inventario: servidor MCP»A continuación, agregaremos un servidor MCP a nuestro proyecto TypeScript:
pnpm nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactiveyarn nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactivenpx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactivebunx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#mcp-server - Complete los parámetros requeridos
- project: inventory
- Haga clic en
Generate
Esto agregará un servidor MCP.
Examinar los archivos generados por ts#mcp-server en detalle
El generador ts#mcp-server genera estos archivos.
Directoriopackages/
Directorioinventory/
Directoriosrc/mcp-server/
- index.ts exportación barrel
- server.ts crea el servidor MCP
Directoriotools/
- divide.ts herramienta de ejemplo
Directorioresources/
- sample-guidance.ts recurso de ejemplo
- stdio.ts punto de entrada para MCP con transporte STDIO
- http.ts punto de entrada para MCP con transporte HTTP Streamable
- Dockerfile construye la imagen para AgentCore Runtime
- rolldown.config.ts configuración para empaquetar el servidor MCP para despliegue en AgentCore
Directoriocommon/constructs/
Directoriosrc
Directorioapp/mcp-servers/inventory-mcp-server/
- inventory-mcp-server.ts construct para desplegar tu servidor MCP de inventario en AgentCore Runtime
Crear la base de datos del juego
Sección titulada «Crear la base de datos del juego»Nuestro estado de juego — partidas guardadas e inventario de cada jugador — vive en Amazon DynamoDB. Crea un proyecto DynamoDB llamado DungeonDb con el generador ts#dynamodb:
pnpm nx g @aws/nx-plugin:ts#dynamodb --name=DungeonDb --no-interactiveyarn nx g @aws/nx-plugin:ts#dynamodb --name=DungeonDb --no-interactivenpx nx g @aws/nx-plugin:ts#dynamodb --name=DungeonDb --no-interactivebunx nx g @aws/nx-plugin:ts#dynamodb --name=DungeonDb --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#dynamodb --name=DungeonDb --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#dynamodb --name=DungeonDb --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#dynamodb --name=DungeonDb --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#dynamodb --name=DungeonDb --no-interactive --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#dynamodb - Complete los parámetros requeridos
- name: DungeonDb
- Haga clic en
Generate
Verás algunos archivos nuevos aparecer en tu árbol de archivos.
Examinar los archivos generados por ts#dynamodb en detalle
El generador ts#dynamodb genera estos archivos.
Directoriopackages/
Directoriodungeon-db/
- config.json configuración de DynamoDB incluyendo puerto, nombre de tabla, configuración del contenedor e Índices Secundarios Globales
Directoriosrc/
- index.ts punto de entrada y exportaciones
- client.ts singleton del cliente DynamoDB y resolución del nombre de tabla
Directorioentities/
- example.ts entidad ElectroDB de ejemplo (la reemplazaremos)
- index.ts exportaciones de entidades
- project.json agrega los objetivos
devypull-image
Directoriocommon/
Directorioscripts/
Directoriosrc/
Directoriodynamodb/
- create-local-table.ts crea la tabla en DynamoDB Local
- pull-image.ts descarga la imagen de DynamoDB Local
- start-container.ts inicia el contenedor de DynamoDB Local
Directorioconstructs/
Directoriosrc/
Directorioapp/dynamodb/
- dungeon-db.ts construct para aprovisionar tu tabla
Directoriocore/
- dynamodb.ts construct genérico de tabla DynamoDB
El src/client.ts generado exporta getDynamoDBClient() y resolveTableName(). Cuando LOCAL_DEV=true (establecido automáticamente por los objetivos dev) estos se conectan a DynamoDB Local; de lo contrario, se conectan a AWS y resuelven el nombre de tabla desplegado desde la Configuración de Runtime. Modelaremos nuestras entidades Game e Inventory en este proyecto en el Módulo 2.
Para más detalles, consulta la guía del generador ts#dynamodb.
Crear la Interfaz de Usuario (UI)
Sección titulada «Crear la Interfaz de Usuario (UI)»A continuación, crearemos la UI que te permitirá interactuar con el juego.
Game UI: Sitio web
Sección titulada «Game UI: Sitio web»Para crear la UI, crea un sitio web llamado GameUI siguiendo estos pasos:
pnpm nx g @aws/nx-plugin:ts#website --name=GameUI --ux=shadcn --no-interactiveyarn nx g @aws/nx-plugin:ts#website --name=GameUI --ux=shadcn --no-interactivenpx nx g @aws/nx-plugin:ts#website --name=GameUI --ux=shadcn --no-interactivebunx nx g @aws/nx-plugin:ts#website --name=GameUI --ux=shadcn --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#website --name=GameUI --ux=shadcn --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#website --name=GameUI --ux=shadcn --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#website --name=GameUI --ux=shadcn --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#website --name=GameUI --ux=shadcn --no-interactive --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#website - Complete los parámetros requeridos
- name: GameUI
- ux: shadcn
- Haga clic en
Generate
Verás algunos archivos nuevos aparecer en tu árbol de archivos.
Examinar los archivos generados por ts#website en detalle
El ts#website genera estos archivos. Examinemos algunos de los archivos clave resaltados en el árbol de archivos:
Directoriopackages/
Directoriocommon/
Directorioconstructs/
Directoriosrc/
Directorioapp/ constructs cdk específicos de la aplicación
Directoriostatic-websites/
- game-ui.ts construct cdk para crear tu Game UI
Directoriocore/
- static-website.ts construct genérico de sitio web estático
Directoriogame-ui/
Directoriopublic/
- …
Directoriosrc/
Directoriocomponents/
DirectorioAppLayout/
- index.tsx diseño general de página usando shadcn
SidebarProvider+ encabezado
- index.tsx diseño general de página usando shadcn
- app-sidebar.tsx barra lateral shadcn predeterminada con elementos de navegación
- alert.tsx, spinner.tsx primitivas de retroalimentación envueltas en shadcn
Directorioroutes/ rutas basadas en archivos de @tanstack/react-router
- index.tsx página raíz ’/’
- __root.tsx todas las páginas usan este componente como base
- config.ts
- main.tsx punto de entrada de React
- routeTree.gen.ts esto se actualiza automáticamente por @tanstack/react-router
- styles.css importa los globales compartidos de shadcn (Tailwind v4)
- index.html
- project.json
- vite.config.mts
- …
Directoriocommon/
Directorioshadcn/ biblioteca shadcn/ui compartida (tokens de tema,
Button,Card,Input,Sidebar, …) importada por cada sitio web conux=shadcn- src/components/ui/*
- src/styles/globals.css tokens de diseño de Tailwind + shadcn
- …
import * as url from 'url';import { Construct } from 'constructs';import { StaticWebsite, StaticWebsiteProps } from '../../core/index.js';
export type GameUIProps = Omit< StaticWebsiteProps, 'websiteName' | 'websiteFilePath'>;
export class GameUI extends StaticWebsite { constructor(scope: Construct, id: string, props?: GameUIProps) { super(scope, id, { ...props, websiteName: 'GameUI', websiteFilePath: url.fileURLToPath( new URL( '../../../../../../dist/packages/game-ui/bundle', import.meta.url, ), ), }); }}Este es el construct CDK que define nuestro GameUI. Ya ha configurado la ruta al bundle generado para nuestra UI basada en Vite. Esto significa que en el momento de build, el empaquetado ocurre dentro del target de build del proyecto game-ui y la salida se usa aquí.
import React from 'react';import { createRoot } from 'react-dom/client';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';import './styles.css';
export type RouterProviderContext = {};
const router = createRouter({ routeTree, context: {} });
declare module '@tanstack/react-router' { interface Register { router: typeof router; }}
const App = () => <RouterProvider router={router} context={{}} />;
const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <App /> </React.StrictMode>, );Este es el punto de entrada donde se monta React. El estilo proviene de tokens de Tailwind v4 importados a través de styles.css. @tanstack/react-router está configurado en modo de enrutamiento basado en archivos: mientras el servidor de desarrollo esté ejecutándose, cualquier archivo que crees bajo routes/ se recoge automáticamente y el árbol de rutas se regenera. Los generadores posteriores (auth, connection) parchearán este archivo mediante AST para envolver <App /> en proveedores adicionales.
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/')({ component: RouteComponent,});
function RouteComponent() { return ( <div className="text-center"> <header> <h1>Welcome</h1> <p>Welcome to your new React website!</p> </header> </div> );}Un componente se renderizará al navegar a la ruta /. @tanstack/react-router gestionará la Route por ti cuando crees/muevas este archivo (siempre que el servidor de desarrollo esté ejecutándose).
Game UI: Autenticación
Sección titulada «Game UI: Autenticación»Configuremos nuestra Game UI para requerir acceso autenticado a través de Amazon Cognito siguiendo estos pasos:
pnpm nx g @aws/nx-plugin:ts#website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactiveyarn nx g @aws/nx-plugin:ts#website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactivenpx nx g @aws/nx-plugin:ts#website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactivebunx nx g @aws/nx-plugin:ts#website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#website#auth - Complete los parámetros requeridos
- cognitoDomain: game-ui
- project: @dungeon-adventure/game-ui
- allowSignup: true
- Haga clic en
Generate
Verás algunos archivos nuevos aparecer/cambiar en tu árbol de archivos.
Examinar los archivos generados por ts#website#auth en detalle
El generador ts#website#auth actualiza/genera estos archivos. Examinemos algunos de los archivos clave resaltados en el árbol de archivos:
Directoriopackages/
Directoriocommon/
Directorioconstructs/
Directoriosrc/
Directoriocore/
- user-identity.ts construct cdk para crear grupos de usuarios/identidad
Directoriogame-ui/
Directoriosrc/
Directoriocomponents/
DirectorioAppLayout/
- index.tsx agrega el usuario conectado/cierre de sesión al encabezado
DirectorioCognitoAuth/
- index.tsx gestiona el inicio de sesión en Cognito
DirectorioRuntimeConfig/
- index.tsx obtiene el
runtime-config.jsony lo proporciona a los hijos a través del contexto
- index.tsx obtiene el
Directoriohooks/
- useRuntimeConfig.tsx
- main.tsx Actualizado para agregar Cognito
import { useAuth } from 'react-oidc-context';import CognitoAuth from './components/CognitoAuth';import { useRuntimeConfig } from './hooks/useRuntimeConfig';import RuntimeConfigProvider from './components/RuntimeConfig';import React from 'react';import { createRoot } from 'react-dom/client';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';import './styles.css';export type RouterProviderContext = {};export type RouterProviderContext = { runtimeConfig?: ReturnType<typeof useRuntimeConfig>; auth?: ReturnType<typeof useAuth>;};const router = createRouter({ routeTree, context: {} });const router = createRouter({ routeTree, context: { runtimeConfig: undefined, auth: undefined },});// Register the router instance for type safetydeclare module '@tanstack/react-router' { interface Register { router: typeof router; }}const App = () => <RouterProvider router={router} context={{}} />;const App = () => { const auth = useAuth(); const runtimeConfig = useRuntimeConfig(); return <RouterProvider router={router} context={{ runtimeConfig, auth }} />;};const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <RuntimeConfigProvider> <CognitoAuth> <App /> </CognitoAuth> </RuntimeConfigProvider> </React.StrictMode>, );Los componentes RuntimeConfigProvider y CognitoAuth se han agregado al archivo main.tsx mediante una transformación AST. Esto permite que el componente CognitoAuth se autentique con Amazon Cognito obteniendo el runtime-config.json que contiene la configuración de conexión de Cognito requerida para realizar las llamadas al backend al destino correcto.
Game UI: Conectar a la Game API
Sección titulada «Game UI: Conectar a la Game API»Configuremos nuestra Game UI para conectarse a nuestra Game API creada anteriormente.
pnpm nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactiveyarn nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactivenpx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactivebunx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runyarn nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runnpx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runbunx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - connection - Complete los parámetros requeridos
- sourceProject: @dungeon-adventure/game-ui
- targetProject: @dungeon-adventure/game-api
- Haga clic en
Generate
Verás algunos archivos nuevos aparecer/cambiar en tu árbol de archivos.
Examinar los archivos de conexión UI → tRPC
El generador connection genera/actualiza estos archivos. Examinemos algunos de los archivos clave resaltados en el árbol de archivos:
Directoriopackages/
Directoriogame-ui/
Directoriosrc/
Directoriocomponents/
- GameApiClientProvider.tsx configura el cliente de GameAPI
Directoriohooks/
- useGameApi.tsx hooks para llamar a la GameApi
- main.tsx inyecta los proveedores del cliente trpc
- package.json
import { useContext } from 'react';import { GameApiTRPCContext } from '../components/GameApiClientProvider';
export const useGameApi = () => { const container = useContext(GameApiTRPCContext); if (!container) { throw new Error('useGameApi must be used within GameApiClientProvider'); } return container.optionsProxy;};
export const useGameApiClient = () => { const container = useContext(GameApiTRPCContext); if (!container) { throw new Error( 'useGameApiClient must be used within GameApiClientProvider', ); } return container.client;};Este hook proporciona acceso al cliente tRPC para llamar a la GameApi. Para ejemplos sobre cómo llamar a APIs tRPC, consulta la guía de uso del hook tRPC.
import GameApiClientProvider from './components/GameApiClientProvider';import QueryClientProvider from './components/QueryClientProvider';import { useAuth } from 'react-oidc-context';import CognitoAuth from './components/CognitoAuth';import { useRuntimeConfig } from './hooks/useRuntimeConfig';import RuntimeConfigProvider from './components/RuntimeConfig';import React from 'react';import { createRoot } from 'react-dom/client';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';import './styles.css';...const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <RuntimeConfigProvider> <CognitoAuth> <QueryClientProvider> <GameApiClientProvider> <App /> </GameApiClientProvider> </QueryClientProvider> </CognitoAuth> </RuntimeConfigProvider> </React.StrictMode>, );El archivo main.tsx ha sido actualizado mediante una transformación AST para inyectar los proveedores de tRPC.
Story Agent: Conectar al servidor MCP de inventario
Sección titulada «Story Agent: Conectar al servidor MCP de inventario»Conectemos nuestro Story Agent al servidor MCP de inventario para que el agente pueda descubrir e invocar las herramientas del servidor MCP.
pnpm nx g @aws/nx-plugin:connection --sourceProject=story --targetProject=inventory --no-interactiveyarn nx g @aws/nx-plugin:connection --sourceProject=story --targetProject=inventory --no-interactivenpx nx g @aws/nx-plugin:connection --sourceProject=story --targetProject=inventory --no-interactivebunx nx g @aws/nx-plugin:connection --sourceProject=story --targetProject=inventory --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:connection --sourceProject=story --targetProject=inventory --no-interactive --dry-runyarn nx g @aws/nx-plugin:connection --sourceProject=story --targetProject=inventory --no-interactive --dry-runnpx nx g @aws/nx-plugin:connection --sourceProject=story --targetProject=inventory --no-interactive --dry-runbunx nx g @aws/nx-plugin:connection --sourceProject=story --targetProject=inventory --no-interactive --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - connection - Complete los parámetros requeridos
- sourceProject: story
- targetProject: inventory
- Haga clic en
Generate
Examinar los archivos de conexión Story Agent → Inventory MCP
El generador connection genera/actualiza estos archivos:
Directoriopackages/
Directoriocommon/
Directorioagent_connection/
Directoriodungeon_adventure_agent_connection/
Directoriocore/
- agentcore_endpoints.py Resolución de ARN/URL independiente del framework
- agentcore_mcp_transport.py Transporte MCP independiente del framework
- agentcore_mcp_client_strands.py Cliente MCP de Strands que envuelve el transporte
Directorioauth/ SigV4 / reenvío de sesión
httpx.Authindependiente del framework- …
Directorioapp/
- inventory_mcp_server_client_strands.py Cliente Strands para conectarse al servidor MCP de inventario
- __init__.py Re-exporta clientes por conexión
Directoriostory/
Directoriodungeon_adventure_story/agent/
- agent.py Modificado para importar y usar el cliente MCP
El generador:
- Crea un proyecto Python
agent_connectioncompartido (si no existe ya) con el núcleoAgentCoreMCPClientStrands - Genera una clase
InventoryMcpServerClientStrandsque maneja la conexión al servidor MCP tanto localmente (HTTP directo) como cuando está desplegado (a través de AgentCore con autenticación IAM) - Transforma
agent.pypara importar el cliente, crear una instancia y conectar las herramientas del servidor MCP al agente - Agrega el proyecto
agent_connectioncomo dependencia del espacio de trabajo del proyecto story - Actualiza el objetivo
devpara iniciar automáticamente el servidor MCP al ejecutar localmente
Para más detalles, consulta la guía de conexión de agente Python a MCP.
Game UI: Conectar al Story Agent
Sección titulada «Game UI: Conectar al Story Agent»Conectemos nuestra Game UI al Story Agent. Dado que el agente habla AG-UI, el generador connection conecta CopilotKit: un componente de chat con tema y un HttpAgent de @ag-ui/client listo para renderizar.
pnpm nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=story --no-interactiveyarn nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=story --no-interactivenpx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=story --no-interactivebunx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=story --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=story --no-interactive --dry-runyarn nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=story --no-interactive --dry-runnpx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=story --no-interactive --dry-runbunx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-ui --targetProject=story --no-interactive --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - connection - Complete los parámetros requeridos
- sourceProject: @dungeon-adventure/game-ui
- targetProject: story
- Haga clic en
Generate
Examinar los archivos de conexión UI → Story Agent
El generador connection genera/actualiza estos archivos:
Directoriopackages/
Directoriogame-ui/
Directoriosrc/
Directoriocomponents/
- AguiProvider.tsx
CopilotKitProvidercon cada agente AG-UI conectado registrado Directoriocopilot/
- index.tsx
CopilotChat/CopilotSidebar/CopilotPopupcon tema Shadcn - ShadcnAssistantMessage.tsx, ShadcnUserMessage.tsx, ShadcnChatInput.tsx, ShadcnCursor.tsx, copilot.css
- index.tsx
- AguiProvider.tsx
Directoriohooks/
- useAguiStoryAgent.tsx Construye un
HttpAgent, inyecta el token bearer de Cognito y rellenathreadIdal ID de sesión de 33 caracteres de AgentCore
- useAguiStoryAgent.tsx Construye un
- main.tsx Envuelve
<App />en<AguiProvider>
El generador:
- Detecta el
uxdel sitio web React (Shadcn aquí) y vende componentes de chat coincidentes. - Registra cada agente conectado en un único
CopilotKitProvider— volver a ejecutar para otro agente simplemente agrega otro hook. - Lee el ARN de runtime del agente desde la Configuración de Runtime, construye la URL de invocación de AgentCore y adjunta el token bearer de Cognito más el encabezado de ID de sesión de AgentCore.
Para más detalles, consulta la guía de conexión React a AG-UI.
Conectar la Game API y el servidor MCP de inventario a la base de datos
Sección titulada «Conectar la Game API y el servidor MCP de inventario a la base de datos»Tanto la Game API como el servidor MCP de inventario leen y escriben en nuestra tabla DynamoDB, así que conectémoslos al proyecto DungeonDb. El generador connection detecta que el objetivo es un proyecto ts#dynamodb y conecta el objetivo dev de cada proyecto fuente para iniciar DynamoDB Local automáticamente.
pnpm nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-api --targetProject=@dungeon-adventure/dungeon-db --no-interactiveyarn nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-api --targetProject=@dungeon-adventure/dungeon-db --no-interactivenpx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-api --targetProject=@dungeon-adventure/dungeon-db --no-interactivebunx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-api --targetProject=@dungeon-adventure/dungeon-db --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-api --targetProject=@dungeon-adventure/dungeon-db --no-interactive --dry-runyarn nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-api --targetProject=@dungeon-adventure/dungeon-db --no-interactive --dry-runnpx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-api --targetProject=@dungeon-adventure/dungeon-db --no-interactive --dry-runbunx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/game-api --targetProject=@dungeon-adventure/dungeon-db --no-interactive --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - connection - Complete los parámetros requeridos
- sourceProject: @dungeon-adventure/game-api
- targetProject: @dungeon-adventure/dungeon-db
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/inventory --targetProject=@dungeon-adventure/dungeon-db --no-interactiveyarn nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/inventory --targetProject=@dungeon-adventure/dungeon-db --no-interactivenpx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/inventory --targetProject=@dungeon-adventure/dungeon-db --no-interactivebunx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/inventory --targetProject=@dungeon-adventure/dungeon-db --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/inventory --targetProject=@dungeon-adventure/dungeon-db --no-interactive --dry-runyarn nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/inventory --targetProject=@dungeon-adventure/dungeon-db --no-interactive --dry-runnpx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/inventory --targetProject=@dungeon-adventure/dungeon-db --no-interactive --dry-runbunx nx g @aws/nx-plugin:connection --sourceProject=@dungeon-adventure/inventory --targetProject=@dungeon-adventure/dungeon-db --no-interactive --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - connection - Complete los parámetros requeridos
- sourceProject: @dungeon-adventure/inventory
- targetProject: @dungeon-adventure/dungeon-db
- Haga clic en
Generate
Game UI: Infraestructura
Sección titulada «Game UI: Infraestructura»Creemos el sub-proyecto final para la infraestructura CDK.
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactiveyarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactivenpx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactivebunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#infra - Complete los parámetros requeridos
- name: infra
- Haga clic en
Generate
Verás algunos archivos nuevos aparecer/cambiar en tu árbol de archivos.
Examinar los archivos generados por ts#infra en detalle
El generador ts#infra genera/actualiza estos. Examinemos algunos de los archivos clave resaltados en el árbol de archivos:
Directoriopackages/
Directoriocommon/
Directorioconstructs/
Directoriosrc/
Directoriocore/
- checkov.ts
- index.ts
Directorioinfra
Directoriosrc/
Directoriostages/
- application-stage.ts stacks cdk definidos aquí
Directoriostacks/
- application-stack.ts recursos cdk definidos aquí
- main.ts punto de entrada que define todas las etapas
- cdk.json
- checkov.yml
- project.json
- …
- package.json
- tsconfig.json agrega referencias
- tsconfig.base.json agrega alias
import { ApplicationStage } from './stages/application-stage.js';import { App } from '@dungeon-adventure/common-constructs';
const app = new App();
// Use this to deploy your own sandbox environment (assumes your CLI credentials)new ApplicationStage(app, 'dungeon-adventure-infra-sandbox', { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, },});
app.synth();Este es el punto de entrada para tu aplicación CDK.
import { Stack, StackProps } from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
// The code that defines your stack goes here }}Instanciemos nuestros constructs CDK para construir nuestro juego de aventuras en mazmorras.
Tarea 3: Actualizar nuestra infraestructura
Sección titulada «Tarea 3: Actualizar nuestra infraestructura»Actualicemos packages/infra/src/stacks/application-stack.ts para instanciar algunos de nuestros constructs generados:
import { GameApi, GameUI, InventoryMcpServer, StoryAgent, UserIdentity,} from '@dungeon-adventure/common-constructs';import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';import { Construct } from 'constructs';export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
const userIdentity = new UserIdentity(this, 'UserIdentity');
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this).build(), });
const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer');
// Use Cognito for user authentication with the agent const storyAgent = new StoryAgent(this, 'StoryAgent', { identity: userIdentity, });
new CfnOutput(this, 'StoryAgentArn', { value: storyAgent.agentCoreRuntime.agentRuntimeArn, }); new CfnOutput(this, 'InventoryMcpArn', { value: mcpServer.agentCoreRuntime.agentRuntimeArn, });
// Grant the agent permissions to invoke our mcp server mcpServer.grantInvokeAccess(storyAgent);
// Grant the authenticated role access to invoke the api gameApi.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
new GameUI(this, 'GameUI'); }}import { Stack, StackProps } from 'aws-cdk-lib';import { GameApi, GameUI, InventoryMcpServer, StoryAgent, UserIdentity,} from '@dungeon-adventure/common-constructs';import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
// The code that defines your stack goes here const userIdentity = new UserIdentity(this, 'UserIdentity');
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this).build(), });
const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer');
// Use Cognito for user authentication with the agent const storyAgent = new StoryAgent(this, 'StoryAgent', { identity: userIdentity, });
new CfnOutput(this, 'StoryAgentArn', { value: storyAgent.agentCoreRuntime.agentRuntimeArn, }); new CfnOutput(this, 'InventoryMcpArn', { value: mcpServer.agentCoreRuntime.agentRuntimeArn, });
// Grant the agent permissions to invoke our mcp server mcpServer.grantInvokeAccess(storyAgent);
// Grant the authenticated role access to invoke the api gameApi.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
new GameUI(this, 'GameUI'); }}Tarea 4: Compilar el código
Sección titulada «Tarea 4: Compilar el código»Comandos de Nx
Objetivos individuales vs múltiples
Sección titulada «Objetivos individuales vs múltiples»El comando run-many ejecutará un objetivo en múltiples sub-proyectos listados (--all los apuntará a todos). Esto asegura que las dependencias se ejecuten en el orden correcto.
También puedes activar una compilación (o cualquier otra tarea) para un objetivo de proyecto único ejecutando el objetivo directamente en el proyecto. Por ejemplo, para compilar el proyecto @dungeon-adventure/infra, ejecuta el siguiente comando:
pnpm nx build infrayarn nx build infranpx nx build infrabunx nx build infraTambién puedes omitir el alcance y usar la sintaxis abreviada de Nx si lo prefieres:
pnpm nx build infrayarn nx build infranpx nx build infrabunx nx build infraVisualizar tus dependencias
Sección titulada «Visualizar tus dependencias»Para visualizar tus dependencias, ejecuta:
pnpm nx graphyarn nx graphnpx nx graphbunx nx graph
Nx se basa en el caché para que puedas reutilizar artefactos de compilaciones anteriores y acelerar el desarrollo. Se requiere algo de configuración para que esto funcione correctamente y puede haber casos en los que quieras realizar una compilación sin usar el caché. Para hacerlo, simplemente agrega el argumento --skip-nx-cache a tu comando. Por ejemplo:
pnpm nx build infra --skip-nx-cacheyarn nx build infra --skip-nx-cachenpx nx build infra --skip-nx-cachebunx nx build infra --skip-nx-cacheSi por alguna razón quisieras limpiar tu caché (almacenado en la carpeta .nx), puedes ejecutar el siguiente comando:
pnpm nx resetyarn nx resetnpx nx resetbunx nx resetUsando la línea de comandos, ejecuta el siguiente comando para corregir primero cualquier problema de lint:
pnpm lintyarn lintnpm run lintbun lintLuego, ejecuta el siguiente comando para una compilación completa:
pnpm buildyarn buildnpm run buildbun buildSe te presentará lo siguiente:
NX The workspace is out of sync
[@nx/js:typescript-sync]: Some TypeScript configuration files are missing project references to the projects they depend on or contain outdated project references.
This will result in an error in CI.
? Would you like to sync the identified changes to get your workspace up to date? …Yes, sync the changes and run the tasksNo, run the tasks without syncing the changesEste mensaje indica que NX ha detectado algunos archivos que pueden actualizarse automáticamente. En este caso, se refiere a los archivos tsconfig.json que no tienen referencias de TypeScript configuradas en los proyectos referenciados.
Selecciona la opción Yes, sync the changes and run the tasks para continuar. ¡Deberías notar que todos los errores de importación relacionados con tu IDE se resuelven automáticamente, ya que el generador de sincronización agregará las referencias de TypeScript faltantes automáticamente!
Todos los artefactos compilados ahora están disponibles dentro de la carpeta dist/ ubicada en la raíz del monorepo. Esta es una práctica estándar cuando se usan proyectos generados por el @aws/nx-plugin, ya que no contamina tu árbol de archivos con archivos generados. En caso de que quieras limpiar tus archivos, elimina la carpeta dist/ sin preocuparte por los artefactos de compilación dispersos por todo el árbol de archivos.
¡Felicitaciones! Has creado todos los sub-proyectos necesarios para comenzar a implementar el núcleo de nuestro juego de aventuras AI en mazmorras. 🎉🎉🎉