Configurar um monorepo
Tarefa 1: Criar um monorepo
Seção intitulada “Tarefa 1: Criar um monorepo”Para criar um novo monorepo, dentro do diretório desejado, execute o seguinte 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=cdkIsso configurará um monorepo NX dentro do diretório dungeon-adventure. Quando você abrir o diretório no VSCode, verá esta estrutura de arquivos:
Directory.nx/
- …
Directory.vscode/
- …
Directorynode_modules/
- …
Directorypackages/ aqui é onde seus sub-projetos residirão
- …
- .gitignore
- biome.json configura o Biome para linting e formatação
- nx.json configura o CLI do Nx e os padrões do monorepo
- package.json todas as dependências node são definidas aqui
- pnpm-lock.yaml ou bun.lock, yarn.lock, package-lock.json dependendo do gerenciador de pacotes
- pnpm-workspace.yaml se estiver usando pnpm
- README.md
- tsconfig.base.json todos os sub-projetos baseados em node estendem este
- tsconfig.json
- aws-nx-plugin.config.mts configuração para o Nx Plugin for AWS
Tarefa 2: Estruturar o Jogo de Aventura em Masmorra
Seção intitulada “Tarefa 2: Estruturar o Jogo de Aventura em Masmorra”Com o workspace configurado, estruturamos os sub-projetos do jogo — a API do Jogo, Agente de História, servidor MCP de Inventário, banco de dados do jogo e website — juntamente com as conexões que os conectam. Existem duas maneiras de fazer isso:
- Rápido — copie os comandos diretamente do diagrama abaixo e execute-os. A maneira mais rápida de chegar ao mesmo ponto de partida.
- Passo a Passo — expanda a seção abaixo para executar cada gerador você mesmo e examinar exatamente o que cada um produz.
O diagrama abaixo é o workspace Dungeon Adventure: cada projeto, componente e conexão que você construirá neste módulo. Clique em Copy commands para pegar toda a série, depois execute-os de dentro do diretório dungeon-adventure que você criou na Tarefa 1.
Passo a Passo
Em vez de copiar os comandos de uma vez, você pode executar cada gerador individualmente. Esta é a melhor maneira de entender o que cada gerador adiciona ao seu workspace. Execute cada gerador por vez, de dentro do diretório dungeon-adventure que você criou na Tarefa 1.
Criar uma API do Jogo
Seção intitulada “Criar uma API do Jogo”Primeiro, vamos criar nossa API do Jogo. Para fazer isso, crie uma API tRPC chamada GameApi usando estas etapas:
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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#api - Preencha os parâmetros obrigatórios
- name: GameApi
- framework: trpc
- Clique em
Generate
Você verá alguns novos arquivos aparecerem na sua árvore de arquivos.
Examine os arquivos gerados pelo ts#api em detalhes
Abaixo está uma lista de todos os arquivos que foram gerados pelo gerador ts#api. Vamos examinar alguns dos arquivos principais destacados na árvore de arquivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ constructs cdk específicos da aplicação
Directoryapis/
- game-api.ts construct cdk para criar sua API tRPC
- index.ts
- …
- index.ts
Directorycore/ constructs cdk genéricos
Directoryapi/
- rest-api.ts construct cdk base para uma API Gateway Rest API
- trpc-utils.ts utilitários para constructs CDK de API trpc
- utils.ts utilitários para constructs de API
- index.ts
- runtime-config.ts
- index.ts
- project.json
- …
Directorygame-api/ API tRPC
Directorysrc/
Directoryclient/ cliente vanilla tipicamente usado para chamadas ts máquina para máquina
- index.ts
Directorymiddleware/ instrumentação powertools
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
Directoryschema/ definições de entradas e saídas para sua API
- index.ts
- echo.ts schema de entrada e saída de exemplo
- z-async-iterable.ts schema Zod wrapper para saída de subscription tRPC
Directoryprocedures/ implementações específicas para seus procedimentos/rotas de API
- echo.ts implementação de procedimento de exemplo
- index.ts
- init.ts configura contexto e middleware
- handler.ts ponto de entrada do handler Lambda (usa response streaming para REST APIs)
- local-server.ts usado ao executar o servidor tRPC localmente
- router.ts define o router tRPC e todos os procedimentos
- project.json
- …
- vitest.workspace.ts
Vamos olhar esses arquivos principais:
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;O router define o router tRPC para sua API e é o lugar onde você declarará todos os seus métodos de API. Como você pode ver acima, temos um método chamado echo com sua implementação no arquivo ./procedures/echo.ts. O ponto de entrada do handler Lambda está em handler.ts, que é configurado automaticamente pelo gerador.
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 arquivo é a implementação do método echo e como você pode ver é fortemente tipado ao declarar suas estruturas de dados de entrada e saída.
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 as definições de schema tRPC são definidas usando Zod e são exportadas como tipos typescript via a sintaxe 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 é o construct CDK que define nossa GameApi. Ele fornece um método defaultIntegrations que cria automaticamente uma função Lambda para cada procedimento em nossa API tRPC, apontando para a implementação da API empacotada. Isso significa que no tempo de cdk synth, o empacotamento não ocorre (ao contrário de usar NodeJsFunction) pois já empacotamos como parte do target de build do projeto backend.
Criar o Agente de História
Seção intitulada “Criar o Agente de História”Agora vamos criar nosso Agente de História.
Agente de história: Projeto Python
Seção intitulada “Agente de história: Projeto Python”Para criar um projeto 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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - py#project - Preencha os parâmetros obrigatórios
- name: story
- Clique em
Generate
Você verá alguns novos arquivos aparecerem na sua árvore de arquivos.
Examine os arquivos gerados pelo py#project em detalhes
O py#project gera estes arquivos:
Directory.venv/ ambiente virtual único para o monorepo
- …
Directorypackages/
Directorystory/
Directorydungeon_adventure_story/ módulo python
- …
Directorytests/
- …
- .python-version
- pyproject.toml
- project.json
- .python-version versão python do uv fixada
- pyproject.toml
- uv.lock
Isso configurou um projeto Python e UV Workspace com ambiente virtual compartilhado.
Story agent
Seção intitulada “Story agent”Para adicionar um agente Strands ao projeto com o gerador 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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - py#agent - Preencha os parâmetros obrigatórios
- project: story
- auth: cognito
- protocol: ag-ui
- Clique em
Generate
Você verá alguns novos arquivos aparecerem na sua árvore de arquivos.
Examine os arquivos gerados pelo py#agent em detalhes
O py#agent gera estes arquivos:
Directorypackages/
Directorystory/
Directorydungeon_adventure_story/ módulo python
Directoryagent/
- main.py ponto de entrada para seu agente no Bedrock AgentCore Runtime
- agent.py define um agente e ferramentas de exemplo
- session.py resolve um SessionManager para persistir o estado da conversa
Directorymiddleware/
- session_id_middleware.py vincula o ID de sessão AgentCore de entrada para a requisição
- Dockerfile define a imagem docker para implantação no AgentCore Runtime
Directorycommon/constructs/
Directorysrc
Directoryapp/agents/story-agent/
- story-agent.ts construct para implantar seu agente Story no AgentCore Runtime
Vamos examinar alguns dos arquivos em detalhes:
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, )Isso cria um agente Strands de exemplo e define uma ferramenta de subtração. log_model_errors e log_tool_errors são hooks do projeto compartilhado dungeon_adventure_agent_connection que registram falhas de modelo/ferramenta em vez de deixá-las falhar 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)O SessionIdMiddleware vincula o ID de sessão do runtime AgentCore de entrada em um ContextVar pela duração da requisição.
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 é o ponto de entrada para o agente. Como selecionamos --protocol=ag-ui, o gerador envolve nosso Agent Strands com StrandsAgent de ag_ui_strands e o monta em um aplicativo FastAPI que fala o protocolo AG-UI — é com isso que o CopilotKit conversará a partir do website React. O agente é construído dentro de um handler lifespan — não no momento da importação — então a inicialização do contêiner, não a importação do módulo, possui a construção, e cada sessão AgentCore obtém seu próprio contêiner. O SessionIdMiddleware que vimos acima encaminha o ID de sessão do runtime AgentCore de entrada para que qualquer cliente MCP/A2A downstream que conectarmos mais tarde (por exemplo, o servidor MCP de Inventário no Módulo 2) o encaminhe automaticamente em suas chamadas de saída. Como AG-UI armazena em cache um agente Strands por thread_id, conectamos um session_manager_provider em vez do gerenciador de sessão do próprio agente de template — isso dá a cada thread seu próprio SessionManager para que o histórico de conversação persista entre turnos. Essa função get_session_manager() vem de um session.py irmão gerado: implantado, ele retorna um strands.session.S3SessionManager apoiado por um bucket S3 que o gerador provisiona automaticamente; sob agent-dev (LOCAL_DEV=true) ele sempre retorna um FileSessionManager escrevendo em um diretório temporário local, independentemente da configuração implantada.
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`; }}Isso configura um AgentRuntimeArtifact CDK que faz upload da imagem Docker do seu agente para o ECR e a hospeda usando AgentCore Runtime. Como escolhemos --auth=cognito, o construct requer a identidade do user pool/client e autoriza invocações do AgentCore Runtime através do Cognito, encaminhando o cabeçalho Authorization do chamador. Ele também provisiona o bucket de sessão que o session.py do Story Agent lê de volta em tempo de execução — um bucket S3 criptografado com KMS com logs de acesso do servidor entregues ao CloudWatch Logs — concede ao agente acesso de leitura/gravação a ele, concede acesso para invocar modelos Bedrock e registra seu ARN e nome do bucket no RuntimeConfig para que tanto o agente (em tempo de execução, via AppConfig) quanto a Game API (em tempo de synth, via invocationUrl) possam encontrá-lo.
Você pode notar um Dockerfile extra que referencia a imagem Docker do projeto story, permitindo co-localizar o Dockerfile e o código-fonte do agente.
Configurar as ferramentas de Inventário
Seção intitulada “Configurar as ferramentas de Inventário”Inventário: Projeto TypeScript
Seção intitulada “Inventário: Projeto TypeScript”Vamos criar um servidor MCP para fornecer ferramentas para nosso Story Agent gerenciar o inventário de um jogador.
Primeiro, criamos um projeto 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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#project - Preencha os parâmetros obrigatórios
- name: inventory
- Clique em
Generate
Isso criará um projeto TypeScript vazio.
Examine os arquivos gerados pelo ts#project em detalhes
O gerador ts#project gera estes arquivos.
Directorypackages/
Directoryinventory/
Directorysrc/
- index.ts ponto de entrada com função de exemplo
- project.json configuração do projeto
- vitest.config.mts configuração de teste
- tsconfig.json configuração typescript base para o projeto
- tsconfig.lib.json configuração typescript para o projeto direcionada para compilação e empacotamento
- tsconfig.spec.json configuração typescript para testes
- tsconfig.base.json atualizado para configurar um alias para outros projetos referenciarem este
Inventário: Servidor MCP
Seção intitulada “Inventário: Servidor MCP”Em seguida, adicionaremos um servidor MCP ao nosso projeto 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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#mcp-server - Preencha os parâmetros obrigatórios
- project: inventory
- Clique em
Generate
Isso adicionará um servidor MCP.
Examine os arquivos gerados pelo ts#mcp-server em detalhes
O gerador ts#mcp-server gera estes arquivos.
Directorypackages/
Directoryinventory/
Directorysrc/mcp-server/
- index.ts exportação barrel
- server.ts cria o servidor MCP
Directorytools/
- divide.ts ferramenta de exemplo
Directoryresources/
- sample-guidance.ts recurso de exemplo
- stdio.ts ponto de entrada para MCP com transporte STDIO
- http.ts ponto de entrada para MCP com transporte HTTP Streamable
- Dockerfile constrói a imagem para AgentCore Runtime
- rolldown.config.ts configuração para empacotar o servidor MCP para implantação no AgentCore
Directorycommon/constructs/
Directorysrc
Directoryapp/mcp-servers/inventory-mcp-server/
- inventory-mcp-server.ts construct para implantar seu servidor MCP de inventário no AgentCore Runtime
Criar o banco de dados do jogo
Seção intitulada “Criar o banco de dados do jogo”O estado do nosso jogo — jogos salvos e o inventário de cada jogador — vive no Amazon DynamoDB. Crie um projeto DynamoDB chamado DungeonDb com o gerador 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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#dynamodb - Preencha os parâmetros obrigatórios
- name: DungeonDb
- Clique em
Generate
Você verá alguns novos arquivos aparecerem na sua árvore de arquivos.
Examine os arquivos gerados pelo ts#dynamodb em detalhes
O gerador ts#dynamodb gera estes arquivos.
Directorypackages/
Directorydungeon-db/
- config.json configuração do DynamoDB incluindo porta, nome da tabela, configurações do container e Global Secondary Indexes
Directorysrc/
- index.ts ponto de entrada e exportações
- client.ts singleton do cliente DynamoDB e resolução do nome da tabela
Directoryentities/
- example.ts entidade ElectroDB de exemplo (vamos substituir isso)
- index.ts exportações de entidades
- project.json adiciona os targets
devepull-image
Directorycommon/
Directoryscripts/
Directorysrc/
Directorydynamodb/
- create-local-table.ts cria a tabela no DynamoDB Local
- pull-image.ts puxa a imagem do DynamoDB Local
- start-container.ts inicia o container do DynamoDB Local
Directoryconstructs/
Directorysrc/
Directoryapp/dynamodb/
- dungeon-db.ts construct para provisionar sua tabela
Directorycore/
- dynamodb.ts construct de tabela DynamoDB genérico
O src/client.ts gerado exporta getDynamoDBClient() e resolveTableName(). Quando LOCAL_DEV=true (definido automaticamente pelos targets dev) estes se conectam ao DynamoDB Local; caso contrário, eles se conectam à AWS e resolvem o nome da tabela implantada a partir da Configuração de Runtime. Vamos modelar nossas entidades Game e Inventory neste projeto no Módulo 2.
Para mais detalhes, consulte o guia do gerador ts#dynamodb.
Criar a Interface do Usuário (UI)
Seção intitulada “Criar a Interface do Usuário (UI)”Em seguida, criaremos a UI que permitirá interagir com o jogo.
Game UI: Website
Seção intitulada “Game UI: Website”Para criar a UI, crie um website chamado GameUI usando estas etapas:
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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#website - Preencha os parâmetros obrigatórios
- name: GameUI
- ux: shadcn
- Clique em
Generate
Você verá alguns novos arquivos aparecerem na sua árvore de arquivos.
Examine os arquivos gerados pelo ts#website em detalhes
O ts#website gera estes arquivos. Vamos examinar alguns dos arquivos principais destacados na árvore de arquivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ constructs cdk específicos da aplicação
Directorystatic-websites/
- game-ui.ts construct cdk para criar sua UI do Jogo
Directorycore/
- static-website.ts construct de website estático genérico
Directorygame-ui/
Directorypublic/
- …
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.tsx layout geral da página usando
SidebarProvider+ cabeçalho shadcn
- index.tsx layout geral da página usando
- app-sidebar.tsx barra lateral shadcn padrão com itens de navegação
- alert.tsx, spinner.tsx primitivos de feedback envolvidos em shadcn
Directoryroutes/ rotas baseadas em arquivo @tanstack/react-router
- index.tsx página raiz ’/’
- __root.tsx todas as páginas usam este componente como base
- config.ts
- main.tsx ponto de entrada React
- routeTree.gen.ts isso é atualizado automaticamente pelo @tanstack/react-router
- styles.css importa globais shadcn compartilhados (Tailwind v4)
- index.html
- project.json
- vite.config.mts
- …
Directorycommon/
Directoryshadcn/ biblioteca shadcn/ui compartilhada (tokens de tema,
Button,Card,Input,Sidebar, …) importada por todo websiteux=shadcn- src/components/ui/*
- src/styles/globals.css tokens de design 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 é o construct CDK que define nossa GameUI. Ele já configurou o caminho do arquivo para o bundle gerado para nossa UI baseada em Vite. Isso significa que no tempo de build, o empacotamento ocorre dentro do target de build do projeto game-ui e a saída é usada aqui.
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 é o ponto de entrada onde o React é montado. O estilo vem dos tokens Tailwind v4 importados via styles.css. @tanstack/react-router está configurado no modo roteamento baseado em arquivo: enquanto o servidor de desenvolvimento estiver rodando, qualquer arquivo que você criar em routes/ é capturado automaticamente e a árvore de rotas é regenerada. Geradores posteriores (auth, connection) farão patch AST neste arquivo para envolver <App /> em provedores adicionais.
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> );}Um componente será renderizado ao navegar para a rota /. @tanstack/react-router gerenciará a Route para você sempre que você criar/mover este arquivo (desde que o servidor de desenvolvimento esteja rodando).
Game UI: Auth
Seção intitulada “Game UI: Auth”Vamos configurar nossa Game UI para exigir acesso autenticado via Amazon Cognito usando estas etapas:
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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#website#auth - Preencha os parâmetros obrigatórios
- cognitoDomain: game-ui
- project: @dungeon-adventure/game-ui
- allowSignup: true
- Clique em
Generate
Você verá alguns novos arquivos aparecerem/mudarem na sua árvore de arquivos.
Examine os arquivos gerados pelo ts#website#auth em detalhes
O gerador ts#website#auth atualiza/gera estes arquivos. Vamos examinar alguns dos arquivos principais destacados na árvore de arquivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- user-identity.ts construct cdk para criar pools de usuário/identidade
Directorygame-ui/
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.tsx adiciona o usuário logado/logout ao cabeçalho
DirectoryCognitoAuth/
- index.tsx gerencia o login no Cognito
DirectoryRuntimeConfig/
- index.tsx busca o
runtime-config.jsone o fornece aos filhos via contexto
- index.tsx busca o
Directoryhooks/
- useRuntimeConfig.tsx
- main.tsx Atualizado para adicionar 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>, );Os componentes RuntimeConfigProvider e CognitoAuth foram adicionados ao arquivo main.tsx via uma transformação AST. Isso permite que o componente CognitoAuth autentique com o Amazon Cognito buscando o runtime-config.json que contém a configuração de conexão cognito necessária para fazer as chamadas backend para o destino correto.
Game UI: Conectar à Game API
Seção intitulada “Game UI: Conectar à Game API”Vamos configurar nossa Game UI para se conectar à nossa Game API criada 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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - connection - Preencha os parâmetros obrigatórios
- sourceProject: @dungeon-adventure/game-ui
- targetProject: @dungeon-adventure/game-api
- Clique em
Generate
Você verá alguns novos arquivos aparecerem/mudarem na sua árvore de arquivos.
Examine os arquivos de conexão UI → tRPC
O gerador connection gera/atualiza estes arquivos. Vamos examinar alguns dos arquivos principais destacados na árvore de arquivos:
Directorypackages/
Directorygame-ui/
Directorysrc/
Directorycomponents/
- GameApiClientProvider.tsx configura o cliente GameAPI
Directoryhooks/
- useGameApi.tsx hooks para chamar a GameApi
- main.tsx injeta os provedores do 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 fornece acesso ao cliente tRPC para chamar a GameApi. Para exemplos de como chamar APIs tRPC, consulte o guia de uso do 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>, );O arquivo main.tsx foi atualizado via uma transformação AST para injetar os provedores tRPC.
Story Agent: Conectar ao Servidor MCP de Inventário
Seção intitulada “Story Agent: Conectar ao Servidor MCP de Inventário”Vamos conectar nosso Story Agent ao servidor MCP de Inventário para que o agente possa descobrir e invocar as ferramentas do 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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - connection - Preencha os parâmetros obrigatórios
- sourceProject: story
- targetProject: inventory
- Clique em
Generate
Examine os arquivos de conexão Agente de História → MCP de Inventário
O gerador connection gera/atualiza estes arquivos:
Directorypackages/
Directorycommon/
Directoryagent_connection/
Directorydungeon_adventure_agent_connection/
Directorycore/
- agentcore_endpoints.py Resolução de ARN/URL independente de framework
- agentcore_mcp_transport.py Transporte MCP independente de framework
- agentcore_mcp_client_strands.py Cliente MCP Strands envolvendo o transporte
Directoryauth/
httpx.AuthSigV4 / encaminhamento de sessão independente de framework- …
Directoryapp/
- inventory_mcp_server_client_strands.py Cliente Strands para conectar ao servidor MCP de Inventário
- __init__.py Re-exporta clientes por conexão
Directorystory/
Directorydungeon_adventure_story/agent/
- agent.py Modificado para importar e usar o cliente MCP
O gerador:
- Cria um projeto Python compartilhado
agent_connection(se ainda não existir) com oAgentCoreMCPClientStrandsprincipal - Gera uma classe
InventoryMcpServerClientStrandsque lida com a conexão ao servidor MCP tanto localmente (HTTP direto) quanto quando implantado (via AgentCore com autenticação IAM) - Transforma
agent.pypara importar o cliente, criar uma instância e conectar as ferramentas do servidor MCP ao agente - Adiciona o projeto
agent_connectioncomo uma dependência de workspace do projeto story - Atualiza o target
devpara iniciar automaticamente o servidor MCP ao executar localmente
Para mais detalhes, consulte o guia de conexão Python Agent para MCP.
Game UI: Conectar ao Story Agent
Seção intitulada “Game UI: Conectar ao Story Agent”Vamos conectar nossa Game UI ao Story Agent. Como o agente fala AG-UI, o gerador connection conecta o CopilotKit: um componente de chat com tema e um HttpAgent do @ag-ui/client pronto 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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - connection - Preencha os parâmetros obrigatórios
- sourceProject: @dungeon-adventure/game-ui
- targetProject: story
- Clique em
Generate
Examine os arquivos de conexão UI → Agente de História
O gerador connection gera/atualiza estes arquivos:
Directorypackages/
Directorygame-ui/
Directorysrc/
Directorycomponents/
- AguiProvider.tsx
CopilotKitProvidercom cada agente AG-UI conectado registrado Directorycopilot/
- index.tsx
CopilotChat/CopilotSidebar/CopilotPopupcom tema Shadcn - ShadcnAssistantMessage.tsx, ShadcnUserMessage.tsx, ShadcnChatInput.tsx, ShadcnCursor.tsx, copilot.css
- index.tsx
- AguiProvider.tsx
Directoryhooks/
- useAguiStoryAgent.tsx Constrói um
HttpAgent, injeta o token bearer Cognito e preenchethreadIdpara o id de sessão de 33 caracteres do AgentCore
- useAguiStoryAgent.tsx Constrói um
- main.tsx Envolve
<App />em<AguiProvider>
O gerador:
- Detecta o
uxdo website React (Shadcn aqui) e fornece componentes de chat correspondentes. - Registra cada agente conectado em um único
CopilotKitProvider— executar novamente para outro agente apenas adiciona outro hook. - Lê o ARN de runtime do agente da Configuração de Runtime, constrói a URL de invocação do AgentCore e anexa o token bearer Cognito mais o cabeçalho de id de sessão do AgentCore.
Para mais detalhes, consulte o guia de conexão React para AG-UI.
Conectar a Game API e o servidor MCP de Inventário ao banco de dados
Seção intitulada “Conectar a Game API e o servidor MCP de Inventário ao banco de dados”Tanto a Game API quanto o servidor MCP de Inventário leem e escrevem em nossa tabela DynamoDB, então vamos conectá-los ao projeto DungeonDb. O gerador connection detecta que o alvo é um projeto ts#dynamodb e conecta o target dev de cada projeto de origem para iniciar o DynamoDB Local automaticamente.
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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - connection - Preencha os parâmetros obrigatórios
- sourceProject: @dungeon-adventure/game-api
- targetProject: @dungeon-adventure/dungeon-db
- Clique em
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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - connection - Preencha os parâmetros obrigatórios
- sourceProject: @dungeon-adventure/inventory
- targetProject: @dungeon-adventure/dungeon-db
- Clique em
Generate
Game UI: Infraestrutura
Seção intitulada “Game UI: Infraestrutura”Vamos criar o sub-projeto final para a infraestrutura 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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#infra - Preencha os parâmetros obrigatórios
- name: infra
- Clique em
Generate
Você verá alguns novos arquivos aparecerem/mudarem na sua árvore de arquivos.
Examine os arquivos gerados pelo ts#infra em detalhes
O gerador ts#infra gera/atualiza estes. Vamos examinar alguns dos arquivos principais destacados na árvore de arquivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- checkov.ts
- index.ts
Directoryinfra
Directorysrc/
Directorystages/
- application-stage.ts stacks cdk definidos aqui
Directorystacks/
- application-stack.ts recursos cdk definidos aqui
- main.ts ponto de entrada que define todos os stages
- cdk.json
- checkov.yml
- project.json
- …
- package.json
- tsconfig.json adiciona referências
- tsconfig.base.json adiciona 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 é o ponto de entrada para sua aplicação 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 }}Vamos instanciar nossos constructs CDK para construir nosso jogo de aventura em masmorra.
Tarefa 3: Atualizar nossa infraestrutura
Seção intitulada “Tarefa 3: Atualizar nossa infraestrutura”Vamos atualizar packages/infra/src/stacks/application-stack.ts para instanciar alguns dos nossos constructs gerados:
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'); }}Tarefa 4: Construir o código
Seção intitulada “Tarefa 4: Construir o código”Comandos Nx
Targets únicos vs múltiplos
Seção intitulada “Targets únicos vs múltiplos”O comando run-many executará um target em múltiplos sub-projetos listados (--all irá direcioná-los todos). Isso garante que as dependências sejam executadas na ordem correta.
Você também pode acionar um build (ou qualquer outra tarefa) para um target de projeto único executando o target diretamente no projeto. Por exemplo, para construir o projeto @dungeon-adventure/infra, execute o seguinte comando:
pnpm nx build infrayarn nx build infranpx nx build infrabunx nx build infraVocê também pode omitir o escopo e usar a sintaxe abreviada do Nx se preferir:
pnpm nx build infrayarn nx build infranpx nx build infrabunx nx build infraVisualizando suas dependências
Seção intitulada “Visualizando suas dependências”Para visualizar suas dependências, execute:
pnpm nx graphyarn nx graphnpx nx graphbunx nx graph
O Nx depende de cache para que você possa reutilizar artefatos de builds anteriores a fim de acelerar o desenvolvimento. Há alguma configuração necessária para que isso funcione corretamente e pode haver casos em que você queira realizar um build sem usar o cache. Para isso, basta adicionar o argumento --skip-nx-cache ao seu comando. Por exemplo:
pnpm nx build infra --skip-nx-cacheyarn nx build infra --skip-nx-cachenpx nx build infra --skip-nx-cachebunx nx build infra --skip-nx-cacheSe por algum motivo você quiser limpar seu cache (armazenado na pasta .nx), você pode executar o seguinte comando:
pnpm nx resetyarn nx resetnpx nx resetbunx nx resetUsando a linha de comando, execute o seguinte comando para corrigir quaisquer problemas de lint primeiro:
pnpm lintyarn lintnpm run lintbun lintEm seguida, execute o seguinte comando para um build completo:
pnpm buildyarn buildnpm run buildbun buildVocê será solicitado com o seguinte:
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 changesEsta mensagem indica que o NX detectou alguns arquivos que podem ser atualizados automaticamente para você. Neste caso, está se referindo aos arquivos tsconfig.json que não têm referências Typescript configuradas em projetos referenciados.
Selecione a opção Yes, sync the changes and run the tasks para prosseguir. Você deve notar que todos os seus erros de importação relacionados ao IDE são resolvidos automaticamente, pois o gerador de sincronização adicionará as referências typescript ausentes automaticamente!
Todos os artefatos construídos agora estão disponíveis dentro da pasta dist/ localizada na raiz do monorepo. Esta é uma prática padrão ao usar projetos gerados pelo @aws/nx-plugin, pois não polui sua árvore de arquivos com arquivos gerados. No caso de você querer limpar seus arquivos, exclua a pasta dist/ sem se preocupar com artefatos de build espalhados por toda a árvore de arquivos.
Parabéns! Você criou todos os sub-projetos necessários para começar a implementar o núcleo do nosso jogo de Aventura em Masmorra com IA. 🎉🎉🎉