Configurer un monorepo
Tâche 1 : Créer un monorepo
Section intitulée « Tâche 1 : Créer un monorepo »Pour créer un nouveau monorepo, depuis le répertoire de votre choix, exécutez la commande suivante :
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=cdkCela configurera un monorepo NX dans le répertoire dungeon-adventure. Lorsque vous ouvrez le répertoire dans VSCode, vous verrez cette structure de fichiers :
Répertoire.nx/
- …
Répertoire.vscode/
- …
Répertoirenode_modules/
- …
Répertoirepackages/ c’est ici que résideront vos sous-projets
- …
- .gitignore
- biome.json configure Biome pour le linting et le formatage
- nx.json configure le CLI Nx et les paramètres par défaut du monorepo
- package.json toutes les dépendances node sont définies ici
- pnpm-lock.yaml ou bun.lock, yarn.lock, package-lock.json selon le gestionnaire de paquets
- pnpm-workspace.yaml si vous utilisez pnpm
- README.md
- tsconfig.base.json tous les sous-projets node étendent ce fichier
- tsconfig.json
- aws-nx-plugin.config.mts configuration du Nx Plugin for AWS
Tâche 2 : Créer l’échafaudage du jeu d’aventure dans le donjon
Section intitulée « Tâche 2 : Créer l’échafaudage du jeu d’aventure dans le donjon »Une fois l’espace de travail en place, nous créons l’échafaudage des sous-projets du jeu — l’API de jeu, l’agent Story, le serveur MCP d’inventaire, la base de données du jeu et le site web — ainsi que les connexions qui les relient. Il y a deux façons de procéder :
- Rapide — copiez les commandes directement depuis le diagramme ci-dessous et exécutez-les. C’est le moyen le plus rapide d’atteindre le même point de départ.
- Étape par étape — développez la section ci-dessous pour exécuter chaque générateur vous-même et examiner exactement ce que chacun produit.
Le diagramme ci-dessous est l’espace de travail Dungeon Adventure : chaque projet, composant et connexion que vous allez construire dans ce module. Cliquez sur Copier les commandes pour prendre toute la série, puis exécutez-les depuis le répertoire dungeon-adventure que vous avez créé à la Tâche 1.
Étape par étape
Plutôt que de copier toutes les commandes en une seule fois, vous pouvez exécuter chaque générateur individuellement. C’est la meilleure façon de comprendre ce que chaque générateur ajoute à votre espace de travail. Exécutez chaque générateur à tour de rôle, depuis le répertoire dungeon-adventure que vous avez créé à la Tâche 1.
Créer une API de jeu
Section intitulée « Créer une API de jeu »Commençons par créer notre API de jeu. Pour ce faire, créez une API tRPC appelée GameApi en suivant ces étapes :
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-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - ts#api - Remplissez les paramètres requis
- name: GameApi
- framework: trpc
- Cliquez sur
Generate
Vous verrez de nouveaux fichiers apparaître dans votre arborescence de fichiers.
Examiner en détail les fichiers générés par ts#api
Voici la liste de tous les fichiers générés par le générateur ts#api. Nous allons examiner certains des fichiers clés mis en évidence dans l’arborescence :
Répertoirepackages/
Répertoirecommon/
Répertoireconstructs/
Répertoiresrc/
Répertoireapp/ constructs cdk spécifiques à l’application
Répertoireapis/
- game-api.ts construct cdk pour créer votre API tRPC
- index.ts
- …
- index.ts
Répertoirecore/ constructs cdk génériques
Répertoireapi/
- rest-api.ts construct cdk de base pour une API Gateway Rest API
- trpc-utils.ts utilitaires pour les constructs CDK d’API tRPC
- utils.ts utilitaires pour les constructs d’API
- index.ts
- runtime-config.ts
- index.ts
- project.json
- …
Répertoiregame-api/ API tRPC
Répertoiresrc/
Répertoireclient/ client vanilla généralement utilisé pour les appels machine à machine en TypeScript
- index.ts
Répertoiremiddleware/ instrumentation powertools
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
Répertoireschema/ définitions des entrées et sorties de votre API
- index.ts
- echo.ts schéma d’entrée et de sortie d’exemple
- z-async-iterable.ts schéma Zod wrapper pour la sortie d’abonnement tRPC
Répertoireprocedures/ implémentations spécifiques pour les procédures/routes de votre API
- echo.ts implémentation de procédure d’exemple
- index.ts
- init.ts configure le contexte et le middleware
- handler.ts point d’entrée du gestionnaire Lambda (utilise le streaming de réponse pour les API REST)
- local-server.ts utilisé lors de l’exécution locale du serveur tRPC
- router.ts définit le routeur tRPC et toutes les procédures
- project.json
- …
- vitest.workspace.ts
Examinons ces fichiers clés :
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;Le routeur définit le routeur tRPC pour votre API et est l’endroit où vous déclarerez toutes vos méthodes d’API. Comme vous pouvez le voir ci-dessus, nous avons une méthode appelée echo avec son implémentation dans le fichier ./procedures/echo.ts. Le point d’entrée du gestionnaire Lambda se trouve dans handler.ts, qui est configuré automatiquement par le générateur.
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 }));Ce fichier est l’implémentation de la méthode echo et, comme vous pouvez le voir, est fortement typé en déclarant ses structures de données d’entrée et de sortie.
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>;Toutes les définitions de schéma tRPC sont définies à l’aide de Zod et sont exportées en tant que types TypeScript via la syntaxe 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('*', '/*', '*')], }); }}Il s’agit du construct CDK qui définit notre GameApi. Il fournit une méthode defaultIntegrations qui crée automatiquement une fonction Lambda pour chaque procédure de notre API tRPC, pointant vers l’implémentation de l’API bundlée. Cela signifie qu’au moment du cdk synth, le bundling n’a pas lieu (contrairement à l’utilisation de NodeJsFunction) car nous l’avons déjà bundlé dans le cadre de la cible de build du projet backend.
Créer l’agent Story
Section intitulée « Créer l’agent Story »Créons maintenant notre agent Story.
Agent Story : projet Python
Section intitulée « Agent Story : projet Python »Pour créer un projet 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-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - py#project - Remplissez les paramètres requis
- name: story
- Cliquez sur
Generate
Vous verrez de nouveaux fichiers apparaître dans votre arborescence de fichiers.
Examiner en détail les fichiers générés par py#project
Le py#project génère ces fichiers :
Répertoire.venv/ environnement virtuel unique pour le monorepo
- …
Répertoirepackages/
Répertoirestory/
Répertoiredungeon_adventure_story/ module python
- …
Répertoiretests/
- …
- .python-version
- pyproject.toml
- project.json
- .python-version version python uv épinglée
- pyproject.toml
- uv.lock
Cela a configuré un projet Python et un UV Workspace avec un environnement virtuel partagé.
Agent Story
Section intitulée « Agent Story »Pour ajouter un agent Strands au projet avec le générateur 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-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - py#agent - Remplissez les paramètres requis
- project: story
- auth: cognito
- protocol: ag-ui
- Cliquez sur
Generate
Vous verrez de nouveaux fichiers apparaître dans votre arborescence de fichiers.
Examiner en détail les fichiers générés par py#agent
Le py#agent génère ces fichiers :
Répertoirepackages/
Répertoirestory/
Répertoiredungeon_adventure_story/ module python
Répertoireagent/
- main.py point d’entrée de votre agent dans Bedrock AgentCore Runtime
- agent.py définit un exemple d’agent et d’outils
- session.py résout un SessionManager pour persister l’état de la conversation
Répertoiremiddleware/
- session_id_middleware.py lie l’ID de session AgentCore entrant pour la requête
- Dockerfile définit l’image docker pour le déploiement sur AgentCore Runtime
Répertoirecommon/constructs/
Répertoiresrc
Répertoireapp/agents/story-agent/
- story-agent.ts construct pour déployer votre agent Story sur AgentCore Runtime
Examinons certains fichiers en détail :
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, )Cela crée un exemple d’agent Strands et définit un outil de soustraction. log_model_errors et log_tool_errors sont des hooks du projet partagé dungeon_adventure_agent_connection qui enregistrent les échecs de modèle/outil au lieu de les laisser échouer silencieusement.
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)Le SessionIdMiddleware lie l’ID de session AgentCore runtime entrant sur un ContextVar pour la durée de la requête.
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"}Il s’agit du point d’entrée de l’agent. Parce que nous avons sélectionné --protocol=ag-ui, le générateur enveloppe notre Agent Strands avec StrandsAgent de ag_ui_strands et le monte sur une application FastAPI qui parle le protocole AG-UI — c’est ce avec quoi CopilotKit communiquera depuis le site web React. L’agent est construit dans un gestionnaire lifespan — pas au moment de l’import — de sorte que le démarrage du conteneur, et non l’import du module, gère la construction, et chaque session AgentCore obtient son propre conteneur. Le SessionIdMiddleware que nous avons vu ci-dessus transfère l’ID de session AgentCore runtime entrant afin que tout client MCP/A2A en aval que nous connectons plus tard (par exemple, le serveur MCP d’inventaire dans le Module 2) le transfère automatiquement lors de ses appels sortants. Puisque AG-UI met en cache un agent Strands par thread_id, nous branchons un session_manager_provider plutôt que le gestionnaire de session propre à l’agent modèle — cela donne à chaque thread son propre SessionManager afin que l’historique des conversations persiste entre les tours. Cette fonction get_session_manager() provient d’un session.py généré : déployé, il retourne un strands.session.S3SessionManager soutenu par un bucket S3 que le générateur provisionne automatiquement ; sous agent-dev (LOCAL_DEV=true), il retourne toujours un FileSessionManager écrivant dans un répertoire temporaire local, quelle que soit la configuration déployée.
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`; }}Cela configure un AgentRuntimeArtifact CDK qui télécharge votre image Docker d’agent vers ECR et l’héberge en utilisant AgentCore Runtime. Parce que nous avons choisi --auth=cognito, le construct nécessite l’identité du pool d’utilisateurs/client et autorise les invocations AgentCore Runtime via Cognito, en transmettant l’en-tête Authorization de l’appelant. Il provisionne également le bucket de session que le session.py de l’agent Story lit au moment de l’exécution — un bucket S3 chiffré KMS avec des journaux d’accès au serveur livrés à CloudWatch Logs — accorde à l’agent un accès en lecture/écriture, lui accorde l’accès pour invoquer des modèles Bedrock, et enregistre son ARN et le nom du bucket dans RuntimeConfig afin que l’agent (au moment de l’exécution, via AppConfig) et l’API de jeu (au moment de la synthèse, via invocationUrl) puissent le trouver.
Vous remarquerez peut-être un Dockerfile supplémentaire, qui référence l’image Docker du projet story, nous permettant de co-localiser le Dockerfile et le code source de l’agent.
Configurer les outils d’inventaire
Section intitulée « Configurer les outils d’inventaire »Inventaire : projet TypeScript
Section intitulée « Inventaire : projet TypeScript »Créons un serveur MCP pour fournir des outils à notre agent Story afin de gérer l’inventaire d’un joueur.
Tout d’abord, nous créons un projet 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-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - ts#project - Remplissez les paramètres requis
- name: inventory
- Cliquez sur
Generate
Cela créera un projet TypeScript vide.
Examiner en détail les fichiers générés par ts#project
Le générateur ts#project génère ces fichiers.
Répertoirepackages/
Répertoireinventory/
Répertoiresrc/
- index.ts point d’entrée avec une fonction d’exemple
- project.json configuration du projet
- vitest.config.mts configuration des tests
- tsconfig.json configuration TypeScript de base pour le projet
- tsconfig.lib.json configuration TypeScript pour le projet ciblé pour la compilation et le bundling
- tsconfig.spec.json configuration TypeScript pour les tests
- tsconfig.base.json mis à jour pour configurer un alias permettant à d’autres projets de le référencer
Inventaire : serveur MCP
Section intitulée « Inventaire : serveur MCP »Ensuite, nous allons ajouter un serveur MCP à notre projet 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-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - ts#mcp-server - Remplissez les paramètres requis
- project: inventory
- Cliquez sur
Generate
Cela ajoutera un serveur MCP.
Examiner en détail les fichiers générés par ts#mcp-server
Le générateur ts#mcp-server génère ces fichiers.
Répertoirepackages/
Répertoireinventory/
Répertoiresrc/mcp-server/
- index.ts export barrel
- server.ts crée le serveur MCP
Répertoiretools/
- divide.ts outil d’exemple
Répertoireresources/
- sample-guidance.ts ressource d’exemple
- stdio.ts point d’entrée pour MCP avec transport STDIO
- http.ts point d’entrée pour MCP avec transport HTTP Streamable
- Dockerfile construit l’image pour AgentCore Runtime
- rolldown.config.ts configuration pour le bundling du serveur MCP pour le déploiement sur AgentCore
Répertoirecommon/constructs/
Répertoiresrc
Répertoireapp/mcp-servers/inventory-mcp-server/
- inventory-mcp-server.ts construct pour déployer votre serveur MCP d’inventaire sur AgentCore Runtime
Créer la base de données du jeu
Section intitulée « Créer la base de données du jeu »L’état de notre jeu — les parties sauvegardées et l’inventaire de chaque joueur — réside dans Amazon DynamoDB. Créez un projet DynamoDB appelé DungeonDb avec le générateur 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-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - ts#dynamodb - Remplissez les paramètres requis
- name: DungeonDb
- Cliquez sur
Generate
Vous verrez de nouveaux fichiers apparaître dans votre arborescence de fichiers.
Examiner en détail les fichiers générés par ts#dynamodb
Le générateur ts#dynamodb génère ces fichiers.
Répertoirepackages/
Répertoiredungeon-db/
- config.json configuration DynamoDB incluant le port, le nom de la table, les paramètres du conteneur et les Index Secondaires Globaux
Répertoiresrc/
- index.ts point d’entrée et exports
- client.ts singleton du client DynamoDB et résolution du nom de table
Répertoireentities/
- example.ts exemple d’entité ElectroDB (nous remplacerons ceci)
- index.ts exports des entités
- project.json ajoute les cibles
devetpull-image
Répertoirecommon/
Répertoirescripts/
Répertoiresrc/
Répertoiredynamodb/
- create-local-table.ts crée la table dans DynamoDB Local
- pull-image.ts télécharge l’image DynamoDB Local
- start-container.ts démarre le conteneur DynamoDB Local
Répertoireconstructs/
Répertoiresrc/
Répertoireapp/dynamodb/
- dungeon-db.ts construct pour provisionner votre table
Répertoirecore/
- dynamodb.ts construct générique de table DynamoDB
Le src/client.ts généré exporte getDynamoDBClient() et resolveTableName(). Lorsque LOCAL_DEV=true (défini automatiquement par les cibles dev), ceux-ci se connectent à DynamoDB Local ; sinon, ils se connectent à AWS et résolvent le nom de la table déployée depuis la Configuration Runtime. Nous modéliserons nos entités Game et Inventory dans ce projet dans le Module 2.
Pour plus de détails, consultez le guide du générateur ts#dynamodb.
Créer l’interface utilisateur (UI)
Section intitulée « Créer l’interface utilisateur (UI) »Ensuite, nous allons créer l’interface utilisateur qui vous permettra d’interagir avec le jeu.
Interface de jeu : site web
Section intitulée « Interface de jeu : site web »Pour créer l’interface utilisateur, créez un site web appelé GameUI en suivant ces étapes :
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-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - ts#website - Remplissez les paramètres requis
- name: GameUI
- ux: shadcn
- Cliquez sur
Generate
Vous verrez de nouveaux fichiers apparaître dans votre arborescence de fichiers.
Examiner en détail les fichiers générés par ts#website
Le ts#website génère ces fichiers. Examinons certains des fichiers clés mis en évidence dans l’arborescence :
Répertoirepackages/
Répertoirecommon/
Répertoireconstructs/
Répertoiresrc/
Répertoireapp/ constructs cdk spécifiques à l’application
Répertoirestatic-websites/
- game-ui.ts construct cdk pour créer votre interface de jeu
Répertoirecore/
- static-website.ts construct générique de site web statique
Répertoiregame-ui/
Répertoirepublic/
- …
Répertoiresrc/
Répertoirecomponents/
RépertoireAppLayout/
- index.tsx mise en page globale utilisant shadcn
SidebarProvider+ en-tête
- index.tsx mise en page globale utilisant shadcn
- app-sidebar.tsx barre latérale shadcn par défaut avec éléments de navigation
- alert.tsx, spinner.tsx primitives de feedback encapsulées dans shadcn
Répertoireroutes/ routes basées sur les fichiers @tanstack/react-router
- index.tsx page racine ’/’
- __root.tsx toutes les pages utilisent ce composant comme base
- config.ts
- main.tsx point d’entrée React
- routeTree.gen.ts mis à jour automatiquement par @tanstack/react-router
- styles.css importe les globaux shadcn partagés (Tailwind v4)
- index.html
- project.json
- vite.config.mts
- …
Répertoirecommon/
Répertoireshadcn/ bibliothèque shadcn/ui partagée (jetons de thème,
Button,Card,Input,Sidebar, …) importée par chaque site webux=shadcn- src/components/ui/*
- src/styles/globals.css jetons 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, ), ), }); }}Il s’agit du construct CDK qui définit notre GameUI. Il a déjà configuré le chemin vers le bundle généré pour notre interface basée sur Vite. Cela signifie qu’au moment du build, le bundling se produit dans la cible de build du projet game-ui et la sortie est utilisée ici.
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>, );Il s’agit du point d’entrée où React est monté. Le style provient des jetons Tailwind v4 importés via styles.css. @tanstack/react-router est configuré en mode routage basé sur les fichiers : tant que le serveur de développement est en cours d’exécution, tout fichier que vous créez sous routes/ est automatiquement pris en compte et l’arbre de routes est régénéré. Les générateurs ultérieurs (auth, connection) vont patcher ce fichier via AST pour envelopper <App /> dans des fournisseurs supplémentaires.
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 composant sera rendu lors de la navigation vers la route /. @tanstack/react-router gérera la Route pour vous chaque fois que vous créez/déplacez ce fichier (tant que le serveur de développement est en cours d’exécution).
Interface de jeu : authentification
Section intitulée « Interface de jeu : authentification »Configurons notre interface de jeu pour exiger un accès authentifié via Amazon Cognito en suivant ces étapes :
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-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - ts#website#auth - Remplissez les paramètres requis
- cognitoDomain: game-ui
- project: @dungeon-adventure/game-ui
- allowSignup: true
- Cliquez sur
Generate
Vous verrez de nouveaux fichiers apparaître/changer dans votre arborescence de fichiers.
Examiner en détail les fichiers générés par ts#website#auth
Le générateur ts#website#auth met à jour/génère ces fichiers. Examinons certains des fichiers clés mis en évidence dans l’arborescence :
Répertoirepackages/
Répertoirecommon/
Répertoireconstructs/
Répertoiresrc/
Répertoirecore/
- user-identity.ts construct cdk pour créer des pools d’utilisateurs/d’identités
Répertoiregame-ui/
Répertoiresrc/
Répertoirecomponents/
RépertoireAppLayout/
- index.tsx ajoute l’utilisateur connecté/déconnexion à l’en-tête
RépertoireCognitoAuth/
- index.tsx gère la connexion à Cognito
RépertoireRuntimeConfig/
- index.tsx récupère le
runtime-config.jsonet le fournit aux enfants via le contexte
- index.tsx récupère le
Répertoirehooks/
- useRuntimeConfig.tsx
- main.tsx mis à jour pour ajouter 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>, );Les composants RuntimeConfigProvider et CognitoAuth ont été ajoutés au fichier main.tsx via une transformation AST. Cela permet au composant CognitoAuth de s’authentifier auprès d’Amazon Cognito en récupérant le runtime-config.json qui contient la configuration de connexion Cognito requise pour effectuer les appels backend vers la bonne destination.
Interface de jeu : connexion à l’API de jeu
Section intitulée « Interface de jeu : connexion à l’API de jeu »Configurons notre interface de jeu pour se connecter à notre API de jeu précédemment créée.
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-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - connection - Remplissez les paramètres requis
- sourceProject: @dungeon-adventure/game-ui
- targetProject: @dungeon-adventure/game-api
- Cliquez sur
Generate
Vous verrez de nouveaux fichiers apparaître/changer dans votre arborescence de fichiers.
Examiner les fichiers de connexion UI → tRPC
Le générateur connection génère/met à jour ces fichiers. Examinons certains des fichiers clés mis en évidence dans l’arborescence :
Répertoirepackages/
Répertoiregame-ui/
Répertoiresrc/
Répertoirecomponents/
- GameApiClientProvider.tsx configure le client GameAPI
Répertoirehooks/
- useGameApi.tsx hooks pour appeler le GameApi
- main.tsx injecte les fournisseurs du client 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;};Ce hook fournit un accès au client tRPC pour appeler le GameApi. Pour des exemples sur la façon d’appeler des API tRPC, consultez le guide d’utilisation du 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>, );Le fichier main.tsx a été mis à jour via une transformation AST pour injecter les fournisseurs tRPC.
Agent Story : connexion au serveur MCP d’inventaire
Section intitulée « Agent Story : connexion au serveur MCP d’inventaire »Connectons notre agent Story au serveur MCP d’inventaire afin que l’agent puisse découvrir et invoquer les outils du serveur 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-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - connection - Remplissez les paramètres requis
- sourceProject: story
- targetProject: inventory
- Cliquez sur
Generate
Examiner les fichiers de connexion Agent Story → MCP d’inventaire
Le générateur connection génère/met à jour ces fichiers :
Répertoirepackages/
Répertoirecommon/
Répertoireagent_connection/
Répertoiredungeon_adventure_agent_connection/
Répertoirecore/
- agentcore_endpoints.py Résolution ARN/URL indépendante du framework
- agentcore_mcp_transport.py Transport MCP indépendant du framework
- agentcore_mcp_client_strands.py Client MCP Strands encapsulant le transport
Répertoireauth/ SigV4 / session-forwarding
httpx.Authindépendant du framework- …
Répertoireapp/
- inventory_mcp_server_client_strands.py Client Strands pour se connecter au serveur MCP d’inventaire
- __init__.py Ré-exporte les clients par connexion
Répertoirestory/
Répertoiredungeon_adventure_story/agent/
- agent.py Modifié pour importer et utiliser le client MCP
Le générateur :
- Crée un projet Python
agent_connectionpartagé (s’il n’existe pas déjà) avec leAgentCoreMCPClientStrandsde base - Génère une classe
InventoryMcpServerClientStrandsqui gère la connexion au serveur MCP localement (HTTP direct) et lors du déploiement (via AgentCore avec authentification IAM) - Transforme
agent.pypour importer le client, créer une instance et connecter les outils du serveur MCP à l’agent - Ajoute le projet
agent_connectioncomme dépendance de l’espace de travail du projet story - Met à jour la cible
devpour démarrer automatiquement le serveur MCP lors de l’exécution locale
Pour plus de détails, consultez le guide de connexion Agent Python vers MCP.
Interface de jeu : connexion à l’agent Story
Section intitulée « Interface de jeu : connexion à l’agent Story »Connectons notre interface de jeu à l’agent Story. Puisque l’agent parle AG-UI, le générateur connection connecte CopilotKit : un composant de chat avec thème et un HttpAgent @ag-ui/client prêt à être rendu.
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-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - connection - Remplissez les paramètres requis
- sourceProject: @dungeon-adventure/game-ui
- targetProject: story
- Cliquez sur
Generate
Examiner les fichiers de connexion UI → Agent Story
Le générateur connection génère/met à jour ces fichiers :
Répertoirepackages/
Répertoiregame-ui/
Répertoiresrc/
Répertoirecomponents/
- AguiProvider.tsx
CopilotKitProvideravec chaque agent AG-UI connecté enregistré Répertoirecopilot/
- index.tsx
CopilotChat/CopilotSidebar/CopilotPopupavec thème Shadcn - ShadcnAssistantMessage.tsx, ShadcnUserMessage.tsx, ShadcnChatInput.tsx, ShadcnCursor.tsx, copilot.css
- index.tsx
- AguiProvider.tsx
Répertoirehooks/
- useAguiStoryAgent.tsx Construit un
HttpAgent, injecte le jeton bearer Cognito et complètethreadIdjusqu’à l’ID de session de 33 caractères d’AgentCore
- useAguiStoryAgent.tsx Construit un
- main.tsx Enveloppe
<App />dans<AguiProvider>
Le générateur :
- Détecte le
uxdu site web React (Shadcn ici) et fournit des composants de chat correspondants. - Enregistre chaque agent connecté sur un seul
CopilotKitProvider— réexécuter pour un autre agent ajoute simplement un autre hook. - Lit l’ARN runtime de l’agent depuis la Configuration Runtime, construit l’URL d’invocation AgentCore et attache le jeton bearer Cognito ainsi que l’en-tête d’ID de session AgentCore.
Pour plus de détails, consultez le guide de connexion React vers AG-UI.
Connecter l’API de jeu et le serveur MCP d’inventaire à la base de données
Section intitulée « Connecter l’API de jeu et le serveur MCP d’inventaire à la base de données »L’API de jeu et le serveur MCP d’inventaire lisent et écrivent dans notre table DynamoDB, alors connectons-les au projet DungeonDb. Le générateur connection détecte que la cible est un projet ts#dynamodb et connecte la cible dev de chaque projet source pour démarrer DynamoDB Local automatiquement.
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-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - connection - Remplissez les paramètres requis
- sourceProject: @dungeon-adventure/game-api
- targetProject: @dungeon-adventure/dungeon-db
- Cliquez sur
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-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - connection - Remplissez les paramètres requis
- sourceProject: @dungeon-adventure/inventory
- targetProject: @dungeon-adventure/dungeon-db
- Cliquez sur
Generate
Interface de jeu : infrastructure
Section intitulée « Interface de jeu : infrastructure »Créons le dernier sous-projet pour l’infrastructure 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-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - ts#infra - Remplissez les paramètres requis
- name: infra
- Cliquez sur
Generate
Vous verrez de nouveaux fichiers apparaître/changer dans votre arborescence de fichiers.
Examiner en détail les fichiers générés par ts#infra
Le générateur ts#infra génère/met à jour ces fichiers. Examinons certains des fichiers clés mis en évidence dans l’arborescence :
Répertoirepackages/
Répertoirecommon/
Répertoireconstructs/
Répertoiresrc/
Répertoirecore/
- checkov.ts
- index.ts
Répertoireinfra
Répertoiresrc/
Répertoirestages/
- application-stage.ts stacks cdk définies ici
Répertoirestacks/
- application-stack.ts ressources cdk définies ici
- main.ts point d’entrée qui définit toutes les étapes
- cdk.json
- checkov.yml
- project.json
- …
- package.json
- tsconfig.json ajouter des références
- tsconfig.base.json ajouter un 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();Il s’agit du point d’entrée de votre application 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 }}Instancions nos constructs CDK pour construire notre jeu d’aventure dans le donjon.
Tâche 3 : Mettre à jour notre infrastructure
Section intitulée « Tâche 3 : Mettre à jour notre infrastructure »Mettons à jour packages/infra/src/stacks/application-stack.ts pour instancier certains de nos constructs générés :
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'); }}Tâche 4 : Construire le code
Section intitulée « Tâche 4 : Construire le code »Commandes Nx
Cibles uniques vs multiples
Section intitulée « Cibles uniques vs multiples »La commande run-many exécutera une cible sur plusieurs sous-projets listés (--all les ciblera tous). Cela garantit que les dépendances sont exécutées dans le bon ordre.
Vous pouvez également déclencher un build (ou toute autre tâche) pour une seule cible de projet en exécutant la cible directement sur le projet. Par exemple, pour construire le projet @dungeon-adventure/infra, exécutez la commande suivante :
pnpm nx build infrayarn nx build infranpx nx build infrabunx nx build infraVous pouvez également omettre la portée et utiliser la syntaxe abrégée Nx si vous préférez :
pnpm nx build infrayarn nx build infranpx nx build infrabunx nx build infraVisualiser vos dépendances
Section intitulée « Visualiser vos dépendances »Pour visualiser vos dépendances, exécutez :
pnpm nx graphyarn nx graphnpx nx graphbunx nx graph
Mise en cache
Section intitulée « Mise en cache »Nx s’appuie sur la mise en cache pour vous permettre de réutiliser les artefacts des builds précédents afin d’accélérer le développement. Une certaine configuration est nécessaire pour que cela fonctionne correctement et il peut y avoir des cas où vous souhaitez effectuer un build sans utiliser le cache. Pour ce faire, ajoutez simplement l’argument --skip-nx-cache à votre commande. Par exemple :
pnpm nx build infra --skip-nx-cacheyarn nx build infra --skip-nx-cachenpx nx build infra --skip-nx-cachebunx nx build infra --skip-nx-cacheSi pour une raison quelconque vous souhaitez vider votre cache (stocké dans le dossier .nx), vous pouvez exécuter la commande suivante :
pnpm nx resetyarn nx resetnpx nx resetbunx nx resetEn utilisant la ligne de commande, exécutez d’abord la commande suivante pour corriger les problèmes de lint :
pnpm lintyarn lintnpm run lintbun lintEnsuite, exécutez la commande suivante pour un build complet :
pnpm buildyarn buildnpm run buildbun buildVous serez invité avec ce qui suit :
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 changesCe message indique que NX a détecté certains fichiers qui peuvent être mis à jour automatiquement pour vous. Dans ce cas, il fait référence aux fichiers tsconfig.json qui n’ont pas de références TypeScript configurées sur les projets référencés.
Sélectionnez l’option Yes, sync the changes and run the tasks pour continuer. Vous devriez remarquer que toutes les erreurs d’import liées à votre IDE sont automatiquement résolues car le générateur de synchronisation ajoutera automatiquement les références TypeScript manquantes !
Tous les artefacts construits sont maintenant disponibles dans le dossier dist/ situé à la racine du monorepo. Il s’agit d’une pratique standard lors de l’utilisation de projets générés par le @aws/nx-plugin car cela ne pollue pas votre arborescence de fichiers avec des fichiers générés. Si vous souhaitez nettoyer vos fichiers, supprimez le dossier dist/ sans vous soucier des artefacts de build dispersés dans l’arborescence de fichiers.
Félicitations ! Vous avez créé tous les sous-projets nécessaires pour commencer à implémenter le cœur de notre jeu d’aventure IA dans le donjon. 🎉🎉🎉