Amazon Bedrock AgentCore: A hands-on guide to production AI agents

  • Blog
  • 12 minute read
  • July 2026

Jay Kumar

Senior Manager, Advisory (CEDA), PwC US

Vidyashankar Venkataraman

Senior Manager, Advisory (CEDA), PwC US

Key takeaways:

  • Moving AI agents from demo to production is a key challenge for organizations.
  • Amazon Bedrock AgentCore provides modular services to operationalize agents at scale.
  • Services like runtime, memory, and gateway simplify infrastructure and integration.
  • Built-in security, observability, and policy controls improve reliability and governance.
  • Combining services enables production-ready, scalable AI agent architectures.

How Amazon Bedrock AgentCore enables production-ready AI agents

Amazon Bedrock AgentCore is a modular platform designed to help organizations move AI agents from early prototypes into production environments. It provides services for deployment, memory, integrations, identity, observability, and governance—removing the complexity of building and maintaining infrastructure

From demo to production: A hands-on guide to each Amazon Bedrock AgentCore service

If you have been building AI agents and feeling the pain of moving them from a laptop demo to something that actually runs in production, you are not alone. That gap between "it works on my machine" and "it works for ten thousand users" is where most agent projects quietly fall apart.

Amazon Bedrock AgentCore was built to close that gap. It is a platform made up of several pieces, and each one solves a specific problem you run into when you try to take an agent to production. Things like deploying it, giving it memory, connecting it to your APIs, handling user logins, and watching what it does once real users start hitting it.

In this post I am going to walk through each service in AgentCore. I will explain what each one does, when you would actually reach for it, and give you Python code that runs. No hand-waving. No "this is left as an exercise for the reader." Just real examples.

Let us get into it.

The big picture

Before diving into each service, here is a quick map of the offerings. Nine services, each focused on one job, and you can mix and match them however you like.

Before you run the code

Before any of the code below works, you will need a few basics in place:

  • An AWS account with credentials configured (run aws configure if you have not already)

  • Python 3.10 or newer

  • Model access enabled for Anthropic Claude models in the Bedrock console (Claude Sonnet, for example)

  • The AgentCore SDK installed

pip install bedrock-agentcore bedrock-agentcore-starter-toolkit boto3 strands-agents 

A note on regions: AgentCore is generally available in several regions but not all. The examples below use us-east-1, but pick whichever region you have access to and substitute it consistently.

1. AgentCore Runtime

What it does

AgentCore Runtime is where your agent actually runs. It is a serverless, managed environment, so you do not deal with containers, load balancers, or auto-scaling. You hand it your agent code and it runs in an isolated microVM with full session isolation.

The big things going for it: each session runs in its own sandbox (so one user’s data never leaks into another’s), it can handle long-running tasks up to eight hours, and it works with any framework you like. Strands, etc. Or any of your own custom thing, all fine.

Use case: an internal company FAQ agent

Picture this. You are building an internal FAQ bot for your company. It needs to answer employee questions about HR policies, leave rules, expense limits, that kind of thing. You have built it locally, it works, and now you want to give the whole team access to it.

The code

Step 1: Write your agent in a file called my_agent.py.

from strands import Agent 
from bedrock_agentcore.runtime import BedrockAgentCoreApp 

agent = Agent( 
    system_prompt="""You are an internal company FAQ assistant. 
    Answer questions about company policies clearly and briefly. 
    If you do not know something, say so honestly.""" 
) 

app = BedrockAgentCoreApp() 

@app.entrypoint 
def invoke(payload): 
    """Handle a request from a user.""" 
    user_question = payload.get("prompt", "Hello") 
    result = agent(user_question) 
    return {"result": str(result)} 

if __name__ == "__main__": 
    app.run() 

Note the if __name__ == "__main__": app.run() at the bottom. Without it, the AgentCore HTTP server never actually starts when the container runs.

Step 2: Deploy it.

mkdir faq-agent && cd faq-agent 
python3 -m venv .venv 
source .venv/bin/activate 

pip install strands-agents bedrock-agentcore bedrock-agentcore-starter-toolkit 

# Tell the toolkit about your agent 
agentcore configure --entrypoint my_agent.py 

 # Deploy. CodeBuild handles the container build, no Docker needed locally. 
agentcore launch 

The CLI will print out an ARN for the deployed agent. Hold on to that. You need it to call the agent later.

Step 3: Call the deployed agent.

import boto3 
import json 
import uuid 

