diff --git a/lambda-durable-bedrock-agentcore-async/README.md b/lambda-durable-bedrock-agentcore-async/README.md new file mode 100644 index 000000000..c8e36f3a2 --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/README.md @@ -0,0 +1,132 @@ +# Asynchronous Amazon Bedrock AgentCore integration with AWS Lambda durable functions + +This pattern shows how to asynchronously invoke an agent running on Amazon Bedrock AgentCore from AWS Lambda durable functions. The durable function uses `context.map` durable operation to fan out two trip-planning prompts in parallel, each using `waitForCallback` to pause while the agent processes the request via the Strands Agents SDK. When both agents complete, the results are combined into a single response. + +Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/lambda-durable-bedrock-agentcore-async](https://serverlessland.com/patterns/lambda-durable-bedrock-agentcore-async) + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI v2](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) (latest available version) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) (version 2.221.0 or later) installed and configured +* [Node.js 22.x](https://nodejs.org/) installed +* [Finch](https://runfinch.com/), [Docker](https://www.docker.com/products/docker-desktop/) or a compatible tool (required to build the agent container image) + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + + ```bash + git clone https://github.com/aws-samples/serverless-patterns + ``` + +1. Change directory to the pattern directory: + + ```bash + cd lambda-durable-bedrock-agentcore-async + ``` + +1. Install the project dependencies: + + ```bash + npm install + ``` + +1. Install the Lambda durable functions dependencies: + + ```bash + cd durable-lambda && npm install && cd .. + ``` + +1. Deploy the CDK stacks: + + ```bash + cdk deploy --all + ``` + + Note: This deploys two stacks — `AgentCoreStrandsStack` (the agent runtime) and `DurableAgentStack` (the durable Lambda). Deploy to your default AWS region. Please refer to the [AWS capabilities explorer](https://builder.aws.com/build/capabilities/explore) for feature availability in your desired region. + +1. Note the outputs from the CDK deployment process. These contain the resource ARNs used for testing. + +## How it works + +This pattern creates two stacks: + +1. **AgentCoreStrandsStack** — Deploys a containerized Python agent on Amazon Bedrock AgentCore. The agent uses the Strands Agents SDK with Amazon Bedrock foundation models to process prompts. It is built from a local Dockerfile and pushed to ECR automatically by CDK. + +2. **DurableAgentStack** — Deploys a durable function (using Node.js 22.x) that orchestrates the agent invocation using `context.map` for parallel execution: + - The durable function receives a city name and builds two prompts: a weekend trip agenda and a weeklong trip agenda + - `context.map` fans out both prompts in parallel, each running in its own child context + - Inside each map iteration, `waitForCallback` pauses the execution while the agent processes the prompt + - The agent confirms receipt immediately and processes the LLM call in a background thread + - When each agent finishes, it calls `SendDurableExecutionCallbackSuccess` to resume its respective callback + - A final `context.step` combines the two trip plans into a single response + +The durable execution SDK automatically checkpoints progress, so when the Lambda function is paused and restarted, it resumes from the last completed checkpoint rather than re-executing completed steps. If one trip plan completes before the other, its result is checkpointed and won't be re-fetched on replay. + +## Testing + +After deployment, invoke the durable function using the AWS CLI or from the AWS Console. + +### Invoke the durable function + +Use the qualified alias ARN from the CDK output (`DurableFunctionAliasArn`): + +```bash +aws lambda invoke \ + --function-name durableAgentCaller:prod \ + --payload '{"city": "Tokyo"}' \ + --cli-binary-format raw-in-base64-out \ + response.json +``` + +### View the response + +```bash +cat response.json +``` + +### Expected Response + +The durable function returns a JSON response after both agent calls complete: + +```json +{ + "city": "Tokyo", + "weekendTrip": "Day 1: Start your morning at Tsukiji Outer Market...", + "weeklongTrip": "Day 1: Arrive and settle into Shinjuku...", + "timestamp": "2026-02-26T12:00:00.000Z" +} +``` + +The initial `invoke` call returns immediately with a durable execution ID. The function fans out both trip-planning prompts in parallel, suspends while waiting for the agent callbacks, then resumes and combines the results. + +### View CloudWatch logs + +```bash +aws logs filter-log-events \ + --log-group-name /aws/lambda/durableAgentCaller \ + --start-time $(date -v-5M +%s)000 +``` + +## Cleanup + +1. Delete the stacks: + + ```bash + cdk destroy --all + ``` + +1. Confirm the stacks have been deleted by checking the AWS CloudFormation console or running: + + ```bash + aws cloudformation list-stacks --stack-status-filter DELETE_COMPLETE + ``` + +---- +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/lambda-durable-bedrock-agentcore-async/agent/.dockerignore b/lambda-durable-bedrock-agentcore-async/agent/.dockerignore new file mode 100644 index 000000000..8e2eca20d --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/agent/.dockerignore @@ -0,0 +1,3 @@ +__pycache__ +*.pyc +.git diff --git a/lambda-durable-bedrock-agentcore-async/agent/Dockerfile b/lambda-durable-bedrock-agentcore-async/agent/Dockerfile new file mode 100644 index 000000000..c6317cec7 --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/agent/Dockerfile @@ -0,0 +1,18 @@ +FROM ghcr.io/astral-sh/uv:python3.13-alpine + +WORKDIR /app + +ENV UV_SYSTEM_PYTHON=1 \ + UV_COMPILE_BYTECODE=1 + +COPY requirements.txt requirements.txt +RUN uv pip install -r requirements.txt + +RUN adduser -D -u 1000 bedrock_agentcore +USER bedrock_agentcore + +EXPOSE 8080 8000 + +COPY . . + +CMD ["python", "-m", "agent"] diff --git a/lambda-durable-bedrock-agentcore-async/agent/agent.py b/lambda-durable-bedrock-agentcore-async/agent/agent.py new file mode 100644 index 000000000..5ae25dfef --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/agent/agent.py @@ -0,0 +1,109 @@ +""" +Strands Agent for AgentCore Runtime. +Accepts a prompt + callback info, returns confirmation immediately, +then invokes the LLM and sends the result back via Lambda durable callback. +""" +import os +import json +import logging +import threading + +import boto3 +from strands import Agent +from strands.models import BedrockModel +from bedrock_agentcore.runtime import BedrockAgentCoreApp + +logger = logging.getLogger(__name__) +app = BedrockAgentCoreApp() + +LAMBDA_REGION = os.environ.get("AWS_REGION", "us-east-1") +lambda_client = boto3.client("lambda", region_name=LAMBDA_REGION) + + +def send_callback_success(callback_id: str, result: dict): + """Send the LLM result back to the durable function via callback.""" + lambda_client.send_durable_execution_callback_success( + CallbackId=callback_id, + Result=json.dumps(result), + ) + + +def run_agent(prompt: str, model_id: str, callback_id: str, task_id: str): + """Invoke the LLM and send the result back via durable callback.""" + try: + model = BedrockModel( + model_id=model_id, + max_tokens=4096, + temperature=0.7, + ) + + agent = Agent( + model=model, + system_prompt=( + "You are a helpful AI assistant. " + "Answer the user's question clearly and concisely." + ), + ) + + result = agent(prompt) + answer = str(result) + + logger.info("LLM completed, sending callback success") + send_callback_success(callback_id, {"answer": answer}) + + except Exception as e: + logger.error("Agent failed: %s", e) + lambda_client.send_durable_execution_callback_failure( + CallbackId=callback_id, + Error=str(e), + ) + finally: + app.complete_async_task(task_id) + + +@app.entrypoint +def entrypoint(payload): + """ + Main entrypoint invoked by AgentCore Runtime. + + Expects payload: + - prompt: the user's question + - callbackId: durable execution callback ID + - model (optional): { modelId: "..." } + + Returns confirmation immediately, then processes the LLM call + in a background thread and sends the result via callback. + """ + prompt = payload.get("prompt", "") + callback_id = payload.get("callbackId") + model_config = payload.get("model", {}) + model_id = model_config.get( + "modelId", "global.anthropic.claude-sonnet-4-5-20250929-v1:0" + ) + + if not callback_id: + return {"error": "Missing callbackId in payload"} + + # Track the async task so /ping reports HealthyBusy + task_id = app.add_async_task("agent_invocation", { + "prompt": prompt, + "callbackId": callback_id, + }) + + # Run the LLM work in a background thread to avoid blocking /ping + threading.Thread( + target=run_agent, + args=(prompt, model_id, callback_id, task_id), + daemon=True, + ).start() + + # Return confirmation immediately + return { + "status": "accepted", + "message": "Processing prompt, will callback", + "callbackId": callback_id, + } + + +if __name__ == "__main__": + app.run() diff --git a/lambda-durable-bedrock-agentcore-async/agent/requirements.txt b/lambda-durable-bedrock-agentcore-async/agent/requirements.txt new file mode 100644 index 000000000..cca38a001 --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/agent/requirements.txt @@ -0,0 +1,4 @@ +strands-agents +bedrock-agentcore +boto3 +aws-durable-execution-sdk-python diff --git a/lambda-durable-bedrock-agentcore-async/bin/app.ts b/lambda-durable-bedrock-agentcore-async/bin/app.ts new file mode 100644 index 000000000..c02314de9 --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/bin/app.ts @@ -0,0 +1,20 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { AgentCoreStrandsStack } from '../lib/agentcore-strands-stack'; +import { DurableAgentStack } from '../lib/durable-agent-stack'; + +const app = new cdk.App(); + +const env = { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, +}; + +const agentCoreStack = new AgentCoreStrandsStack(app, 'AgentCoreStrandsStack', { env }); + +new DurableAgentStack(app, 'DurableAgentStack', { + env, + agentRuntimeArn: agentCoreStack.runtimeArn, + agentRuntimeEndpointUrl: agentCoreStack.runtimeEndpointUrl, +}); diff --git a/lambda-durable-bedrock-agentcore-async/cdk.context.json b/lambda-durable-bedrock-agentcore-async/cdk.context.json new file mode 100644 index 000000000..fa3802610 --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/cdk.context.json @@ -0,0 +1,6 @@ +{ + "acknowledged-issue-numbers": [ + 37013, + 34892 + ] +} diff --git a/lambda-durable-bedrock-agentcore-async/cdk.json b/lambda-durable-bedrock-agentcore-async/cdk.json new file mode 100644 index 000000000..e6475b158 --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/cdk.json @@ -0,0 +1,8 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts", + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": ["aws", "aws-cn"] + } +} diff --git a/lambda-durable-bedrock-agentcore-async/durable-lambda/index.ts b/lambda-durable-bedrock-agentcore-async/durable-lambda/index.ts new file mode 100644 index 000000000..03e5b2845 --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/durable-lambda/index.ts @@ -0,0 +1,107 @@ +import { + withDurableExecution, + DurableContext, +} from '@aws/durable-execution-sdk-js'; +import { BedrockAgentCoreClient, InvokeAgentRuntimeCommand } from "@aws-sdk/client-bedrock-agentcore"; + +const AGENT_RUNTIME_ARN = process.env.AGENT_RUNTIME_ARN!; + +const client = new BedrockAgentCoreClient(); + +interface AgentEvent { + city: string; +} + +interface TripRequest { + label: string; + prompt: string; +} + +async function invokeAgentEndpoint(payload: Record): Promise { + const command = new InvokeAgentRuntimeCommand({ + agentRuntimeArn: AGENT_RUNTIME_ARN, + payload: Buffer.from(JSON.stringify(payload)), + }); + + const response = await client.send(command); + if (!response.response) { + throw new Error('No response received from agent runtime'); + } + return response.response.transformToString(); +} + +/** + * Durable function that plans two trips in parallel using context.map. + * + * Flow: + * 1. Builds two trip-planning prompts (weekend + weeklong) for the given city + * 2. Maps over them in parallel, each using waitForCallback to invoke the agent + * 3. Combines the two results into a single response + */ +export const handler = withDurableExecution( + async (event: AgentEvent, context: DurableContext) => { + const { city } = event; + context.logger.info('Starting parallel trip planning', { city }); + + const tripRequests: TripRequest[] = [ + { + label: 'weekend', + prompt: `Create a detailed agenda for a weekend trip to ${city}. Include activities, restaurants, and timing for each day.`, + }, + { + label: 'weeklong', + prompt: `Create a detailed agenda for a weeklong trip to ${city}. Include activities, restaurants, and timing for each day.`, + }, + ]; + + // Process both trip requests in parallel, each with its own callback + const mapResult = await context.map( + 'plan-trips', + tripRequests, + async (mapCtx, request) => { + const agentResponse = await mapCtx.waitForCallback( + `invoke-agent-${request.label}`, + async (callbackId, ctx) => { + ctx.logger.info('Sending prompt to AgentCore', { + callbackId, + label: request.label, + }); + + const confirmation = await invokeAgentEndpoint({ + prompt: request.prompt, + callbackId, + }); + + ctx.logger.info('Agent confirmed receipt', { confirmation, label: request.label }); + }, + { timeout: { minutes: 5 } }, + ); + + const parsed = typeof agentResponse === 'string' + ? JSON.parse(agentResponse) + : agentResponse; + + return { label: request.label, answer: parsed.answer }; + }, + { maxConcurrency: 2, itemNamer: (request) => `${city}-${request.label}` }, + ); + + // Combine the two results + const result = await context.step('combine-results', async () => { + mapResult.throwIfError(); + const results = mapResult.getResults(); + + const weekend = results.find((r) => r.label === 'weekend'); + const weeklong = results.find((r) => r.label === 'weeklong'); + + return { + city, + weekendTrip: weekend?.answer, + weeklongTrip: weeklong?.answer, + timestamp: new Date().toISOString(), + }; + }); + + return result; + }, +); diff --git a/lambda-durable-bedrock-agentcore-async/durable-lambda/package.json b/lambda-durable-bedrock-agentcore-async/durable-lambda/package.json new file mode 100644 index 000000000..bf8c40c42 --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/durable-lambda/package.json @@ -0,0 +1,13 @@ +{ + "name": "durable-agent-caller", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "@aws-crypto/sha256-js": "latest", + "@aws-sdk/client-bedrock-agentcore": "^3.998.0", + "@aws-sdk/credential-provider-node": "latest", + "@aws/durable-execution-sdk-js": "latest", + "@smithy/protocol-http": "latest", + "@smithy/signature-v4": "latest" + } +} diff --git a/lambda-durable-bedrock-agentcore-async/durable-lambda/tsconfig.json b/lambda-durable-bedrock-agentcore-async/durable-lambda/tsconfig.json new file mode 100644 index 000000000..8d85f87f2 --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/durable-lambda/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": ["es2022"], + "outDir": ".", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["*.ts"], + "exclude": ["node_modules"] +} diff --git a/lambda-durable-bedrock-agentcore-async/example-pattern.json b/lambda-durable-bedrock-agentcore-async/example-pattern.json new file mode 100644 index 000000000..a4aa0acef --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/example-pattern.json @@ -0,0 +1,68 @@ +{ + "title": "Asynchronous Amazon Bedrock AgentCore integration with AWS Lambda durable functions", + "description": "Orchestrate parallel AI agent invocations on Amazon Bedrock AgentCore using AWS Lambda durable functions with context.map and waitForCallback", + "language": "TypeScript", + "level": "300", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern deploys an AWS Lambda durable function that orchestrates a long-running AI agent hosted on Amazon Bedrock AgentCore using the Strands Agents SDK.", + "The durable function uses context.map to fan out two trip-planning prompts in parallel, each using waitForCallback to pause while the agent processes the request. When both agents complete, the results are combined into a single response.", + "The agent runs as a containerized Python application on AgentCore, built with the Strands Agents SDK and deployed automatically via CDK. The durable execution SDK checkpoints progress so the workflow is fault-tolerant and resumable." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-bedrock-agentcore-async", + "templateURL": "serverless-patterns/lambda-durable-bedrock-agentcore-async", + "projectFolder": "lambda-durable-bedrock-agentcore-async", + "templateFile": "lib/durable-agent-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "AWS Lambda durable functions documentation", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-basic-concepts.html" + }, + { + "text": "Durable Execution SDK", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-execution-sdk.html" + }, + { + "text": "Amazon Bedrock AgentCore documentation", + "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html" + }, + { + "text": "AWS CDK Developer Guide", + "link": "https://docs.aws.amazon.com/cdk/latest/guide/" + } + ] + }, + "deploy": { + "text": [ + "npm install", + "cd durable-lambda && npm install && cd ..", + "cdk deploy --all" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stacks: cdk destroy --all." + ] + }, + "authors": [ + { + "name": "Ben Freiberg", + "image": "https://serverlessland.com/assets/images/resources/contributors/ben-freiberg.jpg", + "bio": "Ben is a Senior Solutions Architect at Amazon Web Services (AWS) based in Frankfurt, Germany.", + "linkedin": "benfreiberg" + } + ] +} diff --git a/lambda-durable-bedrock-agentcore-async/lambda-durable-bedrock-agentcore-async.json b/lambda-durable-bedrock-agentcore-async/lambda-durable-bedrock-agentcore-async.json new file mode 100644 index 000000000..09d371e90 --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/lambda-durable-bedrock-agentcore-async.json @@ -0,0 +1,86 @@ +{ + "title": "Asynchronous Amazon Bedrock AgentCore integration with AWS Lambda durable functions", + "description": "Orchestrate parallel AI agent invocations on Amazon Bedrock AgentCore using AWS Lambda durable functions with context.map and waitForCallback", + "language": "TypeScript", + "level": "300", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern deploys an AWS Lambda durable function that orchestrates a long-running AI agent hosted on Amazon Bedrock AgentCore using the Strands Agents SDK.", + "The durable function uses context.map to fan out two trip-planning prompts in parallel, each using waitForCallback to pause while the agent processes the request. When both agents complete, the results are combined into a single response.", + "The agent runs as a containerized Python application on AgentCore, built with the Strands Agents SDK and deployed automatically via CDK. The durable execution SDK checkpoints progress so the workflow is fault-tolerant and resumable." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-bedrock-agentcore-async", + "templateURL": "serverless-patterns/lambda-durable-bedrock-agentcore-async", + "projectFolder": "lambda-durable-bedrock-agentcore-async", + "templateFile": "lib/durable-agent-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "AWS Lambda durable functions documentation", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-basic-concepts.html" + }, + { + "text": "Durable Execution SDK", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-execution-sdk.html" + }, + { + "text": "Amazon Bedrock AgentCore documentation", + "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html" + }, + { + "text": "AWS CDK Developer Guide", + "link": "https://docs.aws.amazon.com/cdk/latest/guide/" + } + ] + }, + "deploy": { + "text": [ + "npm install", + "cd durable-lambda && npm install && cd ..", + "cdk deploy --all" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stacks: cdk destroy --all." + ] + }, + "authors": [ + { + "name": "Ben Freiberg", + "image": "https://serverlessland.com/assets/images/resources/contributors/ben-freiberg.jpg", + "bio": "Ben is a Senior Solutions Architect at Amazon Web Services (AWS) based in Frankfurt, Germany.", + "linkedin": "benfreiberg" + } + ], + "patternArch": { + "icon1": { + "x": 20, + "y": 50, + "service": "bedrock", + "label": "Amazon Bedrock AgentCore" + }, + "icon2": { + "x": 80, + "y": 50, + "service": "lambda", + "label": "AWS Lambda Durable Functions" + }, + "line1": { + "from": "icon1", + "to": "icon2" + } + } +} diff --git a/lambda-durable-bedrock-agentcore-async/lib/agentcore-strands-stack.ts b/lambda-durable-bedrock-agentcore-async/lib/agentcore-strands-stack.ts new file mode 100644 index 000000000..1a09d4cd3 --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/lib/agentcore-strands-stack.ts @@ -0,0 +1,71 @@ +import * as cdk from 'aws-cdk-lib'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as path from 'path'; +import { Construct } from 'constructs'; +import * as agentcore from '@aws-cdk/aws-bedrock-agentcore-alpha'; + +export class AgentCoreStrandsStack extends cdk.Stack { + public readonly runtimeArn: string; + public readonly runtimeEndpointUrl: string; + + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Build the agent container from the local Dockerfile + const agentRuntimeArtifact = agentcore.AgentRuntimeArtifact.fromAsset( + path.join(__dirname, '../agent'), + ); + + // Create the AgentCore Runtime (L2 construct handles ECR + IAM automatically) + const runtime = new agentcore.Runtime(this, 'StrandsAgentRuntime', { + runtimeName: 'strandsPromptAgent', + agentRuntimeArtifact, + description: 'Strands SDK agent that answers prompts via Bedrock', + }); + + // Grant Bedrock model invocation permissions to the runtime's execution role + runtime.addToRolePolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], + resources: [ + `arn:aws:bedrock:*::foundation-model/*`, + "arn:aws:bedrock:*:*:inference-profile/*", + ], + }), + ); + + runtime.addToRolePolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'lambda:SendDurableExecutionCallbackSuccess', + 'lambda:SendDurableExecutionCallbackFailure', + 'lambda:SendDurableExecutionCallbackHeartbeat', + ], + resources: ["*"], + }), + ); + + this.runtimeArn = runtime.agentRuntimeArn; + this.runtimeEndpointUrl = `https://bedrock-agentcore-runtime.${this.region}.amazonaws.com/runtimes/${runtime.agentRuntimeId}/endpoints/DEFAULT`; + + new cdk.CfnOutput(this, 'RuntimeArn', { + value: runtime.agentRuntimeArn, + description: 'ARN of the AgentCore Runtime', + }); + + new cdk.CfnOutput(this, 'RuntimeId', { + value: runtime.agentRuntimeId, + description: 'ID of the AgentCore Runtime', + }); + + new cdk.CfnOutput(this, 'RuntimeEndpointUrl', { + value: this.runtimeEndpointUrl, + description: 'AgentCore Runtime DEFAULT endpoint URL', + }); + } +} diff --git a/lambda-durable-bedrock-agentcore-async/lib/durable-agent-stack.ts b/lambda-durable-bedrock-agentcore-async/lib/durable-agent-stack.ts new file mode 100644 index 000000000..30ca0499b --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/lib/durable-agent-stack.ts @@ -0,0 +1,78 @@ +import * as cdk from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import * as path from 'path'; +import { Construct } from 'constructs'; + +interface DurableAgentStackProps extends cdk.StackProps { + /** ARN of the AgentCore Runtime to invoke */ + agentRuntimeArn: string; + /** AgentCore Runtime endpoint URL */ + agentRuntimeEndpointUrl: string; +} + +export class DurableAgentStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: DurableAgentStackProps) { + super(scope, id, props); + + const logGroup = new logs.LogGroup(this, 'DurableFunctionLogGroup', { + logGroupName: '/aws/lambda/durableAgentCaller', + retention: logs.RetentionDays.ONE_WEEK, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const durableFunction = new NodejsFunction(this, 'DurableAgentCaller', { + functionName: 'durableAgentCaller', + runtime: lambda.Runtime.NODEJS_22_X, + entry: path.join(__dirname, '../durable-lambda/index.ts'), + handler: 'handler', + timeout: cdk.Duration.minutes(15), + memorySize: 512, + logGroup, + environment: { + AGENT_RUNTIME_ARN: props.agentRuntimeArn, + AGENT_RUNTIME_ENDPOINT_URL: props.agentRuntimeEndpointUrl, + }, + durableConfig: { + executionTimeout: cdk.Duration.minutes(10), + retentionPeriod: cdk.Duration.days(3), + } + }); + + // Allow invoking the AgentCore Runtime + durableFunction.addToRolePolicy( + new iam.PolicyStatement({ + actions: ['bedrock-agentcore:InvokeAgentRuntime'], + resources: [ + props.agentRuntimeArn, + `${props.agentRuntimeArn}/*`], + }), + ); + + // Durable execution checkpoint permissions + durableFunction.role!.addManagedPolicy( + iam.ManagedPolicy.fromAwsManagedPolicyName( + 'service-role/AWSLambdaBasicDurableExecutionRolePolicy', + ), + ); + + // Create version and alias for qualified invocation + const version = durableFunction.currentVersion; + const alias = new lambda.Alias(this, 'ProdAlias', { + aliasName: 'prod', + version, + }); + + new cdk.CfnOutput(this, 'DurableFunctionArn', { + value: durableFunction.functionArn, + description: 'Durable function ARN', + }); + + new cdk.CfnOutput(this, 'DurableFunctionAliasArn', { + value: alias.functionArn, + description: 'Qualified ARN (use this for invocation)', + }); + } +} diff --git a/lambda-durable-bedrock-agentcore-async/package.json b/lambda-durable-bedrock-agentcore-async/package.json new file mode 100644 index 000000000..6d98a13e1 --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/package.json @@ -0,0 +1,23 @@ +{ + "name": "agentcore-strands-cdk", + "version": "1.0.0", + "description": "CDK app deploying a Bedrock AgentCore Runtime agent with Strands SDK", + "scripts": { + "build": "tsc", + "cdk": "cdk", + "deploy": "cdk deploy", + "synth": "cdk synth" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "aws-cdk": "^2.221.0", + "typescript": "^5.0.0" + }, + "dependencies": { + "aws-cdk-lib": "^2.221.0", + "@aws-cdk/aws-bedrock-agentcore-alpha": "2.221.0-alpha.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21", + "@aws/durable-execution-sdk-js": "latest" + } +} diff --git a/lambda-durable-bedrock-agentcore-async/tsconfig.json b/lambda-durable-bedrock-agentcore-async/tsconfig.json new file mode 100644 index 000000000..57d3ea59f --- /dev/null +++ b/lambda-durable-bedrock-agentcore-async/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": ["./node_modules/@types"], + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "exclude": ["node_modules", "cdk.out", "durable-lambda"] +}