Configurare un monorepo
Task 1: Creare un monorepo
Sezione intitolata “Task 1: Creare un monorepo”Per creare un nuovo monorepo, dalla directory desiderata, esegui il seguente 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=cdkQuesto configurerà un monorepo NX all’interno della directory dungeon-adventure. Quando apri la directory in VSCode, vedrai questa struttura di file:
Directory.nx/
- …
Directory.vscode/
- …
Directorynode_modules/
- …
Directorypackages/ qui risiedono i tuoi sotto-progetti
- …
- .gitignore
- biome.json configura Biome per linting e formattazione
- nx.json configura la CLI Nx e le impostazioni predefinite del monorepo
- package.json tutte le dipendenze node sono definite qui
- pnpm-lock.yaml o bun.lock, yarn.lock, package-lock.json a seconda del package manager
- pnpm-workspace.yaml se si utilizza pnpm
- README.md
- tsconfig.base.json tutti i sotto-progetti basati su node estendono questo
- tsconfig.json
- aws-nx-plugin.config.mts configurazione per Nx Plugin for AWS
Task 2: Scaffolding del Dungeon Adventure Game
Sezione intitolata “Task 2: Scaffolding del Dungeon Adventure Game”Con il workspace in posizione, creiamo lo scaffold dei sotto-progetti del gioco — la Game API, lo Story Agent, il server MCP Inventory, il database del gioco e il sito web — insieme alle connessioni che li collegano. Ci sono due modi per farlo:
- Veloce — copia i comandi direttamente dal diagramma qui sotto ed eseguili. Il modo più veloce per raggiungere lo stesso punto di partenza.
- Passo dopo passo — espandi la sezione qui sotto per eseguire ogni generatore tu stesso ed esaminare esattamente cosa produce ciascuno.
Il diagramma qui sotto è il workspace Dungeon Adventure: ogni progetto, componente e connessione che costruirai in questo modulo. Premi Copy commands per prendere l’intera serie, quindi eseguili dalla directory dungeon-adventure che hai creato nel Task 1.
Passo dopo passo
Invece di copiare tutti i comandi in una volta, puoi eseguire ogni generatore individualmente. Questo è il modo migliore per capire cosa aggiunge ogni generatore al tuo workspace. Esegui ogni generatore a turno, dalla directory dungeon-adventure che hai creato nel Task 1.
Creare una Game API
Sezione intitolata “Creare una Game API”Prima, creiamo la nostra Game API. Per farlo, crea un’API tRPC chiamata GameApi usando questi passaggi:
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-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#api - Compila i parametri richiesti
- name: GameApi
- framework: trpc
- Clicca su
Generate
Vedrai alcuni nuovi file apparire nel tuo albero dei file.
Esamina i file generati da ts#api in dettaglio
Di seguito è riportato un elenco di tutti i file che sono stati generati dal generatore ts#api. Esamineremo alcuni dei file chiave evidenziati nell’albero dei file:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ costrutti cdk specifici dell’app
Directoryapis/
- game-api.ts costrutto cdk per creare la tua API tRPC
- index.ts
- …
- index.ts
Directorycore/ costrutti cdk generici
Directoryapi/
- rest-api.ts costrutto cdk base per un’API Gateway Rest API
- trpc-utils.ts utilità per costrutti CDK API trpc
- utils.ts utilità per costrutti API
- index.ts
- runtime-config.ts
- index.ts
- project.json
- …
Directorygame-api/ API tRPC
Directorysrc/
Directoryclient/ client vanilla tipicamente usato per chiamate ts machine to machine
- index.ts
Directorymiddleware/ strumentazione powertools
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
Directoryschema/ definizioni di input e output per la tua API
- index.ts
- echo.ts schema di input e output di esempio
- z-async-iterable.ts schema Zod wrapper per output di sottoscrizione tRPC
Directoryprocedures/ implementazioni specifiche per le procedure/route della tua API
- echo.ts implementazione della procedura di esempio
- index.ts
- init.ts configura contesto e middleware
- handler.ts punto di ingresso del gestore Lambda (usa response streaming per REST API)
- local-server.ts usato quando si esegue il server tRPC localmente
- router.ts definisce il router tRPC e tutte le procedure
- project.json
- …
- vitest.workspace.ts
Diamo un’occhiata a questi file chiave:
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;Il router definisce il router tRPC per la tua API ed è il luogo in cui dichiarerai tutti i metodi della tua API. Come puoi vedere sopra, abbiamo un metodo chiamato echo con la sua implementazione nel file ./procedures/echo.ts. Il punto di ingresso del gestore Lambda è in handler.ts, che è configurato automaticamente dal generatore.
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 }));Questo file è l’implementazione del metodo echo e come puoi vedere è fortemente tipizzato dichiarando le sue strutture dati di input e output.
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>;Tutte le definizioni dello schema tRPC sono definite usando Zod e sono esportate come tipi typescript tramite la sintassi 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('*', '/*', '*')], }); }}Questo è il costrutto CDK che definisce la nostra GameApi. Fornisce un metodo defaultIntegrations che crea automaticamente una funzione Lambda per ogni procedura nella nostra API tRPC, puntando all’implementazione dell’API bundled. Ciò significa che al momento di cdk synth, il bundling non avviene (a differenza dell’uso di NodeJsFunction) poiché abbiamo già effettuato il bundle come parte del target di build del progetto backend.
Creare lo Story Agent
Sezione intitolata “Creare lo Story Agent”Ora creiamo il nostro Story Agent.
Story agent: Progetto Python
Sezione intitolata “Story agent: Progetto Python”Per creare un progetto 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-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - py#project - Compila i parametri richiesti
- name: story
- Clicca su
Generate
Vedrai alcuni nuovi file apparire nel tuo albero dei file.
Esamina i file generati da py#project in dettaglio
Il py#project genera questi file:
Directory.venv/ singolo virtual env per il monorepo
- …
Directorypackages/
Directorystory/
Directorydungeon_adventure_story/ modulo python
- …
Directorytests/
- …
- .python-version
- pyproject.toml
- project.json
- .python-version versione python uv fissata
- pyproject.toml
- uv.lock
Questo ha configurato un progetto Python e UV Workspace con ambiente virtuale condiviso.
Story agent
Sezione intitolata “Story agent”Per aggiungere un agente Strands al progetto con il generatore 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-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - py#agent - Compila i parametri richiesti
- project: story
- auth: cognito
- protocol: ag-ui
- Clicca su
Generate
Vedrai alcuni nuovi file apparire nel tuo albero dei file.
Esamina i file generati da py#agent in dettaglio
Il py#agent genera questi file:
Directorypackages/
Directorystory/
Directorydungeon_adventure_story/ modulo python
Directoryagent/
- main.py punto di ingresso per il tuo agente in Bedrock AgentCore Runtime
- agent.py definisce un agente di esempio e strumenti
- session.py risolve un SessionManager per persistere lo stato della conversazione
- Dockerfile definisce l’immagine docker per il deployment su AgentCore Runtime
Directorycommon/constructs/
Directorysrc
Directoryapp/agents/story-agent/
- story-agent.ts costrutto per il deployment del tuo agente Story su AgentCore Runtime
Diamo un’occhiata ad alcuni dei file in dettaglio:
from contextlib import contextmanager
from strands import Agent, toolfrom 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
@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=[log_model_errors, log_tool_errors], )Questo crea un agente Strands di esempio e definisce uno strumento di sottrazione. log_model_errors e log_tool_errors sono hook dal progetto condiviso dungeon_adventure_agent_connection che registrano i fallimenti di modello/strumento invece di lasciarli fallire silenziosamente.
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 StreamingResponsefrom starlette.middleware.base import BaseHTTPMiddleware
from .agent import get_agentfrom .session import get_session_manager
logging.basicConfig(level=logging.INFO)
SESSION_ID_HEADER = "x-amzn-bedrock-agentcore-runtime-session-id"
@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()), ) yield
class _SessionIdMiddleware(BaseHTTPMiddleware): """Bind the session ID for this request so downstream MCP / A2A clients forward it on outbound calls."""
async def dispatch(self, request: Request, call_next): session_id = request.headers.get(SESSION_ID_HEADER) or str(uuid.uuid4()) with session_id_context(session_id): return await call_next(request)
app = 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"}Questo è il punto di ingresso per l’agente. Poiché abbiamo selezionato --protocol=ag-ui, il generatore avvolge il nostro Agent Strands con StrandsAgent da ag_ui_strands e lo monta su un’app FastAPI che parla il protocollo AG-UI — questo è ciò con cui CopilotKit comunicherà dal sito web React. L’agente è costruito all’interno di un gestore lifespan — non al momento dell’import — quindi l’avvio del container, non l’import del modulo, possiede la costruzione, e ogni sessione AgentCore ottiene il proprio container. Il _SessionIdMiddleware lega l’ID di sessione AgentCore runtime in ingresso su un ContextVar in modo che qualsiasi client MCP/A2A downstream che colleghiamo in seguito (ad es. il server MCP Inventory nel Modulo 2) lo inoltri automaticamente sulle sue chiamate in uscita. Poiché AG-UI memorizza nella cache un agente Strands per thread_id, colleghiamo un session_manager_provider invece del session manager dell’agente template — questo dà a ogni thread il proprio SessionManager in modo che la cronologia delle conversazioni persista tra i turni. Quella funzione get_session_manager() proviene da un session.py generato: quando deployato, restituisce un strands.session.S3SessionManager supportato da un bucket S3 che il generatore fornisce automaticamente; sotto agent-dev (LOCAL_DEV=true) restituisce sempre un FileSessionManager che scrive in una directory temporanea locale, indipendentemente dalla configurazione deployata.
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`; }}Questo configura un AgentRuntimeArtifact CDK che carica l’immagine Docker del tuo agente su ECR e la ospita usando AgentCore Runtime. Poiché abbiamo scelto --auth=cognito, il costrutto richiede l’identità user pool/client e autorizza le invocazioni AgentCore Runtime tramite Cognito, inoltrando l’header Authorization del chiamante. Fornisce anche il bucket di sessione che il session.py dello Story Agent legge a runtime — un bucket S3 crittografato con KMS con log di accesso al server consegnati a CloudWatch Logs — concede all’agente l’accesso in lettura/scrittura ad esso, gli concede l’accesso per invocare i modelli Bedrock e registra il suo ARN e il nome del bucket in RuntimeConfig in modo che sia l’agente (a runtime, tramite AppConfig) che la Game API (al momento di synth, tramite invocationUrl) possano trovarlo.
Potresti notare un Dockerfile extra, che fa riferimento all’immagine Docker dal progetto story, permettendoci di co-localizzare il Dockerfile e il codice sorgente dell’agente.
Configurare gli strumenti Inventory
Sezione intitolata “Configurare gli strumenti Inventory”Inventory: Progetto TypeScript
Sezione intitolata “Inventory: Progetto TypeScript”Creiamo un server MCP per fornire strumenti al nostro Story Agent per gestire l’inventario di un giocatore.
Prima, creiamo un progetto 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-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#project - Compila i parametri richiesti
- name: inventory
- Clicca su
Generate
Questo creerà un progetto TypeScript vuoto.
Esamina i file generati da ts#project in dettaglio
Il generatore ts#project genera questi file.
Directorypackages/
Directoryinventory/
Directorysrc/
- index.ts punto di ingresso con funzione di esempio
- project.json configurazione del progetto
- vitest.config.mts configurazione dei test
- tsconfig.json configurazione typescript base per il progetto
- tsconfig.lib.json configurazione typescript per il progetto mirata alla compilazione e bundling
- tsconfig.spec.json configurazione typescript per i test
- tsconfig.base.json aggiornato per configurare un alias per altri progetti per fare riferimento a questo
Inventory: Server MCP
Sezione intitolata “Inventory: Server MCP”Successivamente, aggiungeremo un server MCP al nostro progetto 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-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#mcp-server - Compila i parametri richiesti
- project: inventory
- Clicca su
Generate
Questo aggiungerà un server MCP.
Esamina i file generati da ts#mcp-server in dettaglio
Il generatore ts#mcp-server genera questi file.
Directorypackages/
Directoryinventory/
Directorysrc/mcp-server/
- index.ts barrel export
- server.ts crea il server MCP
Directorytools/
- divide.ts strumento di esempio
Directoryresources/
- sample-guidance.ts risorsa di esempio
- stdio.ts punto di ingresso per MCP con trasporto STDIO
- http.ts punto di ingresso per MCP con trasporto HTTP Streamable
- Dockerfile costruisce l’immagine per AgentCore Runtime
- rolldown.config.ts configurazione per il bundling del server MCP per il deployment su AgentCore
Directorycommon/constructs/
Directorysrc
Directoryapp/mcp-servers/inventory-mcp-server/
- inventory-mcp-server.ts costrutto per il deployment del tuo server MCP inventory su AgentCore Runtime
Creare il database del gioco
Sezione intitolata “Creare il database del gioco”Lo stato del nostro gioco — partite salvate e inventario di ogni giocatore — risiede in Amazon DynamoDB. Crea un progetto DynamoDB chiamato DungeonDb con il generatore 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-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#dynamodb - Compila i parametri richiesti
- name: DungeonDb
- Clicca su
Generate
Vedrai alcuni nuovi file apparire nel tuo albero dei file.
Esamina i file generati da ts#dynamodb in dettaglio
Il generatore ts#dynamodb genera questi file.
Directorypackages/
Directorydungeon-db/
- config.json configurazione DynamoDB inclusi porta, nome tabella, impostazioni container e Global Secondary Indexes
Directorysrc/
- index.ts punto di ingresso ed esportazioni
- client.ts singleton client DynamoDB e risoluzione nome tabella
Directoryentities/
- example.ts entità ElectroDB di esempio (sostituiremo questa)
- index.ts esportazioni entità
- project.json aggiunge i target
devepull-image
Directorycommon/
Directoryscripts/
Directorysrc/
Directorydynamodb/
- create-local-table.ts crea la tabella in DynamoDB Local
- pull-image.ts scarica l’immagine DynamoDB Local
- start-container.ts avvia il container DynamoDB Local
Directoryconstructs/
Directorysrc/
Directoryapp/dynamodb/
- dungeon-db.ts costrutto per il provisioning della tua tabella
Directorycore/
- dynamodb.ts costrutto tabella DynamoDB generico
Il src/client.ts generato esporta getDynamoDBClient() e resolveTableName(). Quando LOCAL_DEV=true (impostato automaticamente dai target dev) questi si connettono a DynamoDB Local; altrimenti si connettono ad AWS e risolvono il nome della tabella deployata dalla Runtime Configuration. Modelleremo le nostre entità Game e Inventory in questo progetto nel Modulo 2.
Per maggiori dettagli, fai riferimento alla guida del generatore ts#dynamodb.
Creare l’interfaccia utente (UI)
Sezione intitolata “Creare l’interfaccia utente (UI)”Successivamente, creeremo l’UI che ti permetterà di interagire con il gioco.
Game UI: Website
Sezione intitolata “Game UI: Website”Per creare l’UI, crea un sito web chiamato GameUI usando questi passaggi:
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-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#website - Compila i parametri richiesti
- name: GameUI
- ux: shadcn
- Clicca su
Generate
Vedrai alcuni nuovi file apparire nel tuo albero dei file.
Esamina i file generati da ts#website in dettaglio
Il ts#website genera questi file. Esaminiamo alcuni dei file chiave evidenziati nell’albero dei file:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ costrutti cdk specifici dell’app
Directorystatic-websites/
- game-ui.ts costrutto cdk per creare la tua Game UI
Directorycore/
- static-website.ts costrutto sito web statico generico
Directorygame-ui/
Directorypublic/
- …
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.tsx layout complessivo della pagina usando shadcn
SidebarProvider+ header
- index.tsx layout complessivo della pagina usando shadcn
- app-sidebar.tsx sidebar shadcn predefinita con elementi di navigazione
- alert.tsx, spinner.tsx primitive di feedback avvolte in shadcn
Directoryroutes/ route basate su file @tanstack/react-router
- index.tsx pagina root ’/’
- __root.tsx tutte le pagine usano questo componente come base
- config.ts
- main.tsx punto di ingresso React
- routeTree.gen.ts questo viene aggiornato automaticamente da @tanstack/react-router
- styles.css importa globali shadcn condivisi (Tailwind v4)
- index.html
- project.json
- vite.config.mts
- …
Directorycommon/
Directoryshadcn/ libreria shadcn/ui condivisa (token tema,
Button,Card,Input,Sidebar, …) importata da ogni sito webux=shadcn- src/components/ui/*
- src/styles/globals.css token di design Tailwind + shadcn
- …
import * as url from 'url';import { Construct } from 'constructs';import { StaticWebsite } from '../../core/index.js';
export class GameUI extends StaticWebsite { constructor(scope: Construct, id: string) { super(scope, id, { websiteName: 'GameUI', websiteFilePath: url.fileURLToPath( new URL( '../../../../../../dist/packages/game-ui/bundle', import.meta.url, ), ), }); }}Questo è il costrutto CDK che definisce la nostra GameUI. Ha già configurato il percorso del file al bundle generato per la nostra UI basata su Vite. Ciò significa che al momento del build, il bundling avviene all’interno del target di build del progetto game-ui e l’output viene utilizzato qui.
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>, );Questo è il punto di ingresso dove React viene montato. Lo stile proviene dai token Tailwind v4 importati tramite styles.css. @tanstack/react-router è configurato in modalità file-based routing: finché il server di sviluppo è in esecuzione, qualsiasi file che crei sotto routes/ viene rilevato automaticamente e l’albero delle route viene rigenerato. I generatori successivi (auth, connection) patcheranno questo file con AST per avvolgere <App /> in provider aggiuntivi.
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 verrà renderizzato quando si naviga verso la route /. @tanstack/react-router gestirà la Route per te ogni volta che crei/sposti questo file (finché il server di sviluppo è in esecuzione).
Game UI: Auth
Sezione intitolata “Game UI: Auth”Configuriamo la nostra Game UI per richiedere l’accesso autenticato tramite Amazon Cognito usando questi passaggi:
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-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#website#auth - Compila i parametri richiesti
- cognitoDomain: game-ui
- project: @dungeon-adventure/game-ui
- allowSignup: true
- Clicca su
Generate
Vedrai alcuni nuovi file apparire/cambiare nel tuo albero dei file.
Esamina i file generati da ts#website#auth in dettaglio
Il generatore ts#website#auth aggiorna/genera questi file. Esaminiamo alcuni dei file chiave evidenziati nell’albero dei file:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- user-identity.ts costrutto cdk per creare pool utente/identità
Directorygame-ui/
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.tsx aggiunge l’utente loggato/logout all’header
DirectoryCognitoAuth/
- index.tsx gestisce il login in Cognito
DirectoryRuntimeConfig/
- index.tsx recupera il
runtime-config.jsone lo fornisce ai figli tramite contesto
- index.tsx recupera il
Directoryhooks/
- useRuntimeConfig.tsx
- main.tsx Aggiornato per aggiungere 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>, );I componenti RuntimeConfigProvider e CognitoAuth sono stati aggiunti al file main.tsx tramite una trasformazione AST. Questo permette al componente CognitoAuth di autenticarsi con Amazon Cognito recuperando il runtime-config.json che contiene la configurazione di connessione cognito richiesta per effettuare le chiamate backend alla destinazione corretta.
Game UI: Connettere alla Game API
Sezione intitolata “Game UI: Connettere alla Game API”Configuriamo la nostra Game UI per connettersi alla nostra Game API creata in precedenza.
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-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - connection - Compila i parametri richiesti
- sourceProject: @dungeon-adventure/game-ui
- targetProject: @dungeon-adventure/game-api
- Clicca su
Generate
Vedrai alcuni nuovi file apparire/cambiare nel tuo albero dei file.
Esamina i file di connessione UI → tRPC
Il generatore connection genera/aggiorna questi file. Esaminiamo alcuni dei file chiave evidenziati nell’albero dei file:
Directorypackages/
Directorygame-ui/
Directorysrc/
Directorycomponents/
- GameApiClientProvider.tsx configura il client GameAPI
Directoryhooks/
- useGameApi.tsx hook per chiamare la GameApi
- main.tsx inietta i provider trpc client
- 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;};Questo hook fornisce accesso al client tRPC per chiamare la GameApi. Per esempi su come chiamare le API tRPC, fai riferimento alla guida all’uso dell’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>, );Il file main.tsx è stato aggiornato tramite una trasformazione AST per iniettare i provider tRPC.
Story Agent: Connettere al server MCP Inventory
Sezione intitolata “Story Agent: Connettere al server MCP Inventory”Connettiamo il nostro Story Agent al server MCP Inventory in modo che l’agente possa scoprire e invocare gli strumenti del server 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-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - connection - Compila i parametri richiesti
- sourceProject: story
- targetProject: inventory
- Clicca su
Generate
Esamina i file di connessione Story Agent → Inventory MCP
Il generatore connection genera/aggiorna questi file:
Directorypackages/
Directorycommon/
Directoryagent_connection/
Directorydungeon_adventure_agent_connection/
Directorycore/
- agentcore_endpoints.py Risoluzione ARN/URL indipendente dal framework
- agentcore_mcp_transport.py Trasporto MCP indipendente dal framework
- agentcore_mcp_client_strands.py Client MCP Strands che avvolge il trasporto
Directoryauth/
httpx.Authindipendente dal framework per SigV4 / inoltro sessione- …
Directoryapp/
- inventory_mcp_server_client_strands.py Client Strands per connettersi al server MCP Inventory
- __init__.py Re-esporta i client per connessione
Directorystory/
Directorydungeon_adventure_story/agent/
- agent.py Modificato per importare e usare il client MCP
Il generatore:
- Crea un progetto Python condiviso
agent_connection(se non esiste già) con il coreAgentCoreMCPClientStrands - Genera una classe
InventoryMcpServerClientStrandsche gestisce la connessione al server MCP sia localmente (HTTP diretto) che quando deployato (tramite AgentCore con autenticazione IAM) - Trasforma
agent.pyper importare il client, creare un’istanza e collegare gli strumenti del server MCP all’agente - Aggiunge il progetto
agent_connectioncome dipendenza workspace del progetto story - Aggiorna il target
devper avviare automaticamente il server MCP quando si esegue localmente
Per maggiori dettagli, fai riferimento alla guida alla connessione Python Agent a MCP.
Game UI: Connettere allo Story Agent
Sezione intitolata “Game UI: Connettere allo Story Agent”Connettiamo la nostra Game UI allo Story Agent. Poiché l’agente parla AG-UI, il generatore connection collega CopilotKit: un componente chat con tema e un HttpAgent @ag-ui/client pronto per il rendering.
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-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - connection - Compila i parametri richiesti
- sourceProject: @dungeon-adventure/game-ui
- targetProject: story
- Clicca su
Generate
Esamina i file di connessione UI → Story Agent
Il generatore connection genera/aggiorna questi file:
Directorypackages/
Directorygame-ui/
Directorysrc/
Directorycomponents/
- AguiProvider.tsx
CopilotKitProvidercon ogni agente AG-UI connesso registrato Directorycopilot/
- index.tsx
CopilotChat/CopilotSidebar/CopilotPopupcon tema Shadcn - ShadcnAssistantMessage.tsx, ShadcnUserMessage.tsx, ShadcnChatInput.tsx, ShadcnCursor.tsx, copilot.css
- index.tsx
- AguiProvider.tsx
Directoryhooks/
- useAguiStoryAgent.tsx Costruisce un
HttpAgent, inietta il token bearer Cognito e riempiethreadIdall’id di sessione di 33 caratteri di AgentCore
- useAguiStoryAgent.tsx Costruisce un
- main.tsx Avvolge
<App />in<AguiProvider>
Il generatore:
- Rileva l’
uxdel sito web React (Shadcn qui) e fornisce componenti chat corrispondenti. - Registra ogni agente connesso su un singolo
CopilotKitProvider— rieseguire per un altro agente aggiunge semplicemente un altro hook. - Legge l’ARN runtime dell’agente dalla Runtime Configuration, costruisce l’URL di invocazione AgentCore e allega il token bearer Cognito più l’header id sessione AgentCore.
Per maggiori dettagli, fai riferimento alla guida alla connessione React a AG-UI.
Connettere la Game API e il server MCP Inventory al database
Sezione intitolata “Connettere la Game API e il server MCP Inventory al database”Sia la Game API che il server MCP Inventory leggono e scrivono la nostra tabella DynamoDB, quindi connettiamoli al progetto DungeonDb. Il generatore connection rileva che il target è un progetto ts#dynamodb e collega il target dev di ogni progetto sorgente per avviare automaticamente DynamoDB Local.
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-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - connection - Compila i parametri richiesti
- sourceProject: @dungeon-adventure/game-api
- targetProject: @dungeon-adventure/dungeon-db
- Clicca su
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-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - connection - Compila i parametri richiesti
- sourceProject: @dungeon-adventure/inventory
- targetProject: @dungeon-adventure/dungeon-db
- Clicca su
Generate
Game UI: Infrastruttura
Sezione intitolata “Game UI: Infrastruttura”Creiamo il sotto-progetto finale per l’infrastruttura 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-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#infra - Compila i parametri richiesti
- name: infra
- Clicca su
Generate
Vedrai alcuni nuovi file apparire/cambiare nel tuo albero dei file.
Esamina i file generati da ts#infra in dettaglio
Il generatore ts#infra genera/aggiorna questi. Esaminiamo alcuni dei file chiave evidenziati nell’albero dei file:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- checkov.ts
- index.ts
Directoryinfra
Directorysrc/
Directorystages/
- application-stage.ts stack cdk definiti qui
Directorystacks/
- application-stack.ts risorse cdk definite qui
- main.ts punto di ingresso che definisce tutti gli stage
- cdk.json
- checkov.yml
- project.json
- …
- package.json
- tsconfig.json aggiungi riferimenti
- tsconfig.base.json aggiungi 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();Questo è il punto di ingresso per la tua applicazione 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 }}Istanziamo i nostri costrutti CDK per costruire il nostro gioco di avventura nel dungeon.
Task 3: Aggiornare la nostra infrastruttura
Sezione intitolata “Task 3: Aggiornare la nostra infrastruttura”Aggiorniamo packages/infra/src/stacks/application-stack.ts per istanziare alcuni dei nostri costrutti generati:
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'); }}Task 4: Costruire il codice
Sezione intitolata “Task 4: Costruire il codice”Comandi Nx
Target singoli vs multipli
Sezione intitolata “Target singoli vs multipli”Il comando run-many eseguirà un target su più sotto-progetti elencati (--all li targetizzerà tutti). Questo assicura che le dipendenze vengano eseguite nell’ordine corretto.
Puoi anche attivare una build (o qualsiasi altro task) per un singolo target di progetto eseguendo il target sul progetto direttamente. Ad esempio, per costruire il progetto @dungeon-adventure/infra, esegui il seguente comando:
pnpm nx build infrayarn nx build infranpx nx build infrabunx nx build infraPuoi anche omettere lo scope e usare la sintassi abbreviata Nx se preferisci:
pnpm nx build infrayarn nx build infranpx nx build infrabunx nx build infraVisualizzare le tue dipendenze
Sezione intitolata “Visualizzare le tue dipendenze”Per visualizzare le tue dipendenze, esegui:
pnpm nx graphyarn nx graphnpx nx graphbunx nx graph
Caching
Sezione intitolata “Caching”Nx si basa sul caching in modo da poter riutilizzare gli artefatti dalle build precedenti per velocizzare lo sviluppo. C’è una certa configurazione richiesta per far funzionare correttamente questo e potrebbero esserci casi in cui vuoi eseguire una build senza usare la cache. Per farlo, aggiungi semplicemente l’argomento --skip-nx-cache al tuo comando. Ad esempio:
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 per qualsiasi motivo volessi mai cancellare la tua cache (memorizzata nella cartella .nx), puoi eseguire il seguente comando:
pnpm nx resetyarn nx resetnpx nx resetbunx nx resetUsando la riga di comando, esegui il seguente comando per correggere prima eventuali problemi di lint:
pnpm lintyarn lintnpm run lintbun lintQuindi, esegui il seguente comando per una build completa:
pnpm buildyarn buildnpm run buildbun buildTi verrà richiesto quanto segue:
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 changesQuesto messaggio indica che NX ha rilevato alcuni file che possono essere aggiornati automaticamente per te. In questo caso, si riferisce ai file tsconfig.json che non hanno riferimenti TypeScript configurati sui progetti di riferimento.
Seleziona l’opzione Yes, sync the changes and run the tasks per procedere. Dovresti notare che tutti gli errori di import relativi all’IDE vengono risolti automaticamente poiché il generatore di sincronizzazione aggiungerà automaticamente i riferimenti TypeScript mancanti!
Tutti gli artefatti costruiti sono ora disponibili all’interno della cartella dist/ situata alla radice del monorepo. Questa è una pratica standard quando si utilizzano progetti generati da @aws/nx-plugin poiché non inquina il tuo albero dei file con file generati. Nel caso in cui tu voglia pulire i tuoi file, elimina la cartella dist/ senza preoccuparti che gli artefatti di build siano sparsi in tutto l’albero dei file.
Congratulazioni! Hai creato tutti i sotto-progetti richiesti per iniziare a implementare il nucleo del nostro gioco AI Dungeon Adventure. 🎉🎉🎉