client = boto3.client("bedrock-agentcore", region_name="us-east-1") 

# Replace with the ARN you got from agentcore launch 
agent_arn = "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/my-faq-agent-XXXX" 

payload = json.dumps({ 
    "prompt": "What is our remote work policy?" 
}).encode() 

response = client.invoke_agent_runtime( 
    agentRuntimeArn=agent_arn, 
    runtimeSessionId=str(uuid.uuid4()), 
    payload=payload, 
) 

# Read the streamed response 
content = [] 
for chunk in response.get("response", []): 
    content.append(chunk.decode("utf-8")) 

print(json.loads("".join(content))) 

Two things to notice. First, the streamed response lives in response["response"] (a StreamingBody), not response["body"]. Second, each chunk is just raw bytes you decode straight to a string, no nested {"chunk": {"bytes": ...}} wrapper to unwrap.

Why it matters

Without Runtime, you would be setting up ECS or EKS, writing Dockerfiles, fighting with networking, dealing with cold starts, and writing your own session management. Runtime takes all of that off your plate. You write the agent. AWS runs it.

2. AgentCore Memory

What it does

Memory lets your agent remember things. Not just within a single chat, but across many chats, over days and weeks.

It comes with three built-in long-term memory strategies you can turn on:

  • Semantic Memory: pulls out facts from conversations and stores them.

  • User Preference Memory: learns what individual users like.

  • Summary Memory: writes condensed summaries of past sessions.

Use case: a shopping assistant that remembers

Imagine a shopping assistant that learns about a user over time. First chat the user mentions they are vegetarian. A few chats later they say they prefer mid-range prices. By the fifth chat, the agent knows all of this and can suggest things without the user having to repeat themselves every time.

The code

Step 1: Create a memory resource with a couple of strategies.

from bedrock_agentcore.memory import MemoryClient 
import time 

client = MemoryClient(region_name="us-east-1") 

memory = client.create_memory_and_wait( 
    name="ShoppingAssistantMemory", 
    description="Remembers shopper preferences across sessions", 
    strategies=[ 
        { 
            "userPreferenceMemoryStrategy": { 
                "name": "ShopperPreferences", 
                "namespaces": ["/users/{actorId}"] 
            } 
        }, 
        { 
            "summaryMemoryStrategy": { 
                "name": "SessionSummarizer", 
                "namespaces": ["/summaries/{actorId}/{sessionId}/"] 
            } 
        } 
    ] 
) 

print(f"Memory created with ID: {memory.get('id')}") 

Step 2: Save a conversation as a memory event.

client.create_event( 
    memory_id=memory.get("id"), 
    actor_id="shopper_jane_42", 
    session_id="session_march_01", 
    messages=[ 
        ("Hi, I am looking for dinner ideas", "USER"), 
        ("Sure. Any dietary preferences I should know about?", "ASSISTANT"), 
        ("I am vegetarian and I love Italian food", "USER"), 
        ("Got it. How about a classic eggplant parmigiana?", "ASSISTANT"), 
        ("That sounds great. I usually cook for two people.", "USER"), 
    ], 
) 
print("Conversation stored.") 

Step 3: Pull insights back later.

# Memory strategies run asynchronously in the background. 
# For a demo a fixed sleep is fine, but in real code poll for the 
# memory records to appear instead of sleeping a fixed amount. 
time.sleep(60) 

memories = client.retrieve_memories( 
    memory_id=memory.get("id"), 
    namespace="/users/shopper_jane_42", 
    query="What does this customer prefer for food?" 
) 

for mem in memories: 
    print(f"Memory: {mem}") 

Step 4: Plug memory straight into a Strands agent.

from strands import Agent 
from bedrock_agentcore.memory.integrations.strands.config import ( 
    AgentCoreMemoryConfig 
) 
from bedrock_agentcore.memory.integrations.strands.session_manager import ( 
    AgentCoreMemorySessionManager 
) 

config = AgentCoreMemoryConfig( 
    memory_id="your-memory-id-here", 
    session_id="session_march_02", 
    actor_id="shopper_jane_42", 
) 

with AgentCoreMemorySessionManager(config, region_name="us-east-1") as sess: 
    agent = Agent( 
        system_prompt="""You are a personal shopping assistant. 
        Use what you know about the user to suggest things they will like.""", 
        session_manager=sess, 
    ) 

    # The agent already knows: vegetarian, loves Italian, cooks for two 
    agent("What should I make for dinner tonight?") 

Why it matters

