搭建 monorepo
任务 1:创建 monorepo
Section titled “任务 1:创建 monorepo”要创建一个新的 monorepo,请在您希望的目录中运行以下命令:
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=cdk这将在 dungeon-adventure 目录中创建一个 NX monorepo。在 VSCode 中打开该目录后,您将看到如下文件结构:
文件夹.nx/
- …
文件夹.vscode/
- …
文件夹node_modules/
- …
文件夹packages/ 子项目将存放于此
- …
- .gitignore
- biome.json 配置 Biome 的代码检查与格式化
- nx.json 配置 Nx CLI 和 monorepo 默认值
- package.json 所有 node 依赖均在此定义
- pnpm-lock.yaml 或 bun.lock、yarn.lock、package-lock.json,取决于包管理器
- pnpm-workspace.yaml 使用 pnpm 时存在
- README.md
- tsconfig.base.json 所有基于 node 的子项目均继承此配置
- tsconfig.json
- aws-nx-plugin.config.mts Nx Plugin for AWS 的配置文件
任务 2:搭建地牢冒险游戏
Section titled “任务 2:搭建地牢冒险游戏”有了工作区后,我们来搭建游戏的各个子项目——Game API、Story Agent、Inventory MCP 服务器、游戏数据库和网站——以及将它们连接在一起的配置。有两种方式可以完成:
- 快速方式 — 直接从下方的图表中复制命令并运行。这是到达相同起点的最快方式。
- 逐步方式 — 展开下方的章节,逐个运行每个生成器,并仔细查看每个生成器的产出。
下方的图表_就是_地牢冒险工作区:您将在本模块中构建的每个项目、组件和连接。点击 Copy commands 获取完整命令序列,然后在任务 1 中创建的 dungeon-adventure 目录中运行它们。
逐步方式
您可以逐个运行每个生成器,而不是一次性复制所有命令。这是了解每个生成器为工作区添加了什么内容的最佳方式。请在任务 1 中创建的 dungeon-adventure 目录中依次运行每个生成器。
创建 Game API
Section titled “创建 Game API”首先,让我们创建 Game API。为此,请按照以下步骤创建一个名为 GameApi 的 tRPC API:
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-interactive您还可以执行试运行以查看哪些文件会被更改
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- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#api - 填写必需参数
- name: GameApi
- framework: trpc
- 点击
Generate
您将看到文件树中出现一些新文件。
详细查看生成的 ts#api 文件
以下是 ts#api 生成器生成的所有文件列表。我们将检查文件树中高亮显示的一些关键文件:
文件夹packages/
文件夹common/
文件夹constructs/
文件夹src/
文件夹app/ 应用特定的 cdk 构件
文件夹apis/
- game-api.ts 用于创建 tRPC API 的 cdk 构件
- index.ts
- …
- index.ts
文件夹core/ 通用 cdk 构件
文件夹api/
- rest-api.ts API Gateway Rest API 的基础 cdk 构件
- trpc-utils.ts trpc API CDK 构件的工具函数
- utils.ts API 构件的工具函数
- index.ts
- runtime-config.ts
- index.ts
- project.json
- …
文件夹game-api/ tRPC API
文件夹src/
文件夹client/ 通常用于 ts 机器间调用的普通客户端
- index.ts
文件夹middleware/ powertools 插桩
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
文件夹schema/ API 输入输出的定义
- index.ts
- echo.ts 示例输入输出 schema
- z-async-iterable.ts tRPC 订阅输出的 Zod schema 包装器
文件夹procedures/ API 过程/路由的具体实现
- echo.ts 示例过程实现
- index.ts
- init.ts 设置上下文和中间件
- handler.ts Lambda 处理器入口点(REST API 使用响应流)
- local-server.ts 本地运行 tRPC 服务器时使用
- router.ts 定义 tRPC 路由器和所有过程
- project.json
- …
- vitest.workspace.ts
让我们查看这些关键文件:
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;路由器定义了 API 的 tRPC 路由器,是您声明所有 API 方法的地方。如上所示,我们有一个名为 echo 的方法,其实现位于 ./procedures/echo.ts 文件中。Lambda 处理器入口点位于 handler.ts,由生成器自动配置。
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 }));该文件是 echo 方法的实现,通过声明输入和输出数据结构实现了强类型约束。
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>;所有 tRPC schema 定义均使用 Zod 定义,并通过 z.TypeOf 语法导出为 TypeScript 类型。
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('*', '/*', '*')], }); }}这是定义 GameApi 的 CDK 构件。它提供了一个 defaultIntegrations 方法,该方法会自动为 tRPC API 中的每个过程创建一个 Lambda 函数,并指向已打包的 API 实现。这意味着在 cdk synth 时不会发生打包(与使用 NodeJsFunction 相反),因为我们已经在后端项目的构建目标中完成了打包。
创建 Story Agent
Section titled “创建 Story Agent”现在让我们创建 Story Agent。
Story agent:Python 项目
Section titled “Story agent:Python 项目”创建一个 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-interactive您还可以执行试运行以查看哪些文件会被更改
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- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - py#project - 填写必需参数
- name: story
- 点击
Generate
您将看到文件树中出现一些新文件。
详细查看生成的 py#project 文件
py#project 生成以下文件:
文件夹.venv/ monorepo 的单一虚拟环境
- …
文件夹packages/
文件夹story/
文件夹dungeon_adventure_story/ python 模块
- …
文件夹tests/
- …
- .python-version
- pyproject.toml
- project.json
- .python-version 固定的 uv python 版本
- pyproject.toml
- uv.lock
这配置了一个 Python 项目和具有共享虚拟环境的 UV Workspace。
Story agent
Section titled “Story agent”使用 py#agent 生成器向项目添加 Strands 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-interactive您还可以执行试运行以查看哪些文件会被更改
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- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - py#agent - 填写必需参数
- project: story
- auth: cognito
- protocol: ag-ui
- 点击
Generate
您将看到文件树中出现一些新文件。
详细查看生成的 py#agent 文件
py#agent 生成以下文件:
文件夹packages/
文件夹story/
文件夹dungeon_adventure_story/ python 模块
文件夹agent/
- main.py Bedrock AgentCore Runtime 中 agent 的入口点
- agent.py 定义示例 agent 和工具
- session.py 解析用于持久化对话状态的 SessionManager
文件夹middleware/
- session_id_middleware.py 为请求绑定入站 AgentCore 会话 ID
- Dockerfile 定义部署到 AgentCore Runtime 的 Docker 镜像
文件夹common/constructs/
文件夹src
文件夹app/agents/story-agent/
- story-agent.ts 将 Story agent 部署到 AgentCore Runtime 的构件
让我们详细查看一些文件:
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, )这创建了一个示例 Strands agent 并定义了一个减法工具。log_model_errors 和 log_tool_errors 是来自共享 dungeon_adventure_agent_connection 项目的钩子,用于记录模型/工具失败而不是让它们静默失败。
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)SessionIdMiddleware 将入站 AgentCore 运行时会话 ID 绑定到请求期间的 ContextVar 上。
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"}这是 agent 的入口点。由于我们选择了 --protocol=ag-ui,生成器将我们的 Strands Agent 用 ag_ui_strands 中的 StrandsAgent 包装,并将其挂载到一个使用 AG-UI 协议 的 FastAPI 应用上——这就是 CopilotKit 从 React 网站与之通信的方式。agent 在 lifespan 处理器中构建,而非在导入时,因此容器启动(而非模块导入)拥有构建所有权,每个 AgentCore 会话都有自己的容器。上面看到的 SessionIdMiddleware 转发入站 AgentCore 运行时会话 ID,以便我们稍后连接的任何下游 MCP/A2A 客户端(例如 模块 2 中的 Inventory MCP 服务器)在其出站调用中自动转发它。由于 AG-UI 为每个 thread_id 缓存一个 Strands agent,我们插入了一个 session_manager_provider 而不是模板 agent 自己的会话管理器——这为每个线程提供了自己的 SessionManager,使对话历史在轮次间持久化。get_session_manager() 函数来自生成的 session.py 同级文件:部署时,它返回一个由生成器自动配置的 S3 存储桶支持的 strands.session.S3SessionManager;在 agent-dev(LOCAL_DEV=true)下,它始终返回一个写入本地临时目录的 FileSessionManager,而不管部署配置如何。
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`; }}这配置了一个 CDK AgentRuntimeArtifact,将您的 agent Docker 镜像上传到 ECR,并使用 AgentCore Runtime 托管它。由于我们选择了 --auth=cognito,该构件需要用户池/客户端身份,并通过 Cognito 授权 AgentCore Runtime 调用,转发调用者的 Authorization 标头。它还配置了 Story Agent 的 session.py 在运行时读取的会话存储桶——一个使用 KMS 加密的 S3 存储桶,服务器访问日志传送到 CloudWatch Logs——授予 agent 对其的读写访问权限,授予其调用 Bedrock 模型的权限,并在 RuntimeConfig 中注册其 ARN 和存储桶名称,以便 agent(运行时通过 AppConfig)和 Game API(合成时通过 invocationUrl)都能找到它。
您可能会注意到一个额外的 Dockerfile,它引用了 story 项目中的 Docker 镜像,允许我们将 Dockerfile 和 agent 源代码放在一起。
设置 Inventory 工具
Section titled “设置 Inventory 工具”Inventory:TypeScript 项目
Section titled “Inventory:TypeScript 项目”让我们创建一个 MCP 服务器,为 Story Agent 提供管理玩家库存的工具。
首先,我们创建一个 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-interactive您还可以执行试运行以查看哪些文件会被更改
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- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#project - 填写必需参数
- name: inventory
- 点击
Generate
这将创建一个空的 TypeScript 项目。
详细查看生成的 ts#project 文件
ts#project 生成器生成以下文件。
文件夹packages/
文件夹inventory/
文件夹src/
- index.ts 包含示例函数的入口点
- project.json 项目配置
- vitest.config.mts 测试配置
- tsconfig.json 项目的基础 TypeScript 配置
- tsconfig.lib.json 针对编译和打包的项目 TypeScript 配置
- tsconfig.spec.json 测试的 TypeScript 配置
- tsconfig.base.json 已更新,为其他项目配置引用此项目的别名
Inventory:MCP 服务器
Section titled “Inventory:MCP 服务器”接下来,我们将向 TypeScript 项目添加一个 MCP 服务器:
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-interactive您还可以执行试运行以查看哪些文件会被更改
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- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#mcp-server - 填写必需参数
- project: inventory
- 点击
Generate
这将添加一个 MCP 服务器。
详细查看生成的 ts#mcp-server 文件
ts#mcp-server 生成器生成以下文件。
文件夹packages/
文件夹inventory/
文件夹src/mcp-server/
- index.ts 桶形导出
- server.ts 创建 MCP 服务器
文件夹tools/
- divide.ts 示例工具
文件夹resources/
- sample-guidance.ts 示例资源
- stdio.ts 使用 STDIO 传输的 MCP 入口点
- http.ts 使用 Streamable HTTP 传输的 MCP 入口点
- Dockerfile 构建部署到 AgentCore Runtime 的镜像
- rolldown.config.ts 用于将 MCP 服务器打包以部署到 AgentCore 的配置
文件夹common/constructs/
文件夹src
文件夹app/mcp-servers/inventory-mcp-server/
- inventory-mcp-server.ts 将 inventory MCP 服务器部署到 AgentCore Runtime 的构件
创建游戏数据库
Section titled “创建游戏数据库”我们的游戏状态——已保存的游戏和每个玩家的库存——存储在 Amazon DynamoDB 中。使用 ts#dynamodb 生成器创建一个名为 DungeonDb 的 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-interactive您还可以执行试运行以查看哪些文件会被更改
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- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#dynamodb - 填写必需参数
- name: DungeonDb
- 点击
Generate
您将看到文件树中出现一些新文件。
详细查看生成的 ts#dynamodb 文件
ts#dynamodb 生成器生成以下文件。
文件夹packages/
文件夹dungeon-db/
- config.json DynamoDB 配置,包括端口、表名、容器设置和全局二级索引
文件夹src/
- index.ts 入口点和导出
- client.ts DynamoDB 客户端单例和表名解析
文件夹entities/
- example.ts 示例 ElectroDB 实体(我们将替换它)
- index.ts 实体导出
- project.json 添加
dev和pull-image目标
文件夹common/
文件夹scripts/
文件夹src/
文件夹dynamodb/
- create-local-table.ts 在 DynamoDB Local 中创建表
- pull-image.ts 拉取 DynamoDB Local 镜像
- start-container.ts 启动 DynamoDB Local 容器
文件夹constructs/
文件夹src/
文件夹app/dynamodb/
- dungeon-db.ts 用于配置表的构件
文件夹core/
- dynamodb.ts 通用 DynamoDB 表构件
生成的 src/client.ts 导出 getDynamoDBClient() 和 resolveTableName()。当 LOCAL_DEV=true(由 dev 目标自动设置)时,这些函数连接到 DynamoDB Local;否则它们连接到 AWS 并从运行时配置解析已部署的表名。我们将在模块 2 中在此项目中建模我们的 Game 和 Inventory 实体。
有关更多详情,请参阅 ts#dynamodb 生成器指南。
创建用户界面(UI)
Section titled “创建用户界面(UI)”接下来,我们将创建允许您与游戏交互的 UI。
Game UI:网站
Section titled “Game UI:网站”要创建 UI,请按照以下步骤创建一个名为 GameUI 的网站:
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-interactive您还可以执行试运行以查看哪些文件会被更改
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- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#website - 填写必需参数
- name: GameUI
- ux: shadcn
- 点击
Generate
您将看到文件树中出现一些新文件。
详细查看生成的 ts#website 文件
ts#website 生成以下文件。让我们检查文件树中高亮显示的一些关键文件:
文件夹packages/
文件夹common/
文件夹constructs/
文件夹src/
文件夹app/ 应用特定的 cdk 构件
文件夹static-websites/
- game-ui.ts 用于创建 Game UI 的 cdk 构件
文件夹core/
- static-website.ts 通用静态网站构件
文件夹game-ui/
文件夹public/
- …
文件夹src/
文件夹components/
文件夹AppLayout/
- index.tsx 使用 shadcn
SidebarProvider+ 标头的整体页面布局
- index.tsx 使用 shadcn
- app-sidebar.tsx 带导航项的默认 shadcn 侧边栏
- alert.tsx, spinner.tsx shadcn 包装的反馈原语
文件夹routes/ @tanstack/react-router 基于文件的路由
- index.tsx 根 ’/’ 页面
- __root.tsx 所有页面都以此组件为基础
- config.ts
- main.tsx React 入口点
- routeTree.gen.ts 由 @tanstack/react-router 自动更新
- styles.css 导入共享 shadcn 全局样式(Tailwind v4)
- index.html
- project.json
- vite.config.mts
- …
文件夹common/
文件夹shadcn/ 共享 shadcn/ui 库(主题令牌、
Button、Card、Input、Sidebar……),由每个ux=shadcn网站导入- src/components/ui/*
- src/styles/globals.css 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, ), ), }); }}这是定义 GameUI 的 CDK 构件。它已经配置了指向我们基于 Vite 的 UI 生成包的文件路径。这意味着在 build 时,打包发生在 game-ui 项目的构建目标中,输出在此处使用。
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>, );这是挂载 React 的入口点。样式来自通过 styles.css 导入的 Tailwind v4 令牌。@tanstack/react-router 配置为基于文件的路由模式:只要开发服务器正在运行,您在 routes/ 下创建的任何文件都会被自动拾取,路由树会重新生成。后续生成器(auth、connection)将通过 AST 补丁修改此文件,将 <App /> 包装在额外的提供者中。
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> );}导航到 / 路由时将渲染此组件。只要您创建/移动此文件(只要开发服务器正在运行),@tanstack/react-router 就会为您管理 Route。
Game UI:认证
Section titled “Game UI:认证”让我们使用以下步骤配置 Game UI,通过 Amazon Cognito 要求经过身份验证的访问:
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-interactive您还可以执行试运行以查看哪些文件会被更改
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- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#website#auth - 填写必需参数
- cognitoDomain: game-ui
- project: @dungeon-adventure/game-ui
- allowSignup: true
- 点击
Generate
您将看到文件树中出现/更改一些新文件。
详细查看生成的 ts#website#auth 文件
ts#website#auth 生成器更新/生成以下文件。让我们检查文件树中高亮显示的一些关键文件:
文件夹packages/
文件夹common/
文件夹constructs/
文件夹src/
文件夹core/
- user-identity.ts 用于创建用户/身份池的 cdk 构件
文件夹game-ui/
文件夹src/
文件夹components/
文件夹AppLayout/
- index.tsx 在标头中添加已登录用户/注销功能
文件夹CognitoAuth/
- index.tsx 管理登录到 Cognito
文件夹RuntimeConfig/
- index.tsx 获取
runtime-config.json并通过上下文提供给子组件
- index.tsx 获取
文件夹hooks/
- useRuntimeConfig.tsx
- main.tsx 已更新以添加 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>, );RuntimeConfigProvider 和 CognitoAuth 组件已通过 AST 转换添加到 main.tsx 文件中。这允许 CognitoAuth 组件通过获取包含所需 Cognito 连接配置的 runtime-config.json 来向 Amazon Cognito 进行身份验证,以便向正确的目标发出后端调用。
Game UI:连接到 Game API
Section titled “Game UI:连接到 Game API”让我们配置 Game UI 以连接到我们之前创建的 Game API。
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-interactive您还可以执行试运行以查看哪些文件会被更改
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- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - connection - 填写必需参数
- sourceProject: @dungeon-adventure/game-ui
- targetProject: @dungeon-adventure/game-api
- 点击
Generate
您将看到文件树中出现/更改一些新文件。
查看 UI → tRPC 连接文件
connection 生成器生成/更新以下文件。让我们检查文件树中高亮显示的一些关键文件:
文件夹packages/
文件夹game-ui/
文件夹src/
文件夹components/
- GameApiClientProvider.tsx 设置 GameAPI 客户端
文件夹hooks/
- useGameApi.tsx 调用 GameApi 的钩子
- main.tsx 注入 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;};此钩子提供对用于调用 GameApi 的 tRPC 客户端的访问。有关如何调用 tRPC API 的示例,请参阅 使用 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>, );main.tsx 文件已通过 AST 转换更新,注入了 tRPC 提供者。
Story Agent:连接到 Inventory MCP 服务器
Section titled “Story Agent:连接到 Inventory MCP 服务器”让我们将 Story Agent 连接到 Inventory MCP 服务器,使 agent 能够发现并调用 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-interactive您还可以执行试运行以查看哪些文件会被更改
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- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - connection - 填写必需参数
- sourceProject: story
- targetProject: inventory
- 点击
Generate
查看 Story Agent → Inventory MCP 连接文件
connection 生成器生成/更新以下文件:
文件夹packages/
文件夹common/
文件夹agent_connection/
文件夹dungeon_adventure_agent_connection/
文件夹core/
- agentcore_endpoints.py 框架无关的 ARN/URL 解析
- agentcore_mcp_transport.py 框架无关的 MCP 传输
- agentcore_mcp_client_strands.py 包装传输的 Strands MCP 客户端
文件夹auth/ 框架无关的 SigV4 / 会话转发
httpx.Auth- …
文件夹app/
- inventory_mcp_server_client_strands.py 连接到 Inventory MCP 服务器的 Strands 客户端
- __init__.py 重新导出每个连接的客户端
文件夹story/
文件夹dungeon_adventure_story/agent/
- agent.py 已修改以导入和使用 MCP 客户端
生成器:
- 创建一个共享的
agent_connectionPython 项目(如果尚不存在),包含核心AgentCoreMCPClientStrands - 生成一个
InventoryMcpServerClientStrands类,处理本地(直接 HTTP)和部署时(通过带 IAM 认证的 AgentCore)连接到 MCP 服务器 - 转换
agent.py以导入客户端、创建实例,并将 MCP 服务器的工具连接到 agent - 将
agent_connection项目添加为 story 项目的工作区依赖 - 更新
dev目标以在本地运行时自动启动 MCP 服务器
有关更多详情,请参阅 Python Agent 到 MCP 连接指南。
Game UI:连接到 Story Agent
Section titled “Game UI:连接到 Story Agent”让我们将 Game UI 连接到 Story Agent。由于 agent 使用 AG-UI 协议,connection 生成器会连接 CopilotKit:一个主题化的聊天组件和一个准备好渲染的 @ag-ui/client HttpAgent。
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-interactive您还可以执行试运行以查看哪些文件会被更改
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- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - connection - 填写必需参数
- sourceProject: @dungeon-adventure/game-ui
- targetProject: story
- 点击
Generate
查看 UI → Story Agent 连接文件
connection 生成器生成/更新以下文件:
文件夹packages/
文件夹game-ui/
文件夹src/
文件夹components/
- AguiProvider.tsx 注册了所有已连接 AG-UI agent 的
CopilotKitProvider 文件夹copilot/
- index.tsx Shadcn 主题的
CopilotChat/CopilotSidebar/CopilotPopup - ShadcnAssistantMessage.tsx, ShadcnUserMessage.tsx, ShadcnChatInput.tsx, ShadcnCursor.tsx, copilot.css
- index.tsx Shadcn 主题的
- AguiProvider.tsx 注册了所有已连接 AG-UI agent 的
文件夹hooks/
- useAguiStoryAgent.tsx 构建
HttpAgent,注入 Cognito 承载令牌,并将threadId填充到 AgentCore 的 33 字符会话 ID
- useAguiStoryAgent.tsx 构建
- main.tsx 将
<App />包装在<AguiProvider>中
生成器:
- 检测 React 网站的
ux(此处为 Shadcn)并提供匹配的聊天组件。 - 在单个
CopilotKitProvider上注册每个已连接的 agent——为另一个 agent 重新运行只需添加另一个钩子。 - 从运行时配置读取 agent 的运行时 ARN,构建 AgentCore 调用 URL,并附加 Cognito 承载令牌和 AgentCore 会话 ID 标头。
有关更多详情,请参阅 React 到 AG-UI 连接指南。
将 Game API 和 Inventory MCP 服务器连接到数据库
Section titled “将 Game API 和 Inventory MCP 服务器连接到数据库”Game API 和 Inventory MCP 服务器都读写我们的 DynamoDB 表,因此让我们将它们连接到 DungeonDb 项目。connection 生成器检测到目标是 ts#dynamodb 项目,并将每个源项目的 dev 目标配置为自动启动 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-interactive您还可以执行试运行以查看哪些文件会被更改
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- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - connection - 填写必需参数
- sourceProject: @dungeon-adventure/game-api
- targetProject: @dungeon-adventure/dungeon-db
- 点击
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-interactive您还可以执行试运行以查看哪些文件会被更改
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- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - connection - 填写必需参数
- sourceProject: @dungeon-adventure/inventory
- targetProject: @dungeon-adventure/dungeon-db
- 点击
Generate
Game UI:基础设施
Section titled “Game UI:基础设施”让我们为 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-interactive您还可以执行试运行以查看哪些文件会被更改
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- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#infra - 填写必需参数
- name: infra
- 点击
Generate
您将看到文件树中出现/更改一些新文件。
详细查看生成的 ts#infra 文件
ts#infra 生成器生成/更新以下内容。让我们检查文件树中高亮显示的一些关键文件:
文件夹packages/
文件夹common/
文件夹constructs/
文件夹src/
文件夹core/
- checkov.ts
- index.ts
文件夹infra
文件夹src/
文件夹stages/
- application-stage.ts 在此定义 cdk 堆栈
文件夹stacks/
- application-stack.ts 在此定义 cdk 资源
- main.ts 定义所有阶段的入口点
- cdk.json
- checkov.yml
- project.json
- …
- package.json
- tsconfig.json 添加引用
- tsconfig.base.json 添加别名
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();这是您的 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 }}让我们实例化我们的 CDK 构件来构建地牢冒险游戏。
任务 3:更新基础设施
Section titled “任务 3:更新基础设施”让我们更新 packages/infra/src/stacks/application-stack.ts 以实例化一些生成的构件:
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'); }}任务 4:构建代码
Section titled “任务 4:构建代码”Nx 命令
单目标与多目标
Section titled “单目标与多目标”run-many 命令将在多个列出的子项目上运行目标(--all 将针对所有项目)。这确保依赖项以正确的顺序执行。
您也可以通过直接在项目上运行目标来触发单个项目目标的构建(或任何其他任务)。例如,要构建 @dungeon-adventure/infra 项目,请运行以下命令:
pnpm nx build infrayarn nx build infranpx nx build infrabunx nx build infra您也可以省略范围,如果您愿意,可以使用 Nx 简写语法:
pnpm nx build infrayarn nx build infranpx nx build infrabunx nx build infra可视化依赖关系
Section titled “可视化依赖关系”要可视化您的依赖关系,请运行:
pnpm nx graphyarn nx graphnpx nx graphbunx nx graph
Nx 依赖缓存,以便您可以重用之前构建的产物来加速开发。需要一些配置才能使其正常工作,在某些情况下您可能希望在不使用缓存的情况下执行构建。为此,只需在命令中附加 --skip-nx-cache 参数。例如:
pnpm nx build infra --skip-nx-cacheyarn nx build infra --skip-nx-cachenpx nx build infra --skip-nx-cachebunx nx build infra --skip-nx-cache如果出于任何原因您想清除缓存(存储在 .nx 文件夹中),可以运行以下命令:
pnpm nx resetyarn nx resetnpx nx resetbunx nx reset使用命令行,首先运行以下命令修复任何 lint 问题:
pnpm lintyarn lintnpm run lintbun lint然后,运行以下命令进行完整构建:
pnpm buildyarn buildnpm run buildbun build您将看到以下提示:
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 changes此消息表明 NX 检测到一些可以自动为您更新的文件。在这种情况下,它指的是没有在引用项目上设置 TypeScript 引用的 tsconfig.json 文件。
选择 Yes, sync the changes and run the tasks 选项继续。您应该会注意到所有与 IDE 相关的导入错误都会自动解决,因为同步生成器会自动添加缺失的 TypeScript 引用!
所有构建产物现在都可以在位于 monorepo 根目录的 dist/ 文件夹中找到。这是使用 @aws/nx-plugin 生成的项目时的标准做法,因为它不会用生成的文件污染您的文件树。如果您想清理文件,只需删除 dist/ 文件夹,无需担心构建产物散落在整个文件树中。
恭喜!您已经创建了开始实现 AI 地牢冒险游戏核心所需的所有子项目。🎉🎉🎉