@aws/nx-plugin-mcp 1.0.0-rc.70 → 1.0.0-rc.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -198,6 +198,7 @@ import {
198
198
  FunctionProps,
199
199
  Tracing,
200
200
  } from 'aws-cdk-lib/aws-lambda';
201
+ import { RuntimeConfig } from '../../core/runtime-config.js';
201
202
  import {
202
203
  AuthorizationType,
203
204
  LambdaIntegration,
@@ -213,6 +214,7 @@ import {
213
214
  Grant,
214
215
  } from 'aws-cdk-lib/aws-iam';
215
216
  import {
217
+ ApiIntegrations,
216
218
  IntegrationBuilder,
217
219
  RestApiIntegration,
218
220
  } from '../../core/api/utils.js';
@@ -230,12 +232,18 @@ type Operations = Procedures<AppRouter>;
230
232
  * @template TIntegrations - Map of operation names to their integrations
231
233
  */
232
234
  export interface GameApiProps<
233
- TIntegrations extends Record<Operations, RestApiIntegration>,
235
+ TIntegrations extends ApiIntegrations<Operations, RestApiIntegration>,
234
236
  > {
235
237
  /**
236
238
  * Map of operation names to their API Gateway integrations
237
239
  */
238
240
  integrations: TIntegrations;
241
+ /**
242
+ * Whether to enable AWS WAFv2 with the default managed ruleset on the API's default stage.
243
+ *
244
+ * @default true
245
+ */
246
+ enableWaf?: boolean;
239
247
  }
240
248
 
241
249
  /**
@@ -244,7 +252,7 @@ export interface GameApiProps<
244
252
  * @template TIntegrations - Map of operation names to their integrations
245
253
  */
246
254
  export class GameApi<
247
- TIntegrations extends Record<Operations, RestApiIntegration>,
255
+ TIntegrations extends ApiIntegrations<Operations, RestApiIntegration>,
248
256
  > extends RestApi<Operations, TIntegrations> {
249
257
  private allowedOrigins: readonly string[] = ['*'];
250
258
 
@@ -256,7 +264,9 @@ export class GameApi<
256
264
  * @returns An IntegrationBuilder with default lambda integrations
257
265
  */
258
266
  public static defaultIntegrations = (scope: Construct) => {
267
+ const rc = RuntimeConfig.ensure(scope);
259
268
  return IntegrationBuilder.rest({
269
+ pattern: 'isolated',
260
270
  operations: routerToOperations(appRouter),
261
271
  defaultIntegrationOptions: {
262
272
  runtime: Runtime.NODEJS_LATEST,
@@ -274,6 +284,8 @@ export class GameApi<
274
284
  } as FunctionProps,
275
285
  buildDefaultIntegration: (op, props: FunctionProps) => {
276
286
  const handler = new Function(scope, `GameApi${op}Handler`, props);
287
+ handler.addEnvironment('RUNTIME_CONFIG_APP_ID', rc.appConfigApplicationId);
288
+ rc.grantReadAppConfig(handler);
277
289
  return {
278
290
  handler,
279
291
  integration: new LambdaIntegration(handler, {
@@ -439,6 +451,7 @@ The `py#agent` generates these files:
439
451
  - agent/
440
452
  - main.py entrypoint for your agent in Bedrock AgentCore Runtime
441
453
  - agent.py defines an example agent and tools
454
+ - session.py resolves a SessionManager for persisting conversation state
442
455
  - Dockerfile defines the docker image for deployment to AgentCore Runtime
443
456
  - common/constructs/
444
457
  - src
@@ -454,6 +467,7 @@ from contextlib import contextmanager
454
467
 
455
468
  from strands import Agent, tool
456
469
  from strands_tools import current_time
470
+ from dungeon_adventure_agent_connection import log_model_errors, log_tool_errors
457
471
 
458
472
 
459
473
  @tool
@@ -465,17 +479,18 @@ def subtract(a: int, b: int) -> int:
465
479
  def get_agent():
466
480
  yield Agent(
467
481
  name="StoryAgent",
468
- description="StoryAgent Agent",
482
+ description="StoryAgent Strands Agent",
469
483
  system_prompt="""
470
484
  You are a mathematical wizard.
471
485
  Use your tools for mathematical tasks.
472
486
  Refer to tools as your 'spellbook'.
473
487
  """,
474
488
  tools=[subtract, current_time],
489
+ hooks=[log_model_errors, log_tool_errors],
475
490
  )
476
491
  ```
477
492
 
478
- This creates an example Strands agent and defines a subtraction tool.
493
+ This creates an example Strands agent and defines a subtraction tool. `log_model_errors` and `log_tool_errors` are hooks from the shared `dungeon_adventure_agent_connection` project that log model/tool failures instead of letting them fail silently.
479
494
 
480
495
  ```python
481
496
  # agent/main.py
@@ -485,7 +500,7 @@ from contextlib import asynccontextmanager
485
500
 
486
501
  from ag_ui.core import EventType, RunAgentInput, RunErrorEvent
487
502
  from ag_ui.encoder import EventEncoder
488
- from ag_ui_strands import StrandsAgent
503
+ from ag_ui_strands import StrandsAgent, StrandsAgentConfig
489
504
  from dungeon_adventure_agent_connection import get_current_session_id, session_id_context
490
505
  from fastapi import FastAPI, Request
491
506
  from fastapi.middleware.cors import CORSMiddleware
@@ -493,6 +508,7 @@ from fastapi.responses import StreamingResponse
493
508
  from starlette.middleware.base import BaseHTTPMiddleware
494
509
 
495
510
  from .agent import get_agent
511
+ from .session import get_session_manager
496
512
 
497
513
  logging.basicConfig(level=logging.INFO)
498
514
 
@@ -506,6 +522,9 @@ async def lifespan(app: FastAPI):
506
522
  agent=agent,
507
523
  name="StoryAgent",
508
524
  description="A Strands Agent exposed via the AG-UI protocol.",
525
+ # A per-thread session manager, not the template Agent's own, since
526
+ # AG-UI caches one Strands agent per thread_id.
527
+ config=StrandsAgentConfig(session_manager_provider=lambda _input_data: get_session_manager()),
509
528
  )
510
529
  yield
511
530
 
@@ -570,14 +589,27 @@ async def ping():
570
589
  return {"status": "healthy"}
571
590
  ```
572
591
 
573
- This is the entrypoint for the agent. Because we selected `--protocol=ag-ui`, the generator wraps our Strands `Agent` with `StrandsAgent` from [`ag_ui_strands`](https://docs.copilotkit.ai/aws-strands/integration) and mounts it on a FastAPI app that speaks the [AG-UI protocol](https://docs.copilotkit.ai/aws-strands/protocol) — this is what CopilotKit will talk to from the React website. The agent is built inside a `lifespan` handler — not at import time — so container startup, not module import, owns construction, and each AgentCore session gets its own container. The `_SessionIdMiddleware` binds the inbound AgentCore runtime session ID onto a `ContextVar` so any downstream MCP/A2A client we wire up later (e.g. the Inventory MCP server in <Link path="get_started/tutorials/dungeon-game/2">Module 2</Link>) automatically forwards it on its outbound calls. In <Link path="get_started/tutorials/dungeon-game/3">Module 3</Link> we'll also add a `session_manager_provider` so each thread id gets its own `S3SessionManager` and conversation history persists across turns.
592
+ This is the entrypoint for the agent. Because we selected `--protocol=ag-ui`, the generator wraps our Strands `Agent` with `StrandsAgent` from [`ag_ui_strands`](https://docs.copilotkit.ai/aws-strands/integration) and mounts it on a FastAPI app that speaks the [AG-UI protocol](https://docs.copilotkit.ai/aws-strands/protocol) — this is what CopilotKit will talk to from the React website. The agent is built inside a `lifespan` handler — not at import time — so container startup, not module import, owns construction, and each AgentCore session gets its own container. The `_SessionIdMiddleware` binds the inbound AgentCore runtime session ID onto a `ContextVar` so any downstream MCP/A2A client we wire up later (e.g. the Inventory MCP server in <Link path="get_started/tutorials/dungeon-game/2">Module 2</Link>) automatically forwards it on its outbound calls. Since AG-UI caches one Strands agent per `thread_id`, we plug in a `session_manager_provider` rather than the template agent's own session manager — this gives each thread its own `SessionManager` so conversation history persists across turns. That `get_session_manager()` function comes from a generated `session.py` sibling: deployed, it returns a `strands.session.S3SessionManager` backed by an S3 bucket the generator provisions automatically; under `agent-dev` (`LOCAL_DEV=true`) it always returns a `FileSessionManager` writing to a local temp directory instead, regardless of deployed configuration.
574
593
 
575
594
  ```ts
576
- // common/constructs/src/app/agents/story-agent.ts
577
- import { Lazy, Names } from 'aws-cdk-lib';
595
+ // common/constructs/src/app/agents/story-agent/story-agent.ts
596
+ import { Fn, Lazy, Names, RemovalPolicy, Stack } from 'aws-cdk-lib';
578
597
  import { Platform } from 'aws-cdk-lib/aws-ecr-assets';
598
+ import { Connections, IConnectable } from 'aws-cdk-lib/aws-ec2';
599
+ import {
600
+ BlockPublicAccess,
601
+ Bucket,
602
+ BucketEncryption,
603
+ } from 'aws-cdk-lib/aws-s3';
604
+ import { Key } from 'aws-cdk-lib/aws-kms';
605
+ import {
606
+ CfnDelivery,
607
+ CfnDeliveryDestination,
608
+ CfnDeliverySource,
609
+ LogGroup,
610
+ RetentionDays,
611
+ } from 'aws-cdk-lib/aws-logs';
579
612
  import { Construct } from 'constructs';
580
- import { execSync } from 'child_process';
581
613
  import * as path from 'path';
582
614
  import * as url from 'url';
583
615
  import {
@@ -585,31 +617,177 @@ import {
585
617
  ProtocolType,
586
618
  Runtime,
587
619
  RuntimeProps,
620
+ RuntimeAuthorizerConfiguration,
588
621
  } from 'aws-cdk-lib/aws-bedrockagentcore';
589
- import { IGrantable, IPrincipal } from 'aws-cdk-lib/aws-iam';
622
+ import {
623
+ PolicyStatement,
624
+ Effect,
625
+ ServicePrincipal,
626
+ IGrantable,
627
+ IPrincipal,
628
+ } from 'aws-cdk-lib/aws-iam';
629
+ import { IUserPool, IUserPoolClient } from 'aws-cdk-lib/aws-cognito';
630
+ import { suppressRules } from '../../../core/checkov.js';
631
+ import { RuntimeConfig } from '../../../core/runtime-config.js';
632
+ import { findWorkspaceRoot } from '../../../core/workspace.js';
590
633
 
591
634
  export type StoryAgentProps = Omit<
592
635
  RuntimeProps,
593
- 'runtimeName' | 'protocolConfiguration' | 'agentRuntimeArtifact'
594
- >;
636
+ | 'runtimeName'
637
+ | 'protocolConfiguration'
638
+ | 'agentRuntimeArtifact'
639
+ | 'authorizerConfiguration'
640
+ > & {
641
+ /**
642
+ * Identity details for Cognito Authentication
643
+ */
644
+ identity: {
645
+ userPool: IUserPool;
646
+ userPoolClient: IUserPoolClient;
647
+ };
648
+ /**
649
+ * Removal policy for the session bucket holding the agent's conversation
650
+ * history. Defaults to retaining it so a stack `destroy` doesn't silently
651
+ * delete session data — set to `RemovalPolicy.DESTROY` for sandbox/CI teardown.
652
+ *
653
+ * @default RemovalPolicy.RETAIN
654
+ */
655
+ readonly sessionBucketRemovalPolicy?: RemovalPolicy;
656
+ };
595
657
 
596
- export class StoryAgent extends Construct implements IGrantable {
658
+ export class StoryAgent extends Construct implements IGrantable, IConnectable {
597
659
  public readonly dockerImage: AgentRuntimeArtifact;
598
660
  public readonly agentCoreRuntime: Runtime;
661
+ /** Default Gateway target name for this agent. */
662
+ public readonly agentName = 'story-agent';
663
+ /** Inbound auth — a fronting Gateway uses this to pick its outbound credential. */
664
+ public readonly auth = 'cognito';
599
665
 
600
- constructor(scope: Construct, id: string, props?: StoryAgentProps) {
666
+ constructor(scope: Construct, id: string, props: StoryAgentProps) {
601
667
  super(scope, id);
602
668
 
603
- this.dockerImage = AgentRuntimeArtifact.fromAsset(
604
- path.dirname(url.fileURLToPath(new URL(import.meta.url))),
669
+ const rc = RuntimeConfig.ensure(this);
670
+
671
+ // Resolve the bundle output directory containing the Dockerfile and built artifacts
672
+ const bundleDir = path.join(
673
+ findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))),
674
+ 'dist/packages/story/docker/story-agent',
675
+ );
676
+
677
+ this.dockerImage = AgentRuntimeArtifact.fromAsset(bundleDir, {
678
+ platform: Platform.LINUX_ARM64,
679
+ });
680
+
681
+ const {
682
+ identity,
683
+ sessionBucketRemovalPolicy = RemovalPolicy.RETAIN,
684
+ ...restProps
685
+ } = props ?? {};
686
+
687
+ const sessionKey = new Key(this, 'SessionKey', {
688
+ enableKeyRotation: true,
689
+ });
690
+
691
+ // Allow CloudWatch Logs to use the session key for server access log delivery.
692
+ const stack = Stack.of(this);
693
+ sessionKey.addToResourcePolicy(
694
+ new PolicyStatement({
695
+ effect: Effect.ALLOW,
696
+ principals: [
697
+ new ServicePrincipal(`logs.${stack.region}.amazonaws.com`),
698
+ ],
699
+ actions: [
700
+ 'kms:Encrypt',
701
+ 'kms:Decrypt',
702
+ 'kms:ReEncrypt*',
703
+ 'kms:GenerateDataKey*',
704
+ 'kms:DescribeKey',
705
+ ],
706
+ resources: ['*'],
707
+ conditions: {
708
+ ArnLike: {
709
+ 'kms:EncryptionContext:aws:logs:arn': `arn:aws:logs:${stack.region}:${stack.account}:log-group:*`,
710
+ },
711
+ },
712
+ }),
713
+ );
714
+
715
+ const sessionAccessLogs = new LogGroup(this, 'SessionAccessLogs', {
716
+ retention: RetentionDays.ONE_YEAR,
717
+ encryptionKey: sessionKey,
718
+ removalPolicy: RemovalPolicy.DESTROY,
719
+ });
720
+
721
+ const sessionBucket = new Bucket(this, 'SessionBucket', {
722
+ enforceSSL: true,
723
+ removalPolicy: sessionBucketRemovalPolicy,
724
+ encryption: BucketEncryption.KMS,
725
+ encryptionKey: sessionKey,
726
+ blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
727
+ });
728
+ suppressRules(
729
+ sessionBucket,
730
+ ['CKV_AWS_21'],
731
+ 'Session data does not need versioning enabled',
732
+ );
733
+ suppressRules(
734
+ sessionBucket,
735
+ ['CKV2_AWS_61'],
736
+ 'Lifecycle configuration not required for session data',
737
+ );
738
+ suppressRules(
739
+ sessionBucket,
740
+ ['CKV_AWS_144'],
741
+ 'Cross-region replication not required for session data',
742
+ );
743
+ suppressRules(
744
+ sessionBucket,
745
+ ['CKV2_AWS_62'],
746
+ 'Event notifications not required for session data',
747
+ );
748
+ suppressRules(
749
+ sessionBucket,
750
+ ['CKV_AWS_18'],
751
+ 'Server access logs are delivered to CloudWatch Logs',
752
+ );
753
+
754
+ const sessionAccessLogsSource: CfnDeliverySource = new CfnDeliverySource(
755
+ this,
756
+ 'SessionAccessLogsSource',
605
757
  {
606
- platform: Platform.LINUX_ARM64,
607
- extraHash: execSync(
608
- `docker inspect dungeon-adventure-story-agent:latest --format '{{.Id}}'`,
609
- { encoding: 'utf-8' },
610
- ).trim(),
758
+ name: Lazy.string({
759
+ produce: () =>
760
+ Names.uniqueResourceName(sessionAccessLogsSource, {
761
+ maxLength: 60,
762
+ }),
763
+ }),
764
+ logType: 'S3_SERVER_ACCESS_LOGS',
765
+ resourceArn: sessionBucket.bucketArn,
611
766
  },
612
767
  );
768
+ const sessionBucketPolicy = sessionBucket.policy;
769
+ if (sessionBucketPolicy) {
770
+ sessionAccessLogsSource.node.addDependency(sessionBucketPolicy);
771
+ }
772
+ const sessionAccessLogsDestination: CfnDeliveryDestination =
773
+ new CfnDeliveryDestination(this, 'SessionAccessLogsDestination', {
774
+ name: Lazy.string({
775
+ produce: () =>
776
+ Names.uniqueResourceName(sessionAccessLogsDestination, {
777
+ maxLength: 60,
778
+ }),
779
+ }),
780
+ destinationResourceArn: sessionAccessLogs.logGroupArn,
781
+ });
782
+ const sessionAccessLogsDelivery = new CfnDelivery(
783
+ this,
784
+ 'SessionAccessLogsDelivery',
785
+ {
786
+ deliverySourceName: sessionAccessLogsSource.name,
787
+ deliveryDestinationArn: sessionAccessLogsDestination.attrArn,
788
+ },
789
+ );
790
+ sessionAccessLogsDelivery.addDependency(sessionAccessLogsSource);
613
791
 
614
792
  this.agentCoreRuntime = new Runtime(this, 'StoryAgent', {
615
793
  runtimeName: Lazy.string({
@@ -618,17 +796,88 @@ export class StoryAgent extends Construct implements IGrantable {
618
796
  }),
619
797
  protocolConfiguration: ProtocolType.HTTP,
620
798
  agentRuntimeArtifact: this.dockerImage,
621
- ...props,
799
+ authorizerConfiguration: RuntimeAuthorizerConfiguration.usingCognito(
800
+ identity.userPool,
801
+ [identity.userPoolClient],
802
+ ),
803
+ // Receive the caller's Authorization header (validated by the authorizer).
804
+ requestHeaderConfiguration: {
805
+ allowlistedHeaders: ['Authorization'],
806
+ },
807
+ ...restProps,
808
+ environmentVariables: {
809
+ RUNTIME_CONFIG_APP_ID: rc.appConfigApplicationId,
810
+ ...restProps?.environmentVariables,
811
+ },
812
+ });
813
+
814
+ // Grant access for the agent to invoke bedrock models
815
+ this.agentCoreRuntime.addToRolePolicy(
816
+ new PolicyStatement({
817
+ actions: [
818
+ 'bedrock:InvokeModel',
819
+ 'bedrock:InvokeModelWithResponseStream',
820
+ ],
821
+ resources: [
822
+ 'arn:aws:bedrock:*:*:foundation-model/*',
823
+ 'arn:aws:bedrock:*:*:inference-profile/*',
824
+ ],
825
+ }),
826
+ );
827
+
828
+ sessionBucket.grantReadWrite(this.agentCoreRuntime);
829
+
830
+ rc.grantReadAppConfig(this.agentCoreRuntime);
831
+
832
+ rc.set('agentcore', 'agentRuntimes', {
833
+ ...rc.get('agentcore').agentRuntimes,
834
+ StoryAgent: {
835
+ arn: this.agentCoreRuntime.agentRuntimeArn,
836
+ session: {
837
+ bucketName: sessionBucket.bucketName,
838
+ },
839
+ },
840
+ });
841
+
842
+ rc.set('connection', 'agentRuntimes', {
843
+ ...rc.get('connection').agentRuntimes,
844
+ StoryAgent: this.agentCoreRuntime.agentRuntimeArn,
622
845
  });
623
846
  }
624
847
 
848
+ /**
849
+ * The principal to grant permissions to.
850
+ */
625
851
  public get grantPrincipal(): IPrincipal {
626
852
  return this.agentCoreRuntime.grantPrincipal;
627
853
  }
854
+
855
+ /**
856
+ * Network connections for this agent runtime.
857
+ */
858
+ public get connections(): Connections {
859
+ return this.agentCoreRuntime.connections;
860
+ }
861
+
862
+ /**
863
+ * The HTTPS invocation URL of the runtime.
864
+ */
865
+ public get invocationUrl(): string {
866
+ // The URL must URL-encode the runtime ARN (':' -> '%3A', '/' -> '%2F').
867
+ // The ARN is a CDK token, so encode at deploy time via Fn.join/Fn.split.
868
+ const encodedArn = Fn.join(
869
+ '%2F',
870
+ Fn.split(
871
+ '/',
872
+ Fn.join('%3A', Fn.split(':', this.agentCoreRuntime.agentRuntimeArn)),
873
+ ),
874
+ );
875
+ return `https://bedrock-agentcore.${Stack.of(this).region}.amazonaws.com/runtimes/${encodedArn}/invocations?qualifier=DEFAULT`;
876
+ }
628
877
  }
629
878
  ```
630
879
 
631
- This configures a CDK `AgentRuntimeArtifact` which uploads your agent Docker image to ECR, and hosts it using AgentCore Runtime.
880
+ This configures a CDK `AgentRuntimeArtifact` which uploads your agent Docker image to ECR, and hosts it using AgentCore Runtime. Because we chose `--auth=cognito`, the construct requires the user pool/client identity and authorizes AgentCore Runtime invocations through Cognito, forwarding the caller's `Authorization` header. It also provisions the session bucket the Story Agent's `session.py` reads back at runtime — a KMS-encrypted S3 bucket with server access logs delivered to CloudWatch Logs — grants the agent read/write access to it, grants it access to invoke Bedrock models, and registers its ARN and bucket name in `RuntimeConfig` so both the agent (at runtime, via AppConfig) and the Game API (at synth time, via `invocationUrl`) can find it.
632
881
 
633
882
  You may notice an extra `Dockerfile`, that references the Docker image from the `story` project, allowing us to co-locate the Dockerfile and agent source code.
634
883
 
@@ -146,6 +146,14 @@ Delete the `tools` and `resources` directories in `packages/inventory/src/mcp-se
146
146
 
147
147
  ## Task 3: Update the infrastructure
148
148
 
149
+ ### Expose the session bucket
150
+
151
+ The Story Agent's construct already provisions and writes to its own session bucket internally, but doesn't expose it — so nothing outside the agent can be granted access to read from it yet. Since `queryActions` needs to read the conversation history back, for illustration and simplicity we'll expose its internal session bucket as a public property on `common/constructs/src/app/agents/story-agent/story-agent.ts`:
152
+
153
+ <E2EDiff lang="typescript" before="dungeon-adventure/2/agents/story-agent.ts.old.template" after="dungeon-adventure/2/agents/story-agent.ts.template" />
154
+
155
+ ### Wire up the stack
156
+
149
157
  The `DungeonDb` construct generated by `ts#dynamodb` already provisions our table, so we just need to instantiate it in our stack and grant the Game API and Inventory MCP server the permissions they need. Update `packages/infra/src/stacks/application-stack.ts` as follows:
150
158
 
151
159
  <E2EDiff lang="typescript" before="dungeon-adventure/1/application-stack.ts.template" after="dungeon-adventure/2/stacks/application-stack.ts.template" />
@@ -3,7 +3,7 @@ title: Implement and configure the Story agent
3
3
  description: A walkthrough of how to build an agentic AI-powered dungeon adventure game using the @aws/nx-plugin.
4
4
  ---
5
5
 
6
- import { Aside, Code, FileTree, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
6
+ import { Aside, Code, FileTree, Steps } from '@astrojs/starlight/components';
7
7
  import { Image } from 'astro:assets';
8
8
  import Drawer from '@components/drawer.astro';
9
9
  import Link from '@components/link.astro';
@@ -23,25 +23,15 @@ import gameConversationPng from '@assets/game-conversation.png'
23
23
 
24
24
  ## Task 1: Implement the Story Agent
25
25
 
26
- The Story Agent is a [Strands](https://strandsagents.com/) agent generated with `--protocol=ag-ui` in <Link path="get_started/tutorials/dungeon-game/1">Module 1</Link>, so the UI can stream from it over the [Agent-User Interaction protocol](https://docs.copilotkit.ai/aws-strands/protocol) via CopilotKit. It uses the Inventory MCP Server to manage the player's items, and Strands' built-in [`S3SessionManager`](https://strandsagents.com/latest/documentation/docs/user-guide/concepts/agents/sessions/) to persist conversation history into the sessions bucket we provisioned in Module 2.
26
+ The Story Agent is a [Strands](https://strandsagents.com/) agent generated with `--protocol=ag-ui` in <Link path="get_started/tutorials/dungeon-game/1">Module 1</Link>, so the UI can stream from it over the [Agent-User Interaction protocol](https://docs.copilotkit.ai/aws-strands/protocol) via CopilotKit. It uses the Inventory MCP Server to manage the player's items, and already persists conversation history via the `session.py` the generator provisioned in Module 1 — deployed, that's Strands' built-in [`S3SessionManager`](https://strandsagents.com/latest/documentation/docs/user-guide/concepts/agents/sessions/) writing into the same S3 bucket the Game API's `queryActions` reads from, so the browser can rebuild transcripts on revisit.
27
27
 
28
28
  ### Agent implementation
29
29
 
30
- Update the following files in `packages/story/dungeon_adventure_story/agent`:
30
+ Update `agent.py` in `packages/story/dungeon_adventure_story/agent`:
31
31
 
32
- <Tabs>
33
- <TabItem label="main.py">
34
- <E2EDiff lang="python" before="dungeon-adventure/3/main.py.old.template" after="dungeon-adventure/3/main.py.template" />
35
- </TabItem>
36
- <TabItem label="agent.py">
37
32
  <E2EDiff lang="python" before="dungeon-adventure/3/agent.py.old.template" after="dungeon-adventure/3/agent.py.template" />
38
- </TabItem>
39
- </Tabs>
40
33
 
41
- The changes are:
42
-
43
- - `main.py` adds a `session_manager_provider` that creates an `S3SessionManager` per `thread_id` when deployed, and falls back to an on-disk `FileSessionManager` under `/tmp/strands-sessions` when running under `agent-dev` (`LOCAL_DEV=true`). Deployed, the S3 bucket is the same one the Game API's `queryActions` reads from, so the browser can rebuild transcripts on revisit; locally the agent persists sessions to disk and talks to the local MCP server, without a deployment.
44
- - `agent.py` drops the sample `subtract` tool and swaps the system prompt for a dungeon-master one that invites the first user message to state the player's name and genre, and uses the Inventory MCP Server's tools.
34
+ `agent.py` drops the sample `subtract` tool and swaps the system prompt for a dungeon-master one that invites the first user message to state the player's name and genre, and uses the Inventory MCP Server's tools. `main.py` needs no changes here — session management was already wired up when the agent was generated.
45
35
 
46
36
  ## Task 2: Test your Agent locally
47
37
 
@@ -50,6 +50,7 @@ The generator will add the following files to your existing Python project. The
50
50
  - \_\_init\_\_.py Python package initialization
51
51
  - init.py FastAPI application setup with CORS and error handling middleware
52
52
  - agent.py Main agent definition with sample tools
53
+ - session.py Resolves the framework-specific session persistence implementation
53
54
  - main.py FastAPI entry point for Bedrock AgentCore Runtime
54
55
  - Dockerfile Entry point for hosting your agent (excluded when `infra` is set to `None`)
55
56
  - pyproject.toml Updated with Strands dependencies
@@ -68,6 +69,7 @@ The entry point exposes your agent over the A2A protocol (Strands uses the [Stra
68
69
  - agent/ (or custom name if specified)
69
70
  - \_\_init\_\_.py Python package initialization
70
71
  - agent.py Main agent definition with sample tools
72
+ - session.py Resolves the framework-specific session persistence implementation
71
73
  - main.py A2A server entry point
72
74
  - Dockerfile Entry point for hosting your agent (excluded when `infra` is set to `None`)
73
75
  - pyproject.toml Updated with framework and A2A dependencies
@@ -86,6 +88,7 @@ The entry point exposes your agent via the [AG-UI](https://docs.ag-ui.com/) prot
86
88
  - agent/ (or custom name if specified)
87
89
  - \_\_init\_\_.py Python package initialization
88
90
  - agent.py Main agent definition with sample tools
91
+ - session.py Resolves the framework-specific session persistence implementation
89
92
  - main.py AG-UI server entry point
90
93
  - Dockerfile Entry point for hosting your agent (excluded when `infra` is set to `None`)
91
94
  - pyproject.toml Updated with framework and AG-UI dependencies
@@ -486,6 +489,39 @@ Your agent is automatically configured with observability using the [AWS Distro
486
489
  You can find traces in the CloudWatch AWS Console, by selecting "GenAI Observability" in the menu. Note that for traces to be populated you will need to enable [Transaction Search](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Transaction-Search.html).
487
490
 
488
491
  For more details, refer to the [AgentCore documentation on observability](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability-configure.html).
492
+
493
+ ### Session Management
494
+
495
+ The `session` option maps to a different underlying persistence concept depending on your chosen framework: Strands' [session management](https://strandsagents.com/docs/user-guide/concepts/agents/session-management/) concept for the `strands` framework, or LangGraph's [checkpointer](https://docs.langchain.com/oss/python/langgraph/persistence) concept for the `langchain` framework.
496
+
497
+ <OptionFilter when={{ framework: 'strands' }} description="Strands session management (session.py)">
498
+ The `session` option controls how your agent persists conversation state (message history, tool state, etc.) across invocations, using the Strands SDK's [SessionManager](https://strandsagents.com/docs/user-guide/concepts/agents/session-management/):
499
+
500
+ - **`s3`** (default): The CDK/Terraform infrastructure provisions a dedicated S3 bucket for session data, encrypted with a dedicated KMS key and with all public access blocked; server access logs are delivered to a CloudWatch Logs log group via the same key. The agent's IAM role is granted read/write/list/delete access to the bucket and decrypt/generate-data-key access to the key, and the bucket name is registered alongside the agent's ARN in AppConfig runtime configuration.
501
+ - **`in-memory`**: No bucket is provisioned. Conversation state is kept in memory only for the lifetime of the running process and does not survive restarts or scale-in.
502
+
503
+ This is implemented in the generated `session.py`, which exports a `get_session_manager()` function resolving a `SessionManager` for the current session.
504
+
505
+ The session ID itself comes from the AgentCore Runtime session (propagated via the `x-amzn-bedrock-agentcore-runtime-session-id` header) and is bound to a [`contextvars.ContextVar`](https://docs.python.org/3/library/contextvars.html)-based context so `get_current_session_id()` can resolve it anywhere in the request — including in any downstream MCP or A2A clients wired up via the <Link path="/guides/connection">`connection` generator</Link>, so the whole call chain shares a consistent session.
506
+
507
+ :::note[Local Development]
508
+ When running locally (`LOCAL_DEV=true`, set automatically by the `-dev` target), session data is always stored on disk under `tmp/agents/strands/<agent-name>` at the workspace root, regardless of the configured `session` option, for convenience.
509
+ :::
510
+ </OptionFilter>
511
+
512
+ <OptionFilter when={{ framework: 'langchain' }} description="LangChain session management (session.py)">
513
+ The `session` option controls how your agent's LangGraph checkpointer persists conversation state:
514
+
515
+ - **`s3`** (default): The deployed agent uses an `S3CheckpointSaver` with the provisioned session bucket, storing checkpoints and pending writes under the `checkpoints/` prefix. This class lives in `s3_checkpoint_saver_langchain.py` in the shared agent-connection project.
516
+ - **`dynamodb-s3`**: The CDK/Terraform infrastructure provisions a DynamoDB table for checkpoints, configured as recommended in the [AWS documentation on using DynamoDB as a checkpoint store for LangGraph agents](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ddb-langgraph-checkpoint.html) (unified `PK`/`SK` schema, `PAY_PER_REQUEST` billing, point-in-time recovery, and a `ttl` attribute), plus an S3 bucket for offloading checkpoints over 350KB. Both are encrypted with a dedicated KMS key; the bucket's server access logs are delivered to a CloudWatch Logs log group via the same key. The agent's IAM role is granted read/write access to the table and bucket, and the table/bucket names are registered alongside the agent's ARN in AppConfig runtime configuration.
517
+ - **`in-memory`**: No table or bucket is provisioned. Conversation state is kept in memory only for the lifetime of the running process and does not survive restarts or scale-in.
518
+
519
+ This is implemented in the generated `session.py`, which exports a `get_checkpointer()` function called from `agent.py`'s `create_agent(..., checkpointer=get_checkpointer())`.
520
+
521
+ :::note[Local Development]
522
+ When running locally (`LOCAL_DEV=true`, set automatically by the `-dev` target), `get_checkpointer()` always returns an [`AsyncSqliteSaver`](https://reference.langchain.com/python/langgraph.checkpoint.sqlite/aio/AsyncSqliteSaver) backed by a local SQLite database under `tmp/agents/langchain/<agent-name>` at the workspace root, regardless of the configured `session` option, for convenience.
523
+ :::
524
+ </OptionFilter>
489
525
  </OptionFilter>
490
526
 
491
527
  ## Invoking your Agent
@@ -360,14 +360,6 @@ The `session` option controls how your agent persists conversation state (messag
360
360
 
361
361
  This is implemented in the generated `session.ts`, which exports a `getSessionManager()` function resolving a `SessionManager` for the current session.
362
362
 
363
- <OptionFilter when={{ protocol: 'ag-ui' }} description="AG-UI session wiring">
364
- Since AG-UI clones a template agent per conversation thread, `getSessionManager` is wired in as a `sessionManagerProvider` on the `StrandsAgent` config in `index.ts`, so a fresh `SessionManager` is resolved for each thread rather than baked into the shared template agent.
365
- </OptionFilter>
366
-
367
- <OptionFilter when={{ protocol: ['http', 'a2a'] }} description="HTTP/A2A session wiring">
368
- Since `withSessionId` already caches one `Agent` instance per session, `getSessionManager` is called directly inside `getAgent`'s `new Agent({ sessionManager: await getSessionManager() })` call in `agent.ts`.
369
- </OptionFilter>
370
-
371
363
  The session ID itself comes from the AgentCore Runtime session (propagated via the `x-amzn-bedrock-agentcore-runtime-session-id` header for A2A/AG-UI, or the WebSocket connection context for HTTP/tRPC) and is bound to an [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage)-based context so `getCurrentSessionId()` can resolve it anywhere in the request — including in any downstream MCP or A2A clients wired up via the <Link path="/guides/connection">`connection` generator</Link>, so the whole call chain shares a consistent session.
372
364
 
373
365
  :::note[Local Development]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws/nx-plugin-mcp",
3
- "version": "1.0.0-rc.70",
3
+ "version": "1.0.0-rc.72",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/awslabs/nx-plugin-for-aws.git",
@@ -63,10 +63,10 @@
63
63
  },
64
64
  "session": {
65
65
  "type": "string",
66
- "description": "The storage used to persist session for your Agent. Only 'in-memory' is currently supported for Python agents.",
66
+ "description": "The storage used to persist session for your Agent. LangChain supports 's3' or 'dynamodb-s3'; Strands supports 's3'; 'in-memory' is valid for both.",
67
67
  "x-prompt": "How would you like to persist session for your Agent?",
68
- "enum": ["in-memory"],
69
- "default": "in-memory",
68
+ "enum": ["s3", "dynamodb-s3", "in-memory"],
69
+ "default": "s3",
70
70
  "x-priority": "important"
71
71
  },
72
72
  "preferInstallDependencies": {