Building memory yourself means dealing with vector databases, writing extraction pipelines, handling session state, and figuring out retrieval. Memory does all of that as a managed service, so you can focus on building capabilities rather than maintaining infrastructure.

3. AgentCore Gateway

What it does

Agents require tools to do useful work. Calling APIs, running queries, sending messages. Gateway takes your existing APIs, Lambda functions, or MCP servers and makes them available to your agents as tools.

Everything ends up exposed as Model Context Protocol (MCP) endpoints, so your agents get a single, consistent way to find and call tools.

Use case: giving an agent access to your order API

Say you have a REST API that handles order lookups. You want your support agent to be able to check order status, but rewriting your API is not on the table. Gateway lets you point at the existing API and expose it as a tool. No code changes to the API.

The code

Step 1: Set up your project.

mkdir agentcore-gateway-quickstart && cd agentcore-gateway-quickstart 
python3 -m venv .venv 
source .venv/bin/activate 
pip install bedrock-agentcore bedrock-agentcore-starter-toolkit strands-agents mcp 

Step 2: Create a Gateway with a Lambda as a tool target.

Gateways are protected by an OAuth authorizer. The easiest way to set one up is to let the toolkit create a Cognito-backed authorizer for you.

from bedrock_agentcore_starter_toolkit.operations.gateway.client import ( 
    GatewayClient 
) 
import json 
  
client = GatewayClient(region_name="us-east-1") 
  
# 1. Create a Cognito OAuth authorizer (required for the gateway). 
cognito_response = client.create_oauth_authorizer_with_cognito( 
    "OrderManagementGateway" 
) 
  
# 2. Create the gateway itself, wired up to that authorizer. 
gateway = client.create_mcp_gateway( 
    name="OrderManagementGateway", 
    role_arn=None,  # let the toolkit create one 
    authorizer_config=cognito_response["authorizer_config"], 
    enable_semantic_search=True, 
) 
  
# 3. Add your Lambda function as a tool target. 
lambda_target = client.create_mcp_gateway_target( 
    gateway=gateway, 
    name="OrderLookupTool", 
    target_type="lambda", 
    target_payload={ 
        "lambdaArn": "arn:aws:lambda:us-east-1:123456789012:function:order-lookup", 
        "toolSchema": { 
            "inlinePayload": [ 
                { 
                    "name": "lookup_order", 
                    "description": "Look up order status by order ID", 
                    "inputSchema": { 
                        "type": "object", 
                        "properties": { 
                            "order_id": { 
                                "type": "string", 
                                "description": "The order ID" 
                            } 
                        }, 
                        "required": ["order_id"] 
                    } 
                } 
            ] 
        } 
    }, 
) 
  
# 4. Save what the agent will need. 
config = { 
    "gateway_id": gateway["gatewayId"], 
    "gateway_url": gateway["gatewayUrl"], 
    "region": "us-east-1", 
    "client_info": cognito_response["client_info"], 
} 
with open("gateway_config.json", "w") as f: 
    json.dump(config, f) 
  
print(f"Gateway created: {gateway['gatewayId']}") 
print(f"MCP URL: {gateway['gatewayUrl']}") 

Notice the response keys are gatewayId and gatewayUrl (camelCase). Easy to get incorrect.

Step 3: Hook your agent up to the Gateway.

from strands import Agent 
from strands.models import BedrockModel 
from strands.tools.mcp.mcp_client import MCPClient 
from mcp.client.streamable_http import streamablehttp_client 
from bedrock_agentcore_starter_toolkit.operations.gateway.client import ( 
    GatewayClient 
) 
import json 
  
with open("gateway_config.json", "r") as f: 
    config = json.load(f) 
  
# Fetch a fresh OAuth token from the Cognito authorizer we set up earlier. 
gw_client = GatewayClient(region_name=config["region"]) 
access_token = gw_client.get_access_token_for_cognito(config["client_info"]) 
  
def create_transport(mcp_url, token): 
    return streamablehttp_client( 
        mcp_url, 
        headers={"Authorization": f"Bearer {token}"} 
    ) 
  
mcp_client = MCPClient( 
    lambda: create_transport(config["gateway_url"], access_token) 
) 
  
with mcp_client: 
    agent = Agent( 
        model=BedrockModel( 
            model_id="us.anthropic.claude-sonnet-4-20250514-v1:0" 
        ), 
        tools=mcp_client.list_tools_sync(), 
    ) 
    response = agent("What is the status of order #78234?") 
    print(response) 

