diff --git a/sfn-agentcore-bedrock-cdk/.gitignore b/sfn-agentcore-bedrock-cdk/.gitignore new file mode 100644 index 00000000..52f5a011 --- /dev/null +++ b/sfn-agentcore-bedrock-cdk/.gitignore @@ -0,0 +1,3 @@ +node_modules +build +cdk.out diff --git a/sfn-agentcore-bedrock-cdk/README.md b/sfn-agentcore-bedrock-cdk/README.md new file mode 100644 index 00000000..96b75bf1 --- /dev/null +++ b/sfn-agentcore-bedrock-cdk/README.md @@ -0,0 +1,96 @@ +# Step Functions with Bedrock AgentCore Multi-Agent Orchestration + +This workflow deploys an AWS Step Functions state machine that orchestrates multiple Bedrock AgentCore agents in parallel, then aggregates their responses into a unified summary using Amazon Bedrock Converse. + +Learn more about this workflow at Step Functions workflows collection: https://serverlessland.com/workflows/sfn-agentcore-bedrock-cdk + +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](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [Node.js 18+](https://nodejs.org/en/download/) installed +* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed +* [Amazon Bedrock model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) enabled for your chosen model (default: Claude Sonnet) in your target region +* One or more [Bedrock AgentCore agent runtimes](https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html) already deployed and accessible + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/step-functions-workflows-collection + ``` +1. Change directory to the pattern directory: + ``` + cd step-functions-workflows-collection/sfn-agentcore-bedrock-cdk + ``` +1. Install dependencies: + ``` + npm install + ``` +1. Deploy the stack with your AgentCore runtime ARNs: + ``` + cdk deploy \ + --parameters AgentRuntimeArns=arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/agent1,arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/agent2 \ + --parameters BedrockModelId=us.anthropic.claude-sonnet-4-20250514-v1:0 + ``` +1. Note the outputs from the CDK deployment process. These contain the resource names and/or ARNs which are used for testing. + +## How it works + +1. The **Trigger Lambda** receives a prompt and a list of AgentCore runtime ARNs, then starts the Step Functions state machine execution. +2. The **Map state** fans out to invoke each AgentCore agent in parallel. Each iteration calls the **Invoke Agent Lambda**, which uses the bundled `@aws-sdk/client-bedrock-agentcore` SDK to call `InvokeAgentRuntime` and collect the streaming response. +3. After all agents respond, the **Aggregate Lambda** uses Amazon Bedrock Converse (Claude) to synthesize all agent responses into a single coherent summary. +4. The state machine returns the aggregated summary along with the agent count. + +### Important Notes + +- **Bundled SDK**: The `@aws-sdk/client-bedrock-agentcore` package is not included in the Lambda runtime. The invoke-agent function uses `NodejsFunction` with esbuild bundling to include it. +- **Streaming Response**: `InvokeAgentRuntime` returns a streaming response, which cannot be consumed directly by Step Functions SDK integrations. A Lambda intermediary collects the stream and returns the full response. +- **Enforced Guardrails**: If your account has account-level enforced guardrails configured, all roles that call Bedrock must have `bedrock:ApplyGuardrail` permission. + +## Image + +![image](./resources/statemachine.png) + +## Testing + +1. Invoke the trigger function with a prompt and your agent runtime ARNs: + ```bash + aws lambda invoke \ + --function-name \ + --payload '{ + "prompt": "What are the best practices for building serverless applications?", + "agentRuntimeArns": [ + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/agent1", + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/agent2" + ] + }' \ + --cli-binary-format raw-in-base64-out \ + output.json + ``` + +2. The trigger returns the Step Functions execution ARN. Monitor the execution: + ```bash + aws stepfunctions describe-execution \ + --execution-arn + ``` + +3. Once the execution completes (status: `SUCCEEDED`), view the output which contains the aggregated summary from all agents. + +## Cleanup + +1. Delete the stack + ```bash + cdk destroy + ``` +1. Confirm the stack has been deleted + ```bash + aws cloudformation list-stacks --query "StackSummaries[?contains(StackName,'SfnAgentcoreBedrockStack')].StackStatus" + ``` +---- +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/sfn-agentcore-bedrock-cdk/bin/app.ts b/sfn-agentcore-bedrock-cdk/bin/app.ts new file mode 100644 index 00000000..80b11b80 --- /dev/null +++ b/sfn-agentcore-bedrock-cdk/bin/app.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { SfnAgentcoreBedrockStack } from '../lib/sfn-agentcore-bedrock-stack'; + +const app = new cdk.App(); +new SfnAgentcoreBedrockStack(app, 'SfnAgentcoreBedrockStack'); diff --git a/sfn-agentcore-bedrock-cdk/cdk.json b/sfn-agentcore-bedrock-cdk/cdk.json new file mode 100644 index 00000000..27fe6d2e --- /dev/null +++ b/sfn-agentcore-bedrock-cdk/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node bin/app.ts" +} diff --git a/sfn-agentcore-bedrock-cdk/example-workflow.json b/sfn-agentcore-bedrock-cdk/example-workflow.json new file mode 100644 index 00000000..6ca2188f --- /dev/null +++ b/sfn-agentcore-bedrock-cdk/example-workflow.json @@ -0,0 +1,82 @@ +{ + "title": "Step Functions with Bedrock AgentCore Multi-Agent Orchestration", + "description": "Use AWS Step Functions to orchestrate multiple Bedrock AgentCore agents in parallel via a Map state, then aggregate their responses into a unified summary using Bedrock Converse.", + "language": "TypeScript", + "simplicity": "3 - Application", + "usecase": "", + "type": "Standard", + "diagram": "/resources/statemachine.png", + "videoId": "", + "level": "400", + "framework": "CDK", + "services": ["bedrock", "lambda", "stepfunctions"], + "introBox": { + "headline": "How it works", + "text": [ + "This workflow deploys a Step Functions state machine that invokes multiple Bedrock AgentCore agents in parallel using a Map state, then aggregates their responses into a single coherent summary via Amazon Bedrock Converse.", + "The workflow: (1) A trigger Lambda starts the state machine with a prompt and list of agent runtime ARNs, (2) the Map state fans out to invoke each AgentCore agent in parallel via a bundled Lambda intermediary, (3) an aggregate Lambda uses Bedrock Converse to synthesize all agent responses into a unified summary.", + "Key architectural decisions: The @aws-sdk/client-bedrock-agentcore SDK is not available in the Lambda runtime and must be bundled using NodejsFunction. InvokeAgentRuntime returns a streaming response, requiring a Lambda intermediary rather than direct Step Functions SDK integration." + ] + }, + "testing": { + "headline": "Testing", + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "headline": "Cleanup", + "text": [ + "1. Delete the stack: cdk destroy." + ] + }, + "deploy": { + "text": [ + "cdk deploy --parameters AgentRuntimeArns= --parameters BedrockModelId=" + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/step-functions-workflows-collection/tree/main/sfn-agentcore-bedrock-cdk/", + "templateDir": "sfn-agentcore-bedrock-cdk", + "templateFile": "lib/sfn-agentcore-bedrock-stack.ts", + "ASL": "statemachine/statemachine.asl.json" + }, + "payloads": [ + { + "headline": "", + "payloadURL": "" + } + ] + }, + "resources": { + "headline": "Additional resources", + "bullets": [ + { + "text": "Amazon Bedrock AgentCore Documentation", + "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html" + }, + { + "text": "AWS Step Functions Map State", + "link": "https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-map-state.html" + }, + { + "text": "Amazon Bedrock Converse API", + "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html" + }, + { + "text": "The AWS Step Functions Workshop", + "link": "https://catalog.workshops.aws/stepfunctions/en-US" + } + ] + }, + "authors": [ + { + "name": "Nithin Chandran R", + "image": "https://avatars.githubusercontent.com/u/NithinChandranR-AWS", + "bio": "Nithin is a Technical Account Manager at AWS who builds serverless patterns and AI agent orchestration solutions.", + "linkedin": "nithin-chandran-r", + "twitter": "" + } + ] +} diff --git a/sfn-agentcore-bedrock-cdk/lib/sfn-agentcore-bedrock-stack.ts b/sfn-agentcore-bedrock-cdk/lib/sfn-agentcore-bedrock-stack.ts new file mode 100644 index 00000000..1cdea485 --- /dev/null +++ b/sfn-agentcore-bedrock-cdk/lib/sfn-agentcore-bedrock-stack.ts @@ -0,0 +1,162 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as sfn from 'aws-cdk-lib/aws-stepfunctions'; +import * as path from 'path'; + +export class SfnAgentcoreBedrockStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const agentRuntimeArnsParam = new cdk.CfnParameter(this, 'AgentRuntimeArns', { + type: 'String', + default: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/placeholder-agent', + description: 'Comma-separated list of AgentCore runtime ARNs', + }); + + const bedrockModelIdParam = new cdk.CfnParameter(this, 'BedrockModelId', { + type: 'String', + default: 'us.anthropic.claude-sonnet-4-20250514-v1:0', + description: 'Bedrock model ID for aggregation', + }); + + // Lambda to invoke a single AgentCore agent (bundled SDK for streaming support) + const invokeAgentFn = new lambdaNode.NodejsFunction(this, 'InvokeAgentFn', { + entry: path.join(__dirname, '..', 'src', 'invoke-agent.ts'), + handler: 'handler', + runtime: lambda.Runtime.NODEJS_20_X, + memorySize: 256, + timeout: cdk.Duration.minutes(3), + bundling: { + minify: true, + sourceMap: false, + }, + }); + + invokeAgentFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['bedrock-agentcore:InvokeAgentRuntime'], + resources: ['*'], + })); + + // Aggregate Lambda — summarizes multi-agent responses via Bedrock Converse + const aggregateFn = new lambda.Function(this, 'AggregateFn', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'aggregate.handler', + code: lambda.Code.fromAsset('src'), + memorySize: 256, + timeout: cdk.Duration.seconds(60), + environment: { + BEDROCK_MODEL_ID: bedrockModelIdParam.valueAsString, + }, + }); + + aggregateFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['bedrock:InvokeModel'], + resources: ['*'], + })); + + // Required when account-level enforced guardrails are active + aggregateFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['bedrock:ApplyGuardrail'], + resources: ['*'], + })); + + // SFN IAM Role + const sfnRole = new iam.Role(this, 'SfnRole', { + assumedBy: new iam.ServicePrincipal('states.amazonaws.com'), + }); + + sfnRole.addToPolicy(new iam.PolicyStatement({ + actions: ['lambda:InvokeFunction'], + resources: [invokeAgentFn.functionArn, aggregateFn.functionArn], + })); + + // State machine: Map → invoke agents in parallel → aggregate + const definition = { + Comment: 'Orchestrate multiple AgentCore agents in parallel and aggregate responses with Bedrock', + StartAt: 'InvokeAgents', + States: { + InvokeAgents: { + Type: 'Map', + ItemsPath: '$.agentRuntimeArns', + Parameters: { + 'agentRuntimeArn.$': '$$.Map.Item.Value', + 'prompt.$': '$.prompt', + }, + Iterator: { + StartAt: 'CallAgent', + States: { + CallAgent: { + Type: 'Task', + Resource: 'arn:aws:states:::lambda:invoke', + Parameters: { + FunctionName: invokeAgentFn.functionArn, + 'Payload.$': '$', + }, + OutputPath: '$.Payload', + Retry: [ + { + ErrorEquals: ['States.TaskFailed'], + IntervalSeconds: 15, + MaxAttempts: 2, + BackoffRate: 2, + }, + ], + End: true, + }, + }, + }, + ResultPath: '$.agentResults', + Next: 'AggregateResults', + }, + AggregateResults: { + Type: 'Task', + Resource: 'arn:aws:states:::lambda:invoke', + Parameters: { + FunctionName: aggregateFn.functionArn, + 'Payload.$': '$', + }, + OutputPath: '$.Payload', + End: true, + }, + }, + }; + + const stateMachine = new sfn.CfnStateMachine(this, 'StateMachine', { + definitionString: JSON.stringify(definition), + roleArn: sfnRole.roleArn, + stateMachineType: 'STANDARD', + }); + + // Trigger Lambda + const triggerFn = new lambda.Function(this, 'TriggerFn', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'trigger.handler', + code: lambda.Code.fromAsset('src'), + memorySize: 256, + timeout: cdk.Duration.seconds(60), + environment: { + STATE_MACHINE_ARN: stateMachine.attrArn, + }, + }); + + triggerFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['states:StartExecution'], + resources: [stateMachine.attrArn], + })); + + new cdk.CfnOutput(this, 'StateMachineArn', { + value: stateMachine.attrArn, + }); + + new cdk.CfnOutput(this, 'TriggerFunctionName', { + value: triggerFn.functionName, + }); + + new cdk.CfnOutput(this, 'TriggerFunctionArn', { + value: triggerFn.functionArn, + }); + } +} diff --git a/sfn-agentcore-bedrock-cdk/package.json b/sfn-agentcore-bedrock-cdk/package.json new file mode 100644 index 00000000..40c9fc12 --- /dev/null +++ b/sfn-agentcore-bedrock-cdk/package.json @@ -0,0 +1,23 @@ +{ + "name": "sfn-agentcore-bedrock-cdk", + "version": "1.0.0", + "bin": { + "app": "bin/app.ts" + }, + "scripts": { + "build": "tsc", + "cdk": "cdk" + }, + "dependencies": { + "@aws-sdk/client-bedrock-agentcore": "^3.1032.0", + "aws-cdk-lib": "2.180.0", + "constructs": "10.4.2", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "esbuild": "^0.28.0", + "ts-node": "^10.9.0", + "typescript": "~5.4.0" + } +} diff --git a/sfn-agentcore-bedrock-cdk/resources/statemachine.png b/sfn-agentcore-bedrock-cdk/resources/statemachine.png new file mode 100644 index 00000000..79c5ec20 Binary files /dev/null and b/sfn-agentcore-bedrock-cdk/resources/statemachine.png differ diff --git a/sfn-agentcore-bedrock-cdk/src/aggregate.mjs b/sfn-agentcore-bedrock-cdk/src/aggregate.mjs new file mode 100644 index 00000000..d3dcc9d7 --- /dev/null +++ b/sfn-agentcore-bedrock-cdk/src/aggregate.mjs @@ -0,0 +1,33 @@ +import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime'; + +const bedrockClient = new BedrockRuntimeClient(); +const MODEL_ID = process.env.BEDROCK_MODEL_ID; + +export const handler = async (event) => { + const agentResults = event.agentResults || []; + + const responseSummary = agentResults.map((r, i) => + `Agent ${i + 1} (${r.agentRuntimeArn || 'unknown'}): ${r.response || 'No response'}` + ).join('\n\n'); + + const result = await bedrockClient.send(new ConverseCommand({ + modelId: MODEL_ID, + messages: [ + { + role: 'user', + content: [ + { + text: `Summarize and synthesize the following responses from multiple AI agents into a single coherent answer:\n\n${responseSummary}`, + }, + ], + }, + ], + })); + + const outputText = result.output?.message?.content?.[0]?.text || ''; + + return { + summary: outputText, + agentCount: agentResults.length, + }; +}; diff --git a/sfn-agentcore-bedrock-cdk/src/invoke-agent.ts b/sfn-agentcore-bedrock-cdk/src/invoke-agent.ts new file mode 100644 index 00000000..1295de75 --- /dev/null +++ b/sfn-agentcore-bedrock-cdk/src/invoke-agent.ts @@ -0,0 +1,33 @@ +import { BedrockAgentCoreClient, InvokeAgentRuntimeCommand } from '@aws-sdk/client-bedrock-agentcore'; + +const client = new BedrockAgentCoreClient(); + +export const handler = async (event) => { + const { agentRuntimeArn, prompt } = event; + + const response = await client.send(new InvokeAgentRuntimeCommand({ + agentRuntimeArn, + payload: Buffer.from(JSON.stringify({ prompt })), + contentType: 'application/json', + accept: 'application/json', + })); + + // Collect streaming response + let fullResponse = ''; + if (response.response) { + const reader = response.response; + if (typeof reader[Symbol.asyncIterator] === 'function') { + for await (const chunk of reader) { + fullResponse += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk); + } + } else { + fullResponse = reader.toString(); + } + } + + return { + agentRuntimeArn, + response: fullResponse || 'No response', + sessionId: response.runtimeSessionId, + }; +}; diff --git a/sfn-agentcore-bedrock-cdk/src/trigger.mjs b/sfn-agentcore-bedrock-cdk/src/trigger.mjs new file mode 100644 index 00000000..4a7d2ca8 --- /dev/null +++ b/sfn-agentcore-bedrock-cdk/src/trigger.mjs @@ -0,0 +1,18 @@ +import { SFNClient, StartExecutionCommand } from '@aws-sdk/client-sfn'; + +const sfnClient = new SFNClient(); +const STATE_MACHINE_ARN = process.env.STATE_MACHINE_ARN; + +export const handler = async (event) => { + const { prompt, agentRuntimeArns } = event; + + const result = await sfnClient.send(new StartExecutionCommand({ + stateMachineArn: STATE_MACHINE_ARN, + input: JSON.stringify({ prompt, agentRuntimeArns }), + })); + + return { + statusCode: 200, + body: JSON.stringify({ executionArn: result.executionArn }), + }; +}; diff --git a/sfn-agentcore-bedrock-cdk/statemachine/statemachine.asl.json b/sfn-agentcore-bedrock-cdk/statemachine/statemachine.asl.json new file mode 100644 index 00000000..27885431 --- /dev/null +++ b/sfn-agentcore-bedrock-cdk/statemachine/statemachine.asl.json @@ -0,0 +1,49 @@ +{ + "Comment": "Orchestrate multiple AgentCore agents in parallel and aggregate responses with Bedrock", + "StartAt": "InvokeAgents", + "States": { + "InvokeAgents": { + "Type": "Map", + "ItemsPath": "$.agentRuntimeArns", + "Parameters": { + "agentRuntimeArn.$": "$$.Map.Item.Value", + "prompt.$": "$.prompt" + }, + "Iterator": { + "StartAt": "CallAgent", + "States": { + "CallAgent": { + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "Parameters": { + "FunctionName": "${InvokeAgentFunctionArn}", + "Payload.$": "$" + }, + "OutputPath": "$.Payload", + "Retry": [ + { + "ErrorEquals": ["States.TaskFailed"], + "IntervalSeconds": 15, + "MaxAttempts": 2, + "BackoffRate": 2 + } + ], + "End": true + } + } + }, + "ResultPath": "$.agentResults", + "Next": "AggregateResults" + }, + "AggregateResults": { + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "Parameters": { + "FunctionName": "${AggregateFunctionArn}", + "Payload.$": "$" + }, + "OutputPath": "$.Payload", + "End": true + } + } +} diff --git a/sfn-agentcore-bedrock-cdk/tsconfig.json b/sfn-agentcore-bedrock-cdk/tsconfig.json new file mode 100644 index 00000000..0da3f6ef --- /dev/null +++ b/sfn-agentcore-bedrock-cdk/tsconfig.json @@ -0,0 +1,25 @@ +{ + "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, + "outDir": "./build", + "rootDir": ".", + "skipLibCheck": true + }, + "exclude": ["node_modules", "cdk.out"] +}