Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions lambda-durable-bedrock-agentcore-async/README.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions lambda-durable-bedrock-agentcore-async/agent/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__
*.pyc
.git
18 changes: 18 additions & 0 deletions lambda-durable-bedrock-agentcore-async/agent/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
109 changes: 109 additions & 0 deletions lambda-durable-bedrock-agentcore-async/agent/agent.py
Original file line number Diff line number Diff line change
@@ -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()
4 changes: 4 additions & 0 deletions lambda-durable-bedrock-agentcore-async/agent/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
strands-agents
bedrock-agentcore
boto3
aws-durable-execution-sdk-python
20 changes: 20 additions & 0 deletions lambda-durable-bedrock-agentcore-async/bin/app.ts
Original file line number Diff line number Diff line change
@@ -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,
});
6 changes: 6 additions & 0 deletions lambda-durable-bedrock-agentcore-async/cdk.context.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"acknowledged-issue-numbers": [
37013,
34892
]
}
8 changes: 8 additions & 0 deletions lambda-durable-bedrock-agentcore-async/cdk.json
Original file line number Diff line number Diff line change
@@ -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"]
}
}
Loading