Two things worth calling out. First, the access token is fetched programmatically, not hard-coded. Second, the model ID uses the us. cross-region inference prefix, which is what you need for Claude Sonnet 4 on Bedrock in US regions.

Why it matters

Without Gateway, you would be writing custom tool integrations for each API, handling auth for each one separately, running your own MCP server, and keeping tool definitions in sync as things change. Gateway turns what you already have into agent-ready tools with very little extra work.

4. AgentCore Identity

What it does

When agents need to access third-party services like Google Drive, Slack, Salesforce, or GitHub on behalf of a user, identity can quickly become complex. AgentCore Identity handles both sides:

  • Inbound Auth: figuring out who is calling your agent (using JWT tokens from Cognito, Okta, Entra, and so on).

  • Outbound Auth: getting OAuth tokens so your agent can act on a user’s behalf with external services.

It looks after the whole OAuth dance: getting tokens, storing them in a secure vault, refreshing them, and slipping them into your agent code when needed.

Use case: an agent that reads files from Google Drive

Imagine a research assistant agent that can pull documents from a user’s Google Drive, summarize them, and answer questions about them, while still respecting the user’s consent and Google permissions.

The code

Step 1: Create an OAuth credential provider for Google.

aws bedrock-agentcore-control create-oauth2-credential-provider \ 
    --name "GoogleDriveProvider" \ 
    --credential-provider-vendor GoogleOauth2 \ 
    --oauth2-provider-config-input '{ 
        "googleOauth2ProviderConfig": { 
            "clientId": "YOUR_GOOGLE_CLIENT_ID", 
            "clientSecret": "YOUR_GOOGLE_CLIENT_SECRET" 
        } 
    }' 

Step 2: Use the @requires_access_token decorator in your agent.

A few things matter here. The decorator must be applied to an async function, the function must accept access_token as a keyword-only argument, and you invoke it via asyncio.run().

import os 
import asyncio 
from bedrock_agentcore.identity.auth import requires_access_token 
  
os.environ["AWS_REGION"] = "us-east-1" 
  
@requires_access_token( 
    provider_name="GoogleDriveProvider", 
    scopes=["https://www.googleapis.com/auth/drive.readonly"], 
    auth_flow="USER_FEDERATION", 
    on_auth_url=lambda url: print(f"\nVisit this URL to grant access:\n{url}\n"), 
    force_authentication=False, 
) 
async def list_drive_files(*, access_token: str): 
    """List the user's Google Drive files.""" 
    from googleapiclient.discovery import build 
    from google.oauth2.credentials import Credentials 
  
    creds = Credentials(token=access_token) 
    service = build("drive", "v3", credentials=creds) 
  
    results = service.files().list( 
        pageSize=10, 
        fields="files(id, name, mimeType)" 
    ).execute() 
  
    files = results.get("files", []) 
    for f in files: 
        print(f"  {f['name']} ({f['mimeType']})") 
    return files 
  
# When this runs, the user gets a URL to grant consent. 
# After that, the agent gets the token automatically. 
files = asyncio.run(list_drive_files()) 

Step 3: Or use API key auth for simpler services.

import asyncio 
from bedrock_agentcore.identity.auth import requires_api_key 
  
@requires_api_key( 
    provider_name="WeatherAPIProvider" 
) 
async def get_weather(city: str, *, api_key: str): 
    import requests 
    url = f"https://api.weatherservice.com/v1/current?city={city}" 
    response = requests.get(url, headers={"X-API-Key": api_key}) 
    return response.json() 
  
result = asyncio.run(get_weather("Seattle")) 
print(result) 

Same pattern as the OAuth case: async function, api_key as a keyword-only argument, the decorator just takes provider_name.

Why it matters

OAuth flows are really challenging to get right. Token storage, refresh logic, consent flows, secret management. Lots of places to mess up the security. Identity handles all of it with a decorator, and tokens live in a managed vault. You write agent logic, not auth plumbing.

5. AgentCore Observability

What it does

Once your agent is running for real users, you should know what it is doing. Observability gives you metrics, traces, and logs flowing into Amazon CloudWatch.

It uses OpenTelemetry under the hood, so if you already use tools like Datadog, Elastic, Dynatrace, or LangSmith, you can pipe data there too.

What you can track: token usage, latency for each step, error rates, session length, and the full step-by-step decision tree of what the agent did.

Use case: monitoring a production support agent

You have got a customer support agent in production. You want to know if response times are okay, which tools are failing, how many tokens you are burning per session, and where errors cluster.

