Writing an agent loop that works on your laptop takes an afternoon. Running it for real users is the hard part: session isolation, memory that outlives one conversation, tools that need OAuth tokens for that user, traces you can debug, limits so one prompt canât run up a bill. Amazon Bedrock AgentCore is AWSâs answer to that second part. It is a set of separate managed services, each covering one piece of what an agent needs in production. You can use one of them or all of them.
These notes come from an AWS workshop on building agents with Strands and AgentCore, plus the docs I read afterwards to fill in the gaps.
system prompt + persona the agent's "brain"
+ user prompt / query <------+ +-------> (LLM)
| |
v v
+-------------+
| Agent |
+-------------+
^ ^ ^
| | |
v v v
plan, reason interact with the store past interactions
and orchestrate outside world in short- and long-term
through tools memory
Every agent framework (Strands, LangGraph, CrewAI, a hand-rolled while loop) has the same pieces:
| Piece | What it does | AgentCore service |
|---|---|---|
| System prompt + persona + user query | The input and the instructions | Your config / your code |
| The âbrainâ (LLM) | Decides the next step | Any model: Bedrock, OpenAI, Gemini, LiteLLM |
| Plan, reason, orchestrate | The loop itself: think, call a tool, read the result, repeat | Runtime (your loop) or Harness (managed loop) |
| Interact with the outside world through tools | APIs, code execution, a browser | Gateway, Code Interpreter, Browser |
| Store past interactions in short- and long-term memory | Context within a session and across sessions | Memory |
AgentCore adds the pieces the diagram leaves out, which are mostly about running agents for other people:
| Service | Why you need it |
|---|---|
| Identity | Who is calling the agent (inbound JWT), and which credentials the agent uses to call other systems on that userâs behalf (outbound OAuth / API key vault) |
| Policy | Rules about what the agent may do, written in natural language or Cedar, enforced on Gateway tool calls |
| Observability | OpenTelemetry traces of every step and tool call, in CloudWatch |
| Evaluations | LLM-as-a-judge scoring of those traces |
| Registry | A catalog of agents, MCP servers, tools and skills across the org |
| Payments | Lets agents pay for x402-protected APIs and tools |
The workshop showed how these services fit together:
any provider
+--------------------------+ +----------+ +--------+
App <---> | Runtime | ---> | Identity | | Policy |
| +--------------------+ | +----------+ +--------+
any model <---> | | any | Prompt | | ^ ^
| | framework | Tools | | tools +--------------------+
any agent <---> | | | Context| | <------> | Gateway (MCP) |
(A2A) | +--------------------+ | +--------------------+
+-------------^------------+ | Browser |
| +--------------------+
v | Code Interpreter |
+--------------------------+ +--------------------+
| Memory |
+--------------------------+
+--------------------------------------------------------------------------+
| Observability (OTEL) |
+--------------------------------------------------------------------------+
^
v
+--------------------------------------------------------------------------+
| Evaluations |
+--------------------------------------------------------------------------+
How to read it:
The services are decoupled. A LangGraph agent on EKS can use AgentCore Memory and Gateway without touching Runtime.
This is the first decision, and it is the one that matters most.
| Â | Harness | Runtime |
|---|---|---|
| You provide | Configuration (model, prompt, tools, skills, memory, limits) | Agent code + an ARM64 container |
| Agent loop | Managed (Strands under the hood) | You write it, any framework |
| Change model / add a tool | Config change, no redeploy. Can even be overridden per call | Code change + redeploy |
| Best for | Getting an agent running fast, multi-tenant setups, A/B testing models | Custom loop control, graph/workflow patterns, bidirectional streaming, hooks |
AWSâs guidance is to start with Harness and move to Runtime only when configuration stops being enough. Both run on the same infrastructure: every session gets its own isolated, stateful microVM with its own filesystem and shell, so one userâs session canât see anotherâs.
Classic Bedrock Agents (the
bedrock-agentaction-group service) is in maintenance mode and closed to new customers. New work should use AgentCore. Existing agents have a migration path to Harness.
Most of the AWS workshop was hands-on with Strands Agents, the open-source SDK AWS uses inside Harness. It is a good place to start because the whole agent loop fits in a few lines of Python.
What agent("...") does on each call:
user prompt
|
v
+---------------------------------------------+
| Agent |
| system_prompt + conversation history |
| |
| +-----------+ tool call +----------+ |
| | model | ------------> | tools | |
| | (Bedrock) | <------------ | (@tool) | |
| +-----------+ tool result +----------+ |
| | |
| | no more tool calls (end_turn) |
+---------|-----------------------------------+
v
AgentResult (final message, stop reason, token usage)
The model decides at each step whether to call a tool or answer. The loop ends when it answers without a tool call. Without tools, as in the examples below, it is a single model call plus the history.
pip install strands-agents
from strands import Agent
agent = Agent()
response = agent("Describe the Amazon Bedrock service")
print(response)
With no arguments, Agent() uses a default Claude model on Bedrock, so you need AWS credentials and model access in the accountâs region. The call runs the full loop: the model answers, and if it had tools it would call them and loop until it is done.
The next step in the workshop was choosing the model explicitly and giving the agent an identity:
from strands import Agent
from strands.models import BedrockModel
model = BedrockModel(model_id="amazon.nova-micro-v1:0")
agent = Agent(
model=model,
system_prompt="You are an AWS conversational assistant.",
name="chatbot_aws",
description="AWS conversational assistant.",
)
print(agent("Describe the Amazon Bedrock service"))
Two things to watch if you copy this code from the workshop slides:
model, name, description). The slides wrote Model= and Name=, which would fail with an unexpected-keyword error, and the curly quotes (â...â) are not valid Python.us.amazon.nova-micro-v1:0). If the plain model ID returns a ValidationException about on-demand throughput, switch to the profile ID.name and description look cosmetic here, but they matter once agents call other agents (A2A, agent-as-tool): they are what the calling agent reads to decide whether to delegate. This same Agent is what you deploy to Runtime in section 5, or what Harness builds for you from config in section 4.
The workshop described Harness as a fully managed, declarative agent: you define the model, the tools and the instructions in one configuration, and AWS runs the agent loop.
| Feature | What it means in practice |
|---|---|
| Zero code, only configuration | You declare model, prompt, tools and memory; Harness runs the agentic loop end to end. No container, no entrypoint |
| Multiple model providers | Amazon Bedrock, OpenAI, Google Gemini, or any provider LiteLLM supports |
| Isolated microVM sessions | Each session runs in its own microVM, and it auto-scales from zero to thousands of sessions |
| CLI or SDK (boto3) | The AgentCore CLI for quick prototyping, the AWS SDK for programmatic use |
| Synchronous, streaming and long-running | Interactive real-time answers, or background tasks of up to 8 hours (the same microVM limit as Runtime, section 5) |
| Built-in observability | Automatic dashboards, anomaly detection and notifications when a run completes |
The rest of this section shows what that looks like in practice.
The fastest path is the AgentCore CLI:
npm install -g @aws/agentcore@preview
agentcore create --name myresearchagent --model-provider bedrock
agentcore deploy
agentcore invoke --harness myresearchagent --session-id "$(uuidgen)" "Hello, what can you do?"
The same thing through the API takes three steps: create, wait for READY, invoke. Only a name and an execution role are required, but set the execution limits yourself. They are what stops a runaway loop from running up the bill.
aws bedrock-agentcore-control create-harness \
--harness-name "ResearchAgent" \
--execution-role-arn "arn:aws:iam::<account>:role/<HarnessRole>" \
--max-iterations 25 \
--max-tokens 4096 \
--timeout-seconds 300
# poll until "status": "READY" (CREATING -> READY)
aws bedrock-agentcore-control get-harness --harness-id "<harnessId>"
The rest of the agent is declarative. A fuller config looks like this:
{
"model": { "bedrockModelConfig": {
"modelId": "<model-id>",
"maxTokens": 4096,
"temperature": 0.2
}},
"systemPrompt": [ { "text": "You are a research assistant. Cite your sources." } ],
"tools": [
{ "type": "agentcore_code_interpreter", "config": { "agentCoreCodeInterpreter": {} } },
{ "type": "agentcore_gateway", "config": { "agentCoreGateway": { "gatewayArn": "<gateway-arn>" } } },
{ "type": "remote_mcp", "name": "docs", "config": { "remoteMcp": { "url": "https://mcp.example.com/mcp" } } }
],
"skills": [
{ "git": { "url": "https://github.com/example/skills-repo", "path": "research" } }
]
}
A few things to note:
openAiModelConfig, geminiModelConfig and liteLlmModelConfig work too. The provider API key lives in AgentCore Identity and is referenced by ARN, never inline.SKILL.md + scripts), the same one Claude Code uses. They can come from Git, S3, the session filesystem, or AWSâs prebuilt skills.shell and file_operations inside the microVM.Invocation happens on the data plane (bedrock-agentcore, not -control) and returns a stream of events:
import uuid
import boto3
client = boto3.client("bedrock-agentcore", region_name="us-east-1")
session_id = str(uuid.uuid4()) # must be >= 33 chars; a hyphenated UUID is 36
response = client.invoke_harness(
harnessArn="<harness-arn>",
runtimeSessionId=session_id,
messages=[{"role": "user", "content": [{"text": "Summarize the last AWS re:Invent agent launches"}]}],
)
for event in response["stream"]:
if "contentBlockDelta" in event:
delta = event["contentBlockDelta"].get("delta", {})
if "text" in delta:
print(delta["text"], end="", flush=True)
elif "messageStop" in event:
print(f"\n[stop: {event['messageStop']['stopReason']}]")
Reuse the same runtimeSessionId to continue the conversation in the same microVM. The stopReason tells you why the loop ended: end_turn means it finished normally, while max_iterations_exceeded and timeout_exceeded mean one of your limits cut it off.
Per-invocation overrides are what make Harness useful beyond prototyping. model, systemPrompt, tools, skills and the limits can all be overridden on a single invoke_harness call. You can A/B test two models on live traffic without deploying anything.
Versions and endpoints: every update creates an immutable version, and named endpoints point at versions. To roll back, point the endpoint at the previous version. Nothing gets rebuilt.
If you already have an agent in LangGraph, CrewAI, or Strands with custom hooks, Runtime hosts it as is. The workshop showed how small the change is, starting from the minimal agent of section 3:
minimal agent agent for AgentCore
------------- -------------------
from strands import Agent from strands import Agent
+ from bedrock_agentcore.runtime import BedrockAgentCoreApp
+ app = BedrockAgentCoreApp()
agent = Agent() agent = Agent()
+ @app.entrypoint
+ def invoke(payload):
response = agent("Describe ...") response = agent(payload.get("prompt"))
print(response) return ...
+ if __name__ == "__main__":
+ app.run()
Three additions, and the agent itself doesnât change:
BedrockAgentCoreApp() creates the HTTP server Runtime expects.@app.entrypoint marks the function Runtime calls on every invocation. The request body arrives as payload, so the prompt now comes from the caller instead of being hardcoded.app.run() starts the server, which also lets you test locally before deploying.The complete file:
# agent.py
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
app = BedrockAgentCoreApp()
agent = Agent(system_prompt="You are a helpful assistant.")
@app.entrypoint
def invoke(payload: dict) -> dict:
result = agent(payload.get("prompt", ""))
return {"result": str(result)}
if __name__ == "__main__":
app.run()
Compared with the slide, this version fixes the function bodyâs indentation (the slide lost it), defaults the prompt to "" instead of None, and returns a plain dict. The AgentResult object Strands returns is not plain JSON; str(result) gives the final text.
Test it locally before building anything:
python agent.py # serves on http://localhost:8080
curl -s localhost:8080/ping
curl -s -X POST localhost:8080/invocations \
-H "Content-Type: application/json" \
-d '{"prompt": "Describe the Amazon Bedrock service"}'
Runtime calls the same two endpoints, so a passing local test means the contract is right. What still changes on deploy is the environment: the execution role instead of your local credentials, and an ARM64 build.
BedrockAgentCoreApp implements the container contract for you. For the default HTTP protocol that means GET /ping for health and POST /invocations for work, on port 8080. Runtime supports four protocols, and you have to pick one before building the container:
| Protocol | Contract | Use when |
|---|---|---|
| HTTP | /ping, /invocations (JSON/SSE), port 8080 |
Default; REST-style request/response |
| MCP | /mcp (streamable-http), port 8000 |
The thing youâre deploying is a tool server |
| A2A | /.well-known/agent-card.json + JSON-RPC at /, port 9000 |
Agents calling agents |
| AG-UI | /invocations SSE event stream, port 8080 |
Streaming agent state to a frontend |
Two common gotchas:
docker buildx build --platform linux/arm64.READY before the agent can take traffic.aws bedrock-agentcore-control create-agent-runtime \
--agent-runtime-name my_agent \
--agent-runtime-artifact '{"containerConfiguration":{"containerUri":"<ecr-uri>"}}' \
--role-arn <execution-role-arn> \
--network-configuration '{"networkMode":"VPC", ...}' \
--authorizer-configuration '...' \
--protocol-configuration '{"serverProtocol":"HTTP"}'
aws bedrock-agentcore-control create-agent-runtime-endpoint \
--agent-runtime-id <id> --name prod
The AgentCore starter toolkit (agentcore configure / agentcore launch) does the Dockerfile, ECR push and runtime creation for you if youâd rather not run these steps yourself.
The workshop summed up the whole path from code to a live agent in one picture:
developer
|
v
+------------------------------------+
| Agent or tool code |
| models: Bedrock, Anthropic, |
| OpenAI, Gemini, ... |
| frameworks: Strands, LangGraph, |
| CrewAI, LlamaIndex...|
| + | AgentCore Runtime
| AgentCore Runtime decorator | +-----------------------+
| AgentCore Observability config | configure | |
| AgentCore Identity config | ---+ | Runtime Agent |
+------------------------------------+ | | (the versioned code) |
| | |
+-------------------------------+ | Runtime Endpoint |
| | (what clients call) |
+--> Dockerfile ----> Amazon ECR ----> | |
| | |
+--> ZIP upload ----> Amazon S3 ----> | |
+-----------------------+
^
| invoke
user ----------> application ------------------------------+
Reading it left to right:
@app.entrypoint), Observability config (ADOT/OpenTelemetry) and Identity config (inbound authorizer, outbound credential providers).Each session runs in its own microVM, and that microVM is the billing unit:
maxLifetime, default and maximum 28,800 s). Within that window, calls with the same runtimeSessionId hit the same microVM, with its filesystem and in-process state. At the limit the microVM is terminated; the session can continue, but on a new microVM.idleRuntimeSessionTimeout, default 900 s), the microVM is terminated and billing stops. Both values are set in the runtimeâs lifecycleConfiguration, from 60 s up to 8 h.aws bedrock-agentcore-control update-agent-runtime \
--agent-runtime-id <id> \
--lifecycle-configuration '{"idleRuntimeSessionTimeout": 300, "maxLifetime": 3600}' \
...
The practical consequence: reuse session IDs for a real conversation, but donât keep sessions alive artificially, and lower the idle timeout if your users have short interactions. A session kept open âjust in caseâ is memory you are paying for.
If you really need longer sessions (an agent that works on a task for days), Runtime also has an Instances compute type: AgentCore-managed EC2 instances in your account, billed as EC2 cost plus a management fee, with sessions up to 14 days.
Direct code deployment adds the ZIPâs storage at S3 Standard rates; the container path pays for ECR storage instead.
Gateway turns things that arenât MCP servers into MCP tools. You give it an OpenAPI spec, a Lambda function or an existing MCP server, and it exposes all of them through one MCP endpoint. Gateway builds the tool descriptions from the OpenAPI description fields, so the quality of those descriptions decides how well the agent picks tools.
APIs, tools and resources
agent +-----------+ +-----------------+ +--------+
+------------+ /mcp | | --> | target: | --> | tool 1 | RESTful services
| MCP client | ---------> | AgentCore | | API endpoint | | tool 2 | with an OpenAPI
+------------+ | Gateway | +-----------------+ | tool 3 | schema
list tools | | +-----------------+ +--------+
invoke tool | | --> | target: | --> | tool 4 |
search | | | AWS Lambda | | tool 5 | Lambda functions
+-----------+ +-----------------+ | tool 6 |
+--------+
From the agentâs side there is only one MCP server, at the gatewayâs /mcp URL. It speaks three operations:
tools/list): every tool from every target, merged into one catalog.tools/call): Gateway routes the call to the right target, translates the MCP arguments into the REST request or Lambda event, and adds the outbound credentials.x_amz_bedrock_agentcore_search tool that returns only the tools relevant to a natural-language query (see below).Each target is one backend, and one target can expose many tools: an OpenAPI spec turns each operation into a tool, and a Lambda target declares its tools in a schema. Adding a new API means adding a target; the agentâs code does not change.
Real services have a lot of operations. The workshop example had three targets with 250, 100 and 10 tools:
+----------+
without search +-----------+ <-------- | target 1 | 250 tools
tools/list -------------------> | | +----------+
<--- 300+ tools ---- | AgentCore | +----------+
| Gateway | <-------- | target 2 | 100 tools
with search | | +----------+
search: "create a social ----> | | +----------+
media post" | | <-------- | target 3 | 10 tools
<--- 4 most -------- | | +----------+
relevant tools +-----------+
Without search, the agent lists the tools and gets all 360 in its context: every name, description and JSON schema, on every turn. That is thousands of tokens before the userâs question, and the model has to pick one out of hundreds of similar-looking options, which is exactly where tool selection gets worse.
With search, the agent asks in natural language and Gateway returns only the 4 most relevant tools. The benefits, as the workshop put them:
Search has to be enabled when the gateway is created:
client.create_gateway(
name="my-gateway",
roleArn=gateway_role_arn,
protocolType="MCP",
protocolConfiguration={"mcp": {"searchType": "SEMANTIC"}},
authorizerType="CUSTOM_JWT",
authorizerConfiguration=auth_config,
)
and the agent calls it like any other MCP tool:
{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "x_amz_bedrock_agentcore_search",
"arguments": { "query": "create a social media post" }
}
}
The same caveat as before applies, only stronger: search ranks tools by their descriptions. Vague OpenAPI description fields mean bad search results.
The order of steps matters. Create the credential provider before the gateway target, or target creation fails with âcredential provider not foundâ:
# 1. the API key goes into AgentCore Identity (encrypted, stored in Secrets Manager by the service)
export API_KEY=... # don't paste it on the command line
aws bedrock-agentcore-control create-api-key-credential-provider \
--name weather-api --api-key "$API_KEY"
# 2. only then create the target pointing to the OpenAPI spec in S3
aws bedrock-agentcore-control create-gateway-target \
--gateway-identifier <gateway-id> --name weather \
--target-configuration '...' --credential-provider-configurations '...'
Code Interpreter is a sandbox where the agent runs code: pandas over a CSV, a quick plot, checking its own arithmetic. Browser is a managed headless browser for sites that have no API. Both run isolated from your accountâs network unless you configure otherwise.
AgentCore Memory has two layers, and the important part is which calls are synchronous and which are not:
agent deployment AgentCore Memory
+----------------------+ +-----------------------------+
| events | sync | short-term memory | async +------------+
+-------+ | - messages | -------> | - conversations | --------> | automatic |
| agent |<-->| - agent state | | - session state | | memory |
+-------+ | | sync | | | extraction |
| list events | <------- | | +------------+
| | +-----------------------------+ |
| | | long-term memory | async |
| | | - semantic (facts) | <---------------+
| retrieve memory | sync | - user preferences |
| records | <------- | - summary | batch API
| | | - episodic | <---------- hydrate
+----------------------+ | - custom strategy | long-term memory
+-----------------------------+
CreateEvent), and read back with ListEvents. Both are synchronous, so the next turn already sees the previous one. This is your multi-turn chat history.actorId and namespaces.RetrieveMemoryRecords does a semantic search over the long-term records, so before answering âsuggest running shoesâ the agent can pull the userâs size and favorite brand from a conversation weeks ago.What gets extracted depends on the strategies you enable on the memory resource:
| Strategy | What it keeps | Example |
|---|---|---|
| Semantic | Facts and knowledge from the conversation | âThe order #123 shipped to Recifeâ |
| User preferences | Explicit and implicit preferences | âPrefers deployments in us-east-1â |
| Summary | A running summary per session | âUser debugged a VPC timeout, fixed with a NAT gatewayâ |
| Episodic | What happened in an interaction, plus reflections on what worked | âQuerying Cost Explorer first answered the cost questionâ |
| Custom | Your own extraction prompt and model on top of a built-in strategy | Domain-specific entities |
Since extraction is asynchronous, a fact said in this turn usually isnât retrievable as a long-term record on the next one. Within a session, rely on the short-term events; long-term memory pays off in the next session.
With Harness, memory is just config: managedMemoryConfiguration for a managed store, agentCoreMemoryConfiguration with your own memory ARN, or disabled for stateless agents. With Runtime, the SDK gives you hooks that save the conversation and retrieve relevant long-term records before each turn.
Session memory is conversation history, so expect PII in it. Encrypt it with a customer-managed KMS key and keep the TTL as short as your use case allows.
This is the part people mix up most. The workshop drew every hop where someone has to prove who they are:
agent needs AWS resources
+--> [agent outbound auth: IAM] ---------> AWS resources / agents
|
user | agent needs a tool behind Gateway
| +--> [agent outbound auth: ] --> [gateway inbound auth ]
v +---------+ | [OAuth / API key ] [AgentCore Gateway ]
app ----------> |[inbound]| | [gateway outbound auth]
^ | agent |-+ |
| OAuth token | (hosted | v
| | or | | agent needs an external resource
user's identity | self- | +--> [agent outbound auth: ] ---> external resources / agents
provider | hosted)| [OAuth / API key ] (OAuth, API keys, ...)
+---------+
Reading it from the left:
The same thing, grouped by direction:
| Layer | Direction | Example |
|---|---|---|
| Inbound | Caller â agent | Your appâs Cognito/Okta/Entra JWT is validated by the agentâs customJWTAuthorizer |
| Outbound | Agent â external API | The agent gets a Google Calendar token for this user from the Identity token vault |
| Service | AgentCore â AWS | The execution role (IAM) |
A harness or runtime accepts exactly one inbound method: SigV4 (IAM) when no authorizer is configured, OAuth JWT when one is. Thereâs no mixed mode. The important consequence: per-user tokens for downstream tools only work on the JWT path. With SigV4 the agent canât tell which end user is calling, so it canât act on their behalf.
When you configure the JWT authorizer, set allowedAudience and/or allowedClients. Without them, any valid token from that issuer is accepted, including tokens issued for a different app.
Identity answers who is calling. Policy (in preview at the time of the workshop) answers what they are allowed to do, and it does it outside the model. A system prompt saying ânever refund more than $1,000â is a request; a policy is enforced.
tool calls allowed
user --> agents and requested +-------------------+ tool calls +--------------------+
other MCP ---------> | AgentCore Gateway | ------------------> | protected |
clients +-------------------+ | resources |
^ | (tools, APIs, |
| allow / deny | systems, data) |
v +--------------------+
+------------------+ +------------------------------------------------+
| AgentCore | <-- | Policy in AgentCore |
| Observability | | +--------------------+ |
+------------------+ | +-------------------+ | policy lifecycle | | policy
| | dynamic policy | | management | | <-> admin
| | evaluation | +--------------------+ |
| +-------------------+ | policy authoring | |
| | (natural language | |
| | or Cedar) | |
| +--------------------+ |
+------------------------------------------------+
How it works:
A Cedar policy has a principal (who), an action (which tool) and a resource (which gateway), plus optional conditions on the toolâs input:
permit(
principal,
action == AgentCore::Action::"ApplicationToolTarget___create_application",
resource == AgentCore::Gateway::"<gateway-arn>"
) when {
context.input.coverage_amount <= 1000000
};
The agent can create insurance applications, but only up to 1,000,000 of coverage. Above that the call is denied, and the agent has to tell the user it canât do it (or hand off to a human).
The three promises on the slide:
Two Cedar rules worth knowing: anything not explicitly permitted is denied, and a forbid always wins over a permit. Start with a few narrow permits for the tools the agent needs, and add forbids for hard limits.
Policy only sees calls that go through Gateway. A tool implemented inside the agentâs own code, or an API it calls directly, is not covered. If you want an action to be governed, put it behind Gateway.
Observability gets telemetry from two places: from your agent (the frameworkâs own spans) and from the AgentCore services it uses. The workshop slide numbered the three ways to wire it:
agent runtime
+--------------------------------------+
| framework (Strands, LangGraph, | (1) agent telemetry
| CrewAI, LlamaIndex...) ADOT | ------------------------+
| 3P OTEL | |
+--------------------------------------+ v
| | +----------------------+ +----------------------+
| | service telemetry | Amazon Bedrock | | AgentCore |
| +-> AgentCore Memory ---------+ | AgentCore | --> | observability |
| +-> AgentCore Identity -------+--- (3) --> | Observability | | dashboards |
| +-> AgentCore Gateway --------+ | (CloudWatch) | | (sessions, traces, |
| +-> Browser & Code Interp. ---+ +----------------------+ | token usage...) |
| automatic chaining of +----------------------+
| the OTEL spans from the
| agent and the services
| +----------------------+
| (2) agent telemetry via a third-party OTEL SDK | third-party |
+-----------------------------------------------------------------------------> | observability |
| (Langfuse, Datadog) |
+----------------------+
OTEL_* environment variables yourself.DISABLE_ADOT_OBSERVABILITY=true so the default ADOT configuration doesnât get in the way of your own exporter.One prerequisite trips people up: enable CloudWatch Transaction Search in the account once, or no spans show up in the dashboards.
Separately from the traces, Runtime publishes invocations, latency, errors, throttles and session counts to CloudWatch automatically. Token usage is not published automatically. For tokens and per-step traces, instrument the agent with the ADOT (AWS Distro for OpenTelemetry) SDK. The ADOT Collector is not supported for this; it has to be the SDK.
Observability tells you what the agent did. Evaluations (in preview at the time of the workshop) tells you whether it did it well, and it reads exactly the same traces:
+------------------------------+
user <------> | agent |
| - - AgentCore Runtime | +----------------------------+
| - - AgentCore Memory | | AgentCore Evaluations | on-demand
| - - AgentCore Gateway | | | <------------+
+------------------------------+ | call the LLM judge | |
| | for each metric | agent owner
| traces | | | (picks evaluators,
v | v | watches scores)
+------------------------------+ read | write the results with | ^
| AgentCore Observability | ------> | the judge's explanation | |
| | <------ | | |
+------------------------------+ scores +----------------------------+ |
| |
+---------------- dashboards: score over time ------------------------+
The evaluators you pick from, grouped as in the console:
| Group | Evaluators (examples) | Question it answers |
|---|---|---|
| Response quality | Correctness, Helpfulness, Conciseness, Instruction following⌠| Is the answer right, useful, short enough, and does it follow the system prompt? |
| Task completion | Goal success rate | Did the conversation achieve what the user wanted? |
| Component-level | Tool selection accuracy⌠| Did the agent call the right tool for the step? |
| Safety | Harmfulness⌠| Does the response contain harmful content? |
| Custom | Your own judge prompt, or code (e.g. a Lambda) | Domain rules: schema validity, required disclaimers, no PII |
Two ways to run them:
Things to keep in mind:
The hands-on lab built one agent that uses almost every service in this post: a customer support agent that answers questions about products, returns, warranties and refunds.
AWS Cloud
local env +---------------------------------------------------+
+-------+ | AgentCore Runtime | Exa MCP server
user <->| Flask |<---->| +----------------+ local tools: | task +------------------------+
+-------+ | | customer |--> 1 get_return_policy() |------->| 3 web_search_exa() |
: | | support agent |--> 2 get_product_info() | | 4 get_code_context_exa |
: | | (Strands) | | | 5 crawling_exa() |
: | +----------------+ | +------------------------+
: +-----:---------:------------:-----------|----------+
: : : : | task send traces
: invoke LLM : : session + : authn v - - - - - - - - - +
: v v history : +-----------------+ v
: +----------+ +----------+ : | AgentCore |<- -> AgentCore +---------------+
: | Bedrock | | AgentCore| : | Gateway | Policy | AgentCore |
: | LLMs | | Memory | : +-----------------+ | Observability |
: +----------+ +----------+ : : : +---------------+
: : v v : online
: authenticate user : 6 check_ 7 process_ v eval config
v v warranty() refund() +---------------+
+-----------------+ propagate +-----------+ (Lambda) (Lambda) | AgentCore |
| Amazon Cognito | ---------------> | AgentCore |<- - - - - - - - - -: | Evaluations |
+-----------------+ credentials | Identity | authenticate tool calls +---------------+
+-----------+
Following a question through it:
| Tool | Where it lives | Why there |
|---|---|---|
1 get_return_policy(), 2 get_product_info() |
Inside the agentâs code (@tool) |
Read-only, no credentials, no need for governance |
3 web_search_exa(), 4 get_code_context_exa(), 5 crawling_exa() |
Exaâs remote MCP server | Third-party capability (web search) the agent consumes as is |
6 check_warranty(), 7 process_refund() |
Lambda functions behind Gateway | Touch customer data and money: they need authenticated calls and limits |
The split in step 4 is the main design lesson of the lab: put a tool behind Gateway when it needs credentials, an audit trail or a policy, and keep the rest as plain local tools. process_refund() inside the agentâs code would work just as well in a demo, but nothing outside the model would stop it from refunding the wrong amount.
What Iâd verify before putting real users on an agent:
maxIterations, maxTokens, timeoutSeconds. Every session is a microVM with a shell, so an unbounded caller is a cost problem.aws:SourceAccount / aws:SourceArn in the trust policy, model access scoped to specific inference profiles, and one role per agent.invoke_harness through your backend, strip or allowlist model, skills and tools. Skills are injected as trusted context, scripts included.environmentVariables (theyâre readable with get-harness / get-agent-runtime). Use Identity credential providers or Secrets Manager.Use AgentCore when youâre already on AWS and the hard part is everything around the loop: multi-tenant isolation, per-user OAuth to third-party tools, audit trails, rolling back a bad prompt in seconds. Harness in particular removes most of the code for a standard tool-using agent.
Skip it when your âagentâ is a single LLM call with structured output (just call the Converse API), or when you need the loop to run somewhere AWS isnât. Itâs also worth comparing the per-session microVM cost against a plain Lambda for short, stateless, high-volume calls.
Mostly AgentCore saves you from building the platform yourself: session isolation, a credential vault, memory extraction, tracing. Agents mostly need that layer once they leave a notebook.