モノレポのセットアップ
タスク1: モノレポの作成
Section titled “タスク1: モノレポの作成”新しいモノレポを作成するには、目的のディレクトリ内で次のコマンドを実行します:
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モノレポがセットアップされます。VSCodeでディレクトリを開くと、次のファイル構造が表示されます:
Directory.nx/
- …
Directory.vscode/
- …
Directorynode_modules/
- …
Directorypackages/ サブプロジェクトが配置される場所
- …
- .gitignore
- biome.json Biomeのリントとフォーマット設定
- nx.json Nx CLIとモノレポのデフォルト設定
- 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サーバー、ゲームデータベース、ウェブサイト)と、それらを接続するコネクションをスキャフォールディングします。これには2つの方法があります:
- クイック — 以下の図からコマンドを直接コピーして実行します。同じ開始点に到達する最速の方法です。
- ステップバイステップ — 以下のセクションを展開して、各ジェネレーターを自分で実行し、それぞれが何を生成するかを正確に確認します。
以下の図は、ダンジョンアドベンチャーワークスペース_そのもの_です: このモジュールで構築するすべてのプロジェクト、コンポーネント、コネクションが含まれています。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ジェネレーターによって生成されたすべてのファイルのリストです。ファイルツリーで強調表示されている主要なファイルのいくつかを確認します:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ アプリ固有のcdkコンストラクト
Directoryapis/
- game-api.ts tRPC APIを作成するためのcdkコンストラクト
- index.ts
- …
- index.ts
Directorycore/ 汎用cdkコンストラクト
Directoryapi/
- 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
- …
Directorygame-api/ tRPC API
Directorysrc/
Directoryclient/ 通常tsマシン間呼び出しに使用されるバニラクライアント
- index.ts
Directorymiddleware/ powertoolsインストルメンテーション
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
Directoryschema/ APIの入力と出力の定義
- index.ts
- echo.ts サンプル入出力スキーマ
- z-async-iterable.ts tRPCサブスクリプション出力用のラッパーZodスキーマ
Directoryprocedures/ 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スキーマ定義は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は次のファイルを生成します:
Directory.venv/ モノレポ用の単一仮想環境
- …
Directorypackages/
Directorystory/
Directorydungeon_adventure_story/ pythonモジュール
- …
Directorytests/
- …
- .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エージェントを追加するには:
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は次のファイルを生成します:
Directorypackages/
Directorystory/
Directorydungeon_adventure_story/ pythonモジュール
Directoryagent/
- main.py Bedrock AgentCore Runtimeでのエージェントのエントリーポイント
- agent.py サンプルエージェントとツールを定義
- session.py 会話状態を永続化するためのSessionManagerを解決
Directorymiddleware/
- session_id_middleware.py リクエストのインバウンドAgentCoreセッションIDをバインド
- Dockerfile AgentCore Runtimeへのデプロイ用のDockerイメージを定義
Directorycommon/constructs/
Directorysrc
Directoryapp/agents/story-agent/
- story-agent.ts Storyエージェントを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エージェントを作成し、減算ツールを定義します。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"}これはエージェントのエントリーポイントです。--protocol=ag-uiを選択したため、ジェネレーターはStrandsのAgentをag_ui_strandsのStrandsAgentでラップし、AG-UIプロトコルを話すFastAPIアプリにマウントします。これがReactウェブサイトからCopilotKitが通信する対象です。エージェントはlifespanハンドラー内で構築されます(インポート時ではありません)。これにより、モジュールインポートではなくコンテナ起動が構築を所有し、各AgentCoreセッションが独自のコンテナを取得します。上で見たSessionIdMiddlewareは、インバウンドAgentCoreランタイムセッションIDを転送するため、後で接続する下流のMCP/A2Aクライアント(モジュール2のInventory MCPサーバーなど)がアウトバウンド呼び出しで自動的に転送します。AG-UIはthread_idごとに1つのStrandsエージェントをキャッシュするため、テンプレートエージェント自身のセッションマネージャーではなくsession_manager_providerをプラグインします。これにより、各スレッドが独自の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`; }}これは、エージェントDockerイメージをECRにアップロードし、AgentCore Runtimeを使用してホストするCDK AgentRuntimeArtifactを設定します。--auth=cognitoを選択したため、コンストラクトはユーザープール/クライアントIDを必要とし、Cognitoを通じてAgentCore Runtime呼び出しを認証し、呼び出し元のAuthorizationヘッダーを転送します。また、Story Agentのsession.pyが実行時に読み取るセッションバケット(CloudWatch Logsに配信されるサーバーアクセスログを持つKMS暗号化S3バケット)をプロビジョニングし、エージェントに読み取り/書き込みアクセスを付与し、Bedrockモデルを呼び出すアクセスを付与し、そのARNとバケット名をRuntimeConfigに登録して、エージェント(実行時、AppConfig経由)とGame API(synth時、invocationUrl経由)の両方が見つけられるようにします。
エージェントソースコードと同じ場所に配置できるように、storyプロジェクトからDockerイメージを参照する追加のDockerfileがあることに気付くかもしれません。
Inventoryツールのセットアップ
Section titled “Inventoryツールのセットアップ”Inventory: TypeScriptプロジェクト
Section titled “Inventory: TypeScriptプロジェクト”Story Agentがプレイヤーのインベントリを管理するためのツールを提供するMCPサーバーを作成しましょう。
まず、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ジェネレーターは次のファイルを生成します。
Directorypackages/
Directoryinventory/
Directorysrc/
- 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ジェネレーターは次のファイルを生成します。
Directorypackages/
Directoryinventory/
Directorysrc/mcp-server/
- index.ts バレルエクスポート
- server.ts MCPサーバーを作成
Directorytools/
- divide.ts サンプルツール
Directoryresources/
- sample-guidance.ts サンプルリソース
- stdio.ts STDIOトランスポートを使用したMCPのエントリーポイント
- http.ts ストリーマブルHTTPトランスポートを使用したMCPのエントリーポイント
- Dockerfile AgentCore Runtimeへのデプロイ用のイメージをビルド
- rolldown.config.ts AgentCore Runtimeへのデプロイ用にMCPサーバーをバンドルするための設定
Directorycommon/constructs/
Directorysrc
Directoryapp/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ジェネレーターは次のファイルを生成します。
Directorypackages/
Directorydungeon-db/
- config.json ポート、テーブル名、コンテナ設定、グローバルセカンダリインデックスを含むDynamoDB設定
Directorysrc/
- index.ts エントリーポイントとエクスポート
- client.ts DynamoDBクライアントシングルトンとテーブル名解決
Directoryentities/
- example.ts サンプルElectroDBエンティティ(これを置き換えます)
- index.ts エンティティエクスポート
- project.json
devとpull-imageターゲットを追加
Directorycommon/
Directoryscripts/
Directorysrc/
Directorydynamodb/
- create-local-table.ts DynamoDB Localにテーブルを作成
- pull-image.ts DynamoDB Localイメージをプル
- start-container.ts DynamoDB Localコンテナを起動
Directoryconstructs/
Directorysrc/
Directoryapp/dynamodb/
- dungeon-db.ts テーブルをプロビジョニングするためのコンストラクト
Directorycore/
- dynamodb.ts 汎用DynamoDBテーブルコンストラクト
生成されたsrc/client.tsはgetDynamoDBClient()とresolveTableName()をエクスポートします。LOCAL_DEV=true(devターゲットによって自動的に設定)の場合、これらはDynamoDB Localに接続します。それ以外の場合は、AWSに接続し、Runtime Configurationからデプロイされたテーブル名を解決します。モジュール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は次のファイルを生成します。ファイルツリーで強調表示されている主要なファイルのいくつかを確認しましょう:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ アプリ固有のcdkコンストラクト
Directorystatic-websites/
- game-ui.ts Game UIを作成するためのcdkコンストラクト
Directorycore/
- static-website.ts 汎用静的ウェブサイトコンストラクト
Directorygame-ui/
Directorypublic/
- …
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.tsx shadcnの
SidebarProvider+ ヘッダーを使用した全体的なページレイアウト
- index.tsx shadcnの
- app-sidebar.tsx ナビゲーション項目を含むデフォルトのshadcnサイドバー
- alert.tsx, spinner.tsx shadcnでラップされたフィードバックプリミティブ
Directoryroutes/ @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
- …
Directorycommon/
Directoryshadcn/ すべての
ux=shadcnウェブサイトによってインポートされる共有shadcn/uiライブラリ(テーマトークン、Button、Card、Input、Sidebarなど)- 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: 認証”Amazon Cognitoを介した認証アクセスを必要とするようにGame UIを設定しましょう:
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ジェネレーターは次のファイルを更新/生成します。ファイルツリーで強調表示されている主要なファイルのいくつかを確認しましょう:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- user-identity.ts ユーザー/IDプールを作成するためのcdkコンストラクト
Directorygame-ui/
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.tsx ログインユーザー/ログアウトをヘッダーに追加
DirectoryCognitoAuth/
- index.tsx Cognitoへのログインを管理
DirectoryRuntimeConfig/
- index.tsx
runtime-config.jsonを取得し、コンテキスト経由で子に提供
- index.tsx
Directoryhooks/
- 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 APIに接続するようにGame UIを設定しましょう。
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ジェネレーターは次のファイルを生成/更新します。ファイルツリーで強調表示されている主要なファイルのいくつかを確認しましょう:
Directorypackages/
Directorygame-ui/
Directorysrc/
Directorycomponents/
- GameApiClientProvider.tsx GameAPIクライアントをセットアップ
Directoryhooks/
- 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サーバーに接続して、エージェントが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ジェネレーターは次のファイルを生成/更新します:
Directorypackages/
Directorycommon/
Directoryagent_connection/
Directorydungeon_adventure_agent_connection/
Directorycore/
- agentcore_endpoints.py フレームワークに依存しないARN/URL解決
- agentcore_mcp_transport.py フレームワークに依存しないMCPトランスポート
- agentcore_mcp_client_strands.py トランスポートをラップするStrands MCPクライアント
Directoryauth/ フレームワークに依存しないSigV4 / セッション転送
httpx.Auth- …
Directoryapp/
- inventory_mcp_server_client_strands.py Inventory MCPサーバーに接続するためのStrandsクライアント
- __init__.py 接続ごとのクライアントを再エクスポート
Directorystory/
Directorydungeon_adventure_story/agent/
- agent.py MCPクライアントをインポートして使用するように変更
ジェネレーターは:
- コアの
AgentCoreMCPClientStrandsを持つ共有agent_connectionPythonプロジェクトを作成します(まだ存在しない場合) - ローカル(直接HTTP)とデプロイ時(IAM認証を使用したAgentCore経由)の両方でMCPサーバーへの接続を処理する
InventoryMcpServerClientStrandsクラスを生成します agent.pyを変換してクライアントをインポートし、インスタンスを作成し、MCPサーバーのツールをエージェントに接続しますagent_connectionプロジェクトをstoryプロジェクトのワークスペース依存関係として追加します- ローカルで実行する際にMCPサーバーを自動的に起動するように
devターゲットを更新します
詳細については、Python AgentからMCP接続ガイドを参照してください。
Game UI: Story Agentへの接続
Section titled “Game UI: Story Agentへの接続”Game UIをStory 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ジェネレーターは次のファイルを生成/更新します:
Directorypackages/
Directorygame-ui/
Directorysrc/
Directorycomponents/
- AguiProvider.tsx 接続されたすべてのAG-UIエージェントが登録された
CopilotKitProvider Directorycopilot/
- index.tsx Shadcnテーマの
CopilotChat/CopilotSidebar/CopilotPopup - ShadcnAssistantMessage.tsx, ShadcnUserMessage.tsx, ShadcnChatInput.tsx, ShadcnCursor.tsx, copilot.css
- index.tsx Shadcnテーマの
- AguiProvider.tsx 接続されたすべてのAG-UIエージェントが登録された
Directoryhooks/
- useAguiStoryAgent.tsx
HttpAgentを構築し、Cognitoベアラートークンを注入し、threadIdをAgentCoreの33文字のセッションIDにパディング
- useAguiStoryAgent.tsx
- main.tsx
<App />を<AguiProvider>でラップ
ジェネレーターは:
- Reactウェブサイトの
ux(ここではShadcn)を検出し、一致するチャットコンポーネントを提供します。 - 単一の
CopilotKitProviderにすべての接続されたエージェントを登録します。別のエージェントに対して再実行すると、別のフックが追加されるだけです。 - Runtime Configurationからエージェントのランタイム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ジェネレーターは次を生成/更新します。ファイルツリーで強調表示されている主要なファイルのいくつかを確認しましょう:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- checkov.ts
- index.ts
Directoryinfra
Directorysrc/
Directorystages/
- application-stage.ts cdkスタックがここで定義されます
Directorystacks/
- 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コマンド
単一vs複数ターゲット
Section titled “単一vs複数ターゲット”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
キャッシング
Section titled “キャッシング”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コマンドラインを使用して、まずリントの問題を修正するために次のコマンドを実行します:
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オプションを選択して続行します。syncジェネレーターが欠落しているTypeScript参照を自動的に追加するため、すべてのIDE関連のインポートエラーが自動的に解決されることに気付くはずです!
すべてのビルドアーティファクトは、モノレポのルートにあるdist/フォルダー内で利用できるようになりました。これは、@aws/nx-pluginによって生成されたプロジェクトを使用する際の標準的な慣行であり、生成されたファイルでファイルツリーを汚染しません。ファイルをクリーンアップしたい場合は、ビルドアーティファクトがファイルツリー全体に散らばることを心配せずにdist/フォルダーを削除してください。
おめでとうございます!AI Dungeon Adventureゲームのコアの実装を開始するために必要なすべてのサブプロジェクトを作成しました。🎉🎉🎉