The code

Step 1: Turn on CloudWatch Transaction Search.

This is a one-time setup per AWS account. The exact policy is documented in the AgentCore "Enabling runtime observability" guide. Follow that rather than copy-pasting a partial policy here, since the resource policy AgentCore needs is more involved than just two log actions, and getting it incorrect can silently break trace delivery.

Step 2: Add OTEL instrumentation to your agent.

Add these to requirements.txt:

strands-agents[otel] 
aws-opentelemetry-distro 
bedrock-agentcore 

Then instrument your agent:

from strands import Agent 
from bedrock_agentcore.runtime import BedrockAgentCoreApp 
from opentelemetry import trace 
  
# When running inside AgentCore Runtime, traces flow to CloudWatch 
# automatically. No extra setup. 
  
agent = Agent(system_prompt="You are a helpful customer support agent.") 
app = BedrockAgentCoreApp() 
  
@app.entrypoint 
def invoke(payload): 
    user_msg = payload.get("prompt", "") 
  
    # Add your own custom spans for business logic 
    tracer = trace.get_tracer("support-agent") 
    with tracer.start_as_current_span("process_support_request") as span: 
        span.set_attribute("user.query_length", len(user_msg)) 
        span.set_attribute("request.type", "customer_support") 
  
        result = agent(user_msg) 
  
        span.set_attribute("response.token_count", len(str(result).split())) 
  
    return {"result": str(result)} 
  
if __name__ == "__main__": 
    app.run() 

Step 3: Call it with a session ID so things get grouped.

import boto3, json 
from datetime import datetime 
  
client = boto3.client("bedrock-agentcore", region_name="us-east-1") 
session_id = f"user_42_{datetime.now().strftime('%Y%m%d_%H%M%S')}" 
  
questions = [ 
    "I cannot log into my account", 
    "I have already tried resetting my password", 
    "Can you escalate this to a human?" 
] 
  
agent_arn = "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/support-agent-XXXX" 
  
for q in questions: 
    response = client.invoke_agent_runtime( 
        agentRuntimeArn=agent_arn, 
        payload=json.dumps({"prompt": q}).encode(), 
        runtimeSessionId=session_id, 
    ) 
    content = [] 
    for chunk in response.get("response", []): 
        content.append(chunk.decode("utf-8")) 
    print("".join(content)) 
    print("\n---") 

Two important details. The parameter for grouping calls is runtimeSessionId, not sessionId. And the streamed response is read from response["response"], one byte chunk at a time.

Step 4: Open the data in CloudWatch.

After a few invocations, head over to the CloudWatch console:

  • Go to Application Signals, then Generative AI.

  • Click Bedrock AgentCore in the left panel.

  • You will see three views: Agents (overall metrics), Sessions (per-session detail), and Traces (step-by-step execution).

From there you can also wire up CloudWatch alarms for things like error rate spikes or latency creeping up.

Why it matters

Running an agent without observability is like driving at night with the headlights off. Issues will likely occur. When they do, you should see exactly where in the chain the problem happened. Was it a model timeout? A tool error? A bad prompt? Observability shows you that without you having to build a monitoring stack yourself.

6. AgentCore Code Interpreter

What it does

Sometimes your agent needs to actually run code to answer a question. Doing math, processing some data, generating a chart. Code Interpreter gives the agent a sandbox where it can write code and run it safely.

Each run happens in an isolated sandbox so one agent’s code cannot affect another’s. It supports multiple languages and handles the full loop: write code, run code, capture the output.

Use case: a data analyst agent that runs real math

You have a financial assistant. Users ask things like "What is the compound annual growth rate if I invest $10,000 at 7% for 15 years?" Instead of guessing, the agent writes Python, runs it, and gives back the exact answer.

The code

The SDK gives you a CodeInterpreter class that manages a session for you. You call start(), then invoke("executeCode", {...}) however many times you need, then stop().

from bedrock_agentcore.tools.code_interpreter_client import CodeInterpreter 
  
code_client = CodeInterpreter("us-east-1") 
code_client.start() 
  
