@aws/nx-plugin-mcp 1.0.0-rc.71 → 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.
- package/docs/get_started/tutorials/dungeon-game/1.mdx +272 -23
- package/docs/get_started/tutorials/dungeon-game/2.mdx +8 -0
- package/docs/get_started/tutorials/dungeon-game/3.mdx +4 -14
- package/docs/guides/py-agent.mdx +36 -0
- package/docs/guides/ts-agent.mdx +0 -8
- package/package.json +1 -1
- package/src/py/agent/schema.json +3 -3
|
@@ -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
|
|
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
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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
|
|
666
|
+
constructor(scope: Construct, id: string, props: StoryAgentProps) {
|
|
601
667
|
super(scope, id);
|
|
602
668
|
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
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
|
|
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/)
|
|
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
|
|
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
|
-
|
|
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
|
|
package/docs/guides/py-agent.mdx
CHANGED
|
@@ -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
|
package/docs/guides/ts-agent.mdx
CHANGED
|
@@ -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
package/src/py/agent/schema.json
CHANGED
|
@@ -63,10 +63,10 @@
|
|
|
63
63
|
},
|
|
64
64
|
"session": {
|
|
65
65
|
"type": "string",
|
|
66
|
-
"description": "The storage used to persist session for your Agent.
|
|
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": "
|
|
68
|
+
"enum": ["s3", "dynamodb-s3", "in-memory"],
|
|
69
|
+
"default": "s3",
|
|
70
70
|
"x-priority": "important"
|
|
71
71
|
},
|
|
72
72
|
"preferInstallDependencies": {
|