try: 
    response = code_client.invoke("executeCode", { 
        "language": "python", 
        "code": """ 
import math 
  
principal = 10000 
rate = 0.07 
years = 15 
  
future_value = principal * (1 + rate) ** years 
cagr = (future_value / principal) ** (1 / years) - 1 
  
print(f"Initial Investment: ${principal:,.2f}") 
print(f"Annual Rate: {rate:.1%}") 
print(f"Time Period: {years} years") 
print(f"Future Value: ${future_value:,.2f}") 
print(f"Total Return: ${future_value - principal:,.2f}") 
print(f"CAGR: {cagr:.2%}") 
""" 
    }) 
  
    # The response is a stream of events; pull out the text content. 
    print("Code Output:") 
    for event in response["stream"]: 
        result = event.get("result", {}) 
        for item in result.get("content", []): 
            if item.get("type") == "text": 
                print(item["text"]) 
finally: 
    code_client.stop() 

There is also a context-manager helper, code_session, that handles start and stop for you:

from bedrock_agentcore.tools.code_interpreter_client import code_session 
  
with code_session("us-east-1") as code_client: 
    response = code_client.invoke("executeCode", { 
        "language": "python", 
        "code": "print(2 + 2)" 
    }) 

Using it as a tool inside a Strands agent:

from strands import Agent 
from strands_tools.code_interpreter import AgentCoreCodeInterpreter 
  
code_interpreter_tool = AgentCoreCodeInterpreter(region="us-east-1") 
  
agent = Agent( 
    system_prompt="""You are a data analyst. When users ask quantitative 
    questions, write and run Python code to get accurate answers. 
    Always show your work.""", 
    tools=[code_interpreter_tool.code_interpreter], 
) 
  
response = agent( 
    "If a company's revenue was $2.3M in 2020 and $4.1M in 2025, " 
    "what is the compound annual growth rate?" 
) 
print(response) 

The Strands integration lives in the strands_tools.code_interpreter module (not in the bedrock_agentcore package), and you pass tool_object.code_interpreter into the agent, not the wrapper itself.

Why it matters

Letting an LLM do math in its head is asking for incorrect answers. Code Interpreter lets the agent write actual code, run it in a sandbox, and return the right number. The difference between "around $27,000" and the exact figure down to the cent.

7. AgentCore Browser

What it does

Browser gives your agent a managed cloud browser. It can visit websites, fill out forms, click around, and pull information. Think of it as giving your agent eyes and hands for the web.

Each browser instance runs in a secure sandbox. It also supports Web Bot Auth (currently in preview), a draft IETF protocol that cryptographically identifies AI agents to websites, which helps cut down on CAPTCHA blocks for legitimate automation.

Use case: tracking competitor prices on the web

You want an agent that visits competitor product pages every day, pulls the current prices, and puts together a comparison report.

The code

The high-level path is to use the Strands integration, which gives your agent a browser tool it can drive on its own:

from strands import Agent 
from strands_tools.browser import AgentCoreBrowser 
  
browser_tool = AgentCoreBrowser(region="us-east-1") 
  
agent = Agent( 
    system_prompt="""You are a market research assistant. When asked to 
    check competitor pricing, use the browser tool to visit the relevant 
    sites, pull pricing info, and put together a clean comparison.""", 
    tools=[browser_tool.browser], 
) 
  
response = agent( 
    "Visit example-competitor.com/pricing and pull their current " 
    "prices for the Basic, Pro, and Enterprise tiers." 
) 
print(response) 

If you want lower-level control:

The BrowserClient does not expose navigate / screenshot / get_page_content directly. Instead, it gives you a managed browser session you connect to over the Chrome DevTools Protocol, and you drive the browser with Playwright (or browser-use, or any CDP client).

from playwright.sync_api import sync_playwright 
from bedrock_agentcore.tools.browser_client import browser_session 
  
with browser_session("us-east-1") as client: 
    ws_url, headers = client.generate_ws_headers() 
  
    with sync_playwright() as p: 
        # Connect to the AgentCore-hosted browser 
        browser = p.chromium.connect_over_cdp(ws_url, headers=headers) 
        context = browser.contexts[0] 
        page = context.pages[0] 
  
        # Now you have a real Playwright Page to drive 
        page.goto("https://example.com/pricing") 
        title = page.title() 
        print(f"Page title: {title}") 
  
        # Get visible text 
        text = page.locator("body").inner_text() 
        print(text[:500]) 
  
        # Screenshot to a file 
        page.screenshot(path="pricing.png") 
  
        browser.close() 

The key idea: AgentCore Browser runs the actual browser in the cloud. You drive it remotely with Playwright over a WebSocket that generate_ws_headers() gives you.

Why it matters

A lot of useful business data lives on websites that do not have APIs. Browser lets your agents go get it the same way a human would, but at scale, on a schedule, without anyone getting tired.

8. AgentCore Policy

What it does

As agents do more, you need ways to observe how they operate. Policy lets you set the rules for what your agent is allowed to do and what it is not. You can author the rules in Cedar directly (the same policy language used by AWS Verified Permissions), or describe them in natural language and let AgentCore generate the Cedar for you. Either way, the rules are applied in real time through the Gateway.

Each tool call goes through the policy check before it runs. If the action breaks a rule, it gets blocked. No exceptions.

Use case: putting a refund cap on a support agent

Your support agent can issue refunds. But you only want it to issue refunds up to $100 on its own. Anything larger should require a human to approve.

The code

There are two pieces involved: a policy engine (which is associated with a gateway) and one or more policies inside that engine.

Step 1: Create a policy engine for your gateway.

import boto3 
  
client = boto3.client("bedrock-agentcore-control", region_name="us-east-1") 
  
engine = client.create_policy_engine( 
    name="SupportAgentPolicyEngine", 
    description="Policies for the customer support agent", 
    gatewayIdentifier="your-gateway-id",  # ID from create_mcp_gateway 
) 
  
policy_engine_id = engine["policyEngineId"] 
print(f"Policy engine created: {policy_engine_id}") 

Step 2: Create a Cedar policy in that engine.

Policies are written in Cedar. The example below permits refund tool calls only when the amount is at or below 100.

cedar_statement = ''' 
permit ( 
    principal, 
    action == Action::"issue_refund", 
    resource 
) 
when { 
    context.amount <= 100 
}; 
''' 
  
response = client.create_policy( 
    policyEngineId=policy_engine_id, 
    name="RefundLimitPolicy", 
    description="Caps automatic refunds at $100", 
    definition={ 
        "cedar": { 
            "statement": cedar_statement 
        } 
    }, 
    validationMode="FAIL_ON_ANY_FINDINGS", 
) 
  
print(f"Policy created: {response['policyId']}") 

Prefer to describe the rule in plain English?

AgentCore has a separate API, start_policy_generation, that takes natural-language intent and produces Cedar you can review and then pass to create_policy. Useful when you do not want to hand-write Cedar.

# Convert plain English to Cedar, review it, then create the policy. 
gen = client.start_policy_generation( 
    policyEngineId=policy_engine_id, 
    name="refund_limit_nl", 
    content={ 
        "rawText": ( 
            "The agent can issue refunds up to $100 without approval. " 
            "For refunds above $100, the agent must escalate to a human " 
            "supervisor. The agent must never issue refunds above $500 " 
            "under any circumstances." 
        ) 
    }, 
) 
print(f"Generation started: {gen['policyGenerationId']}") 
# Poll get_policy_generation to retrieve the generated Cedar, 
# then pass it into create_policy as shown above. 

How it actually plays out:

When your agent calls issue_refund(amount=75), the call goes through the Gateway, the policy engine checks it against your rules, and it goes through because $75 is under the $100 cap.

When the agent tries issue_refund(amount=250), the policy engine blocks it and the agent has to escalate to a human instead.

Why it matters

Telling an agent in the system prompt to "only refund up to $100" is a suggestion. Telling Policy to enforce that rule is an actual limit at the infrastructure level. That is the difference between a demo and something you would trust with real customer money.

9. AgentCore Evaluations

What it does

How do you know if your agent is doing a good job? Evaluations gives you 13 built-in evaluators that score your agent’s output on things like correctness, helpfulness, safety, tool selection, goal completion, and context relevance.

It samples real interactions, runs them through evaluation models, and surfaces the scores in CloudWatch dashboards. You can also write your own custom evaluators with your own LLMs and prompts.

Use case: catching a quality drop in production

Your support agent has been doing great for weeks. Then a model update happens and satisfaction scores start sliding. Evaluations catches that within hours instead of days, because it is scoring real interactions all the time.

The code

Set up an online evaluation on your agent:

Online evaluations sample traffic from CloudWatch logs and run evaluators against the sampled traces. The built-in evaluators are referenced by IDs like Builtin.Helpfulness, Builtin.Correctness, and so on.

import boto3 
  
client = boto3.client("bedrock-agentcore-control", region_name="us-east-1") 
  
response = client.create_online_evaluation_config( 
    onlineEvaluationConfigName="SupportAgentQuality", 
    description="Continuous quality monitoring for the support agent", 
    rule={ 
        "samplingConfig": { 
            "samplingPercentage": 10.0  # score 10% of traffic 
        } 
    }, 
    dataSourceConfig={ 
        "cloudWatchLogs": { 
            "logGroupNames": ["/aws/bedrock-agentcore/runtimes/support-agent"], 
            "serviceNames": ["support-agent.DEFAULT"] 
        } 
    }, 
    evaluators=[ 
        {"evaluatorId": "Builtin.Correctness"}, 
        {"evaluatorId": "Builtin.Helpfulness"}, 
        {"evaluatorId": "Builtin.Harmfulness"}, 
        {"evaluatorId": "Builtin.ToolSelectionAccuracy"}, 
    ], 
    evaluationExecutionRoleArn=( 
        "arn:aws:iam::123456789012:role/AgentCoreEvaluationRole" 
    ), 
    enableOnCreate=True, 
) 
  
print(f"Evaluation config created: {response['onlineEvaluationConfigId']}") 

Sampling is set once at the top of the config (samplingPercentage), not per-evaluator. The percentage is on a 0–100 scale, where 10.0 means 10% of traffic.

Add a CloudWatch alarm to catch quality drops:

Evaluation scores flow into CloudWatch and can be alarmed on. The exact metric and namespace names depend on the evaluator and AgentCore release, so check the current Evaluations metric reference for the values to use in your account. The shape of the alarm call looks like this:

cloudwatch = boto3.client("cloudwatch", region_name="us-east-1") 
  
cloudwatch.put_metric_alarm( 
    AlarmName="SupportAgent-Helpfulness-Drop", 
    # Use the metric name and namespace from the AgentCore Evaluations 
    # metric reference for your account / region. 
    MetricName="", 
    Namespace="", 
    Statistic="Average", 
    Period=28800,  # 8 hours 
    EvaluationPeriods=1, 
    Threshold=0.7, 
    ComparisonOperator="LessThanThreshold", 
    AlarmActions=[ 
        "arn:aws:sns:us-east-1:123456789012:agent-quality-alerts" 
    ], 
    Dimensions=[ 
        {"Name": "OnlineEvaluationConfigId", 
         "Value": response["onlineEvaluationConfigId"]} 
    ], 
) 
print("Quality alarm set up.") 

Why it matters

Without continuous evaluation, quality issues can go undetected. A model update, a changed data source, or even just users asking different questions over time can drag down agent quality. Evaluations give you an early warning so you catch it before your users do.

Putting it all together

The real value comes from combining these services. Here is what a production-ready agent stack actually looks like, end to end:

  • Runtime hosts the agent.

  • Memory keeps it context-aware across sessions.

  • Gateway gives it access to your APIs and Lambda functions as MCP tools.

  • Identity handles who the user is and which third-party tokens the agent can use on their behalf.

  • Code Interpreter and Browser give the agent the ability to compute and to fetch information from the web.

  • Observability shows you what the agent did.

  • Policy enforces the rules it has to follow.

  • Evaluations tells you whether it is doing a good job.

Each service handles one thing well. You can use them on their own or together. You pick what you need, and you pay for what you actually use.

Final thoughts

Building an agent that works in a demo is one thing. Building one that works in production, with real users, real data, real security needs, and real consequences when issues arise, is a completely different problem.

AgentCore does not write your agent for you. What it does is handle the mountain of infrastructure work that sits between your agent idea and a production deployment you can actually trust. If you are currently duct-taping together your own session isolation, memory, tool integration, auth, and monitoring, this is worth a serious look.

It will not solve every problem. But it takes a whole category of problems off your plate so you can focus on the part that actually matters: making your agent useful.

Getting started costs nothing to try. A few commands get you a scaffolded agent:

pip install bedrock-agentcore bedrock-agentcore-starter-toolkit strands-agents 
  
# Write your agent in my_agent.py, then: 
agentcore configure --entrypoint my_agent.py 
agentcore launch 

From there you are one AgentCore launch away from production.

Happy building.

All code in this post uses the AgentCore Python SDK and starter toolkit. For the full API reference and more samples, check the AgentCore documentation and the samples repo on GitHub.

It will not solve every problem. But it takes a whole category of problems off your plate so you can focus on the part that actually matters: making your agent useful.

Explore AgentCore services

Build production-ready AI agents

Contact us

Jay Kumar

Jay Kumar

Senior Manager, Advisory (CEDA), PwC US

Vidyashankar Venkataraman

Vidyashankar Venkataraman

Senior Manager, Advisory (CEDA), PwC US

Follow us