to perform useful work can be relatively straightforward. For a simple application, a few lines of Python using the boto3 library and the Bedrock API may be all that’s needed. You set up access to an LLM, send a prompt and some input, receive a response, and return it to the user.
That approach becomes harder to manage if your application has to take on more responsibilities. Once the model needs to maintain conversation context, choose between tools, follow detailed instructions, or coordinate several steps, the surrounding application code begins to resemble an agent framework.
That’s where the use of Strands and AgentCore comes into play.
Strands provides that agent layer, but running an agent reliably introduces a different set of concerns. It needs somewhere to run, an invocation interface, session isolation, scaling, security, and potentially services such as long-term memory or managed access to tools. Amazon Bedrock AgentCore provides these operational capabilities without defining how the agent itself should behave.
NB. Apart from being a user of their systems I have no affiliation or association with AWS.
All images shown in this article, apart from the headline image which is AI generated, were created by the author.
Strands
Strands is an open-source agent framework from AWS. It provides the application-level components needed to create an agent.
- An LLM, such as Claude Sonnet, through Amazon Bedrock or local models via Ollama and other providers.
- A system prompt that defines the agent’s role and behaviour.
- Tools the model can choose to call.
- Conversation messages and context.
- An agent loop that sends requests to the model, executes requested tools, and returns tool results to the model.
Think of Strands as the AWS equivalent to LangChain or CrewAI.
A basic Strands agent can be created with just a model and a system prompt:
from strands import Agent
model = 'your_LLM_choice'
agent = Agent(
model=model,
system_prompt="You are an educational SME assistant.",
)
response = agent("Explain Newton's first law.")Although I’m framing this article as a Strands/AgentCore double team, Strands itself doesn’t require AgentCore to run. A Strands agent is ordinary application code and can run locally or on other infrastructure. AgentCore comes in when the agent needs managed deployment on AWS infrastructure and production services, such as memory, scalability and observability.
The agent we’re building
We’ll build and deploy a Strands-based educational assistant before adding AgentCore Memory to preserve user preferences between conversations. Our agent will support questions about:
- Mathematics
- Physics
- Chemistry
- Geography
The model decides which subject best fits each question, so there’s no keyword list or separate routing tool to maintain. It answers supported questions as the relevant subject expert and declines anything outside the four supported areas.
This is a useful first agent because its behaviour is easy to understand, yet the application still involves decisions found in larger systems: selecting a model, writing instructions, validating requests, testing model behaviour, deploying the application, and managing conversation sessions.
I’m only using one Strands agent rather than separate specialist agents for each subject. Separate agents become worthwhile when subjects need different models, tools, instructions, data, or permissions, but for right now, that would add unnecessary coordination and complexity to this initial implementation.
AgentCore
Amazon Bedrock AgentCore is a set of managed AWS services for building, deploying, connecting, and operating agents on the AWS cloud. It’s framework-agnostic, so it can host agents built with Strands, LangChain, OpenAI Agents SDK, and other frameworks.
The main AgentCore capabilities are:
+------------------+---------------------------------------------------------------------------------------------+
| Capability | Purpose |
+------------------+---------------------------------------------------------------------------------------------+
| Runtime | Hosts and scales agents in session-isolated environments. Supports streaming and HTTP, MCP, |
| | and A2A protocols. |
+------------------+---------------------------------------------------------------------------------------------+
| Memory | Stores conversation events and extracts durable facts, preferences, summaries, or episodes |
| | for use across sessions. |
+------------------+---------------------------------------------------------------------------------------------+
| Gateway | Exposes APIs, Lambda functions, and MCP servers as managed tools that agents can discover |
| | and call. |
+------------------+---------------------------------------------------------------------------------------------+
| Identity | Manages inbound authentication and the credentials agents use to access external services. |
+------------------+---------------------------------------------------------------------------------------------+
| Policy | Applies Cedar authorisation rules to Gateway tool calls before they reach their targets. |
+------------------+---------------------------------------------------------------------------------------------+
| Browser | Provides managed browser sessions for agents that need to interact with websites. |
+------------------+---------------------------------------------------------------------------------------------+
| Code Interpreter | Runs Python, JavaScript, or TypeScript in isolated managed sandboxes. |
+------------------+---------------------------------------------------------------------------------------------+
| Observability | Sends agent logs, traces, and metrics to services such as CloudWatch and X-Ray. |
+------------------+---------------------------------------------------------------------------------------------+
| Evaluations | Measures agent behaviour and response quality using built-in or custom evaluators. |
+------------------+---------------------------------------------------------------------------------------------+These capabilities are independent, meaning an agent can use only the ones it needs and add others as its responsibilities grow.
We’ll use AgentCore to scaffold our Strands agent and deploy it to AWS with AgentCore Runtime. This creates a conventional project structure on your local system, a local development workflow, a deployment configuration, and a runtime entry point.
Our SME agent has no external tools and doesn’t need any of the other AgentCore capabilities yet, but we’ll add a memory component later.
How Strands and AgentCore fit together
Strands controls what our agent does. The selected model, system prompt, conversation context, and any tools are part of the Strands application.
AgentCore Runtime controls where and how that application runs. It provides the managed environment around the agent: deployment, scaling, session isolation, streaming, and an invocation interface.
This distinction is worth noting because deploying an agent with AgentCore doesn’t define the overall system behaviour. That fundamentally comes from the model choice and the underlying Strands code.
Installing the development tools
To follow along, you’ll need:
- An AWS account.
- AWS credentials configured locally.
- Node.js 20 or later.
- Python 3.10 or later.
- AWS CDK (used by AgentCore for deployment).
- The AgentCore CLI.
- Access to the selected model in Amazon Bedrock.
My installation was done on a Windows PC using PowerShell. Install the AWS CLI and Node.js first if those commands are unavailable.
PS C:\ > msiexec.exe /i https://awscli.amazonaws.com/AWSCLIV2.msi
PS C:\ > aws --version
#
# Output
#
aws-cli/2.22.15 Python/3.12.6 Windows/11 exe/AMD64Install the AWS CDK globally with npm:
PS C:\ > npm install -g aws-cdk
PS C:\ > cdk --version
#
# Output
#
2.1126.0 (build a90d578)Install the AgentCore CLI:
PS C:\ > npm install -g @aws/agentcore
PS C:\ > agentcore --version
#
# Output
#
The AgentCore CLI collects aggregated, anonymous usage
analytics to help improve the tool.
To opt out: agentcore config telemetry.enabled false
To audit: agentcore config telemetry.audit true
To learn more: agentcore telemetry --help
0.19.0The AgentCore CLI create command creates the Python environment and installs the generated project’s dependencies when running locally. The generated pyproject.toml file records dependencies, such as Strands and the AgentCore SDK.
Configure AWS credentials using the approach appropriate for the environment. For local development, this is commonly an AWS profile file.
Confirm that the LLM or inference profile you want to use is available from the intended source region. You can do that from the AWS CLI like this:
PS C:\ > aws bedrock list-foundation-models --region
PS C:\ > aws bedrock list-inference-profiles --region Choosing your model
When creating an AgentCore project, the –model-provider Bedrock command-line flag selects Amazon Bedrock as the provider but doesn’t specify which foundation model the application intends to use.
Strands uses one of Anthropic’s models by default when no model is supplied. At the time of writing, the latest default Strands model is global.anthropic.claude-sonnet-4–6, but it’s best practice to always specify a particular model you want Strands to use.
This example code fragment explicitly specifies the use of Anthropic Claude Sonnet 4.6 through a global cross-region inference profile:
from strands.models import BedrockModel
model = BedrockModel(
model_id="global.anthropic.claude-sonnet-4-6",
region_name="us-west-2",
temperature=0.2,
max_tokens=1_500,
)Bedrock has several model families to choose from, and Claude Sonnet is a good starting point for SME responses, while Claude Haiku or Amazon Nova Micro may suit a simpler, cost-sensitive routing workload.
Global cross-region inference can improve availability, but it may not satisfy every data-residency requirement. A geographic or application inference profile is a better choice when processing must remain within a defined geographic boundary, such as Europe or Asia.
Creating the AgentCore project
Use the AgentCore CLI to initialise a Strands project with Bedrock as its model provider:
PS C:\Users\thoma\projects\strands-agentcore-demo> agentcore create --name SMETriage --framework Strands --protocol HTTP --model-provider Bedrock --build CodeZip --memory none
#
# Output
#
[done] Create SMETriage/ project directory
[done] Prepare agentcore/ directory
[done] Initialize git repository
[done] Add agent to project
[done] Set up Python environment
Created:
SMETriage/
app/SMETriage/ Python agent (Strands)
agentcore/ Config and CDK project
Project created successfully!
To continue, navigate to your new project:
cd SMETriageWe already mentioned the –model flag. Another important option here is: –build CodeZip.
CodeZip is AgentCore’s direct-code deployment format for Python applications. Instead of building a Docker image and using ECR/ECS, with this flag, the AgentCore CLI:
- Collects the agent’s Python source files.
- Resolves and packages its dependencies.
- Creates a ZIP archive containing the application and Linux
- ARM64-compatible dependencies.
- Uploads the archive to Amazon S3.
- Configures AgentCore Runtime to run the Python entrypoint.
The project records this choice in the agentcore/agentcore.json file.
After the AgentCore create command has completed, you should see a project folder structure similar to this:
SMETriage/
|-- agentcore/
| |-- agentcore.json # Project and deployment configuration
| |-- aws-targets.json # Target AWS account and region
| `-- .env.local # Local-only values; gitignored
`-- app/
`-- SMETriage/
|-- main.py # Agent entrypoint
`-- pyproject.toml # Python dependenciesThe generated project can be tested before making any changes:
PS C:\Users\thoma\projects\strands-agentcore-demo\smetriage> agentcore devThis will open a web page at localhost:8080 or a similar address, so open that, and you can ask the model questions.
How AgentCore Manages IAM Permissions
The AgentCore CLI creates the agent’s Runtime execution role and adds the permissions required to invoke the selected Bedrock model, access configured Memory resources, and write Runtime logs.
You don’t need to create this role manually for this example. However, the IAM identity running AgentCore deploy must already have permission to create and pass IAM roles, deploy CloudFormation resources, upload CodeZip packages to S3, and manage the required AgentCore resources.
Users and applications also need permission to invoke the deployed agent. During development, the generated IAM policies are usually sufficient. Before moving into production, these should be reviewed and limited to the specific Runtime, model, Memory resource, and supporting infrastructure.
Implementing the SME agent
In the main.py file under the app/SMETriage folder, replace the contents with the following code:
import os
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
from strands.models import BedrockModel
app = BedrockAgentCoreApp()
MODEL_ID = os.getenv(
"BEDROCK_MODEL_ID",
"global.anthropic.claude-sonnet-4-6",
)
REGION = os.getenv("AWS_REGION", "us-west-2")
SUBJECT_PREFIXES = {
"mathematics": (
"This is your Math SME. "
"The answer to your question is as follows."
),
"physics": (
"This is your Physics SME. "
"The answer to your question is as follows."
),
"chemistry": (
"This is your Chemistry SME. "
"The answer to your question is as follows."
),
"geography": (
"This is your Geography SME. "
"The answer to your question is as follows."
),
}
def load_model() -> BedrockModel:
return BedrockModel(
model_id=MODEL_ID,
region_name=REGION,
temperature=0.2,
max_tokens=1_500,
)
def build_agent() -> Agent:
prefixes = "\n".join(
f"- {subject}: {prefix}"
for subject, prefix in SUBJECT_PREFIXES.items()
)
return Agent(
model=load_model(),
system_prompt=(
"You are an educational SME triage assistant. "
"First decide whether the user's question is primarily about "
"mathematics, physics, chemistry, geography, or an unsupported "
"subject. "
"Answer only questions about mathematics, physics, chemistry, "
"or geography. "
"For supported questions, begin with the exact prefix for the "
"chosen subject, then give a clear and accurate explanation. "
"When a question overlaps subjects, choose the subject most "
"important to answering it. "
"For unsupported questions, respond exactly: "
"\"I'm sorry, I don't know the answer to that.\"\n\n"
"Required subject prefixes:\n"
f"{prefixes}"
),
)
@app.entrypoint
def invoke(payload, context):
prompt = payload.get("prompt")
if not isinstance(prompt, str) or not prompt.strip():
return {"error": "A non-empty question is required."}
if len(prompt) > 2_000:
return {"error": "Question exceeds the maximum length."}
response = build_agent()(prompt.strip())
return {"response": str(response)}
if __name__ == "__main__":
app.run()There are three parts worth examining: model loading, routing instructions, and the Runtime entrypoint.
1/ Keeping model configuration explicit
The load_model function centralises the Bedrock configuration.
The BEDROCK_MODEL_ID environment variable can override the default LLM without changing the source, for example
PS C:\Users\thoma\projects\strands-agentcore-demo\smetriage> $env:BEDROCK_MODEL_ID = ""
PS C:\Users\thoma\projects\strands-agentcore-demo\smetriage> agentcore dev This is useful when comparing models or using different inference profiles between environments.
2/ Letting the model route questions
The agent doesn’t use a subject keyword list to determine how to decode a question. The system prompt asks the model to decide whether a question is primarily about one of the supported subjects.
That flexible approach handles disparate questions such as:
- “Explain the Riemann hypothesis to a 10-year-old.”
- “Why can stainless steel resist corrosion?”
- “Why do coastal areas often have milder climates?”
A fixed keyword classifier would need to anticipate terms such as Riemann, corrosion, and coastal. The model already understands the relationships between those terms and their subjects.
Some questions sit naturally between subjects. Consider: “How does the chemistry of the atmosphere affect climate?” Chemistry and geography are both reasonable choices, so the prompt asks the model to select whichever is most relevant to its answer.
This approach is flexible, but not fully predictable. The model may classify the same ambiguous question differently from one run to the next, or return a response in the wrong format. That might be fine for an educational assistant, but it’s much riskier if routing controls access to sensitive tools or data. In that case, you would want to put in place other controls, for example, structured outputs.
3/ Validating the Runtime request
The entry point checks that the request contains a usable question.
This is ordinary request validation rather than content moderation. It prevents malformed and excessively large requests from reaching the model. The function decorated with @app.entrypoint is the bridge between AgentCore Runtime and the Strands agent. Runtime supplies the payload, the function validates it, and the Strands agent produces the response.
Testing the agent
After stopping and restarting the agentcore dev command. We can ask the agent questions.
As shown, each answer should begin with the corresponding SME prefix, and the updated responses demonstrate that the new agent is in effect. Also, an unsupported question, such as “Who wrote Pride and Prejudice?” should receive the answer:
"I'm sorry, I don't know the answer to that."An overlapping question is useful for seeing how the model resolves ambiguity:
The important result isn’t whether it always chooses chemistry or geography. The answer should use one matching prefix and give a relevant explanation.
Deploying the agent to the AWS Cloud
Once the agent behaves correctly locally, deploy it to AWS using:
PS C:\Users\thoma\projects\strands-agentcore-demo\smetriage> agentcore deploy
AgentCore Deploy
Project: SMETriage
Target: us-east-2:0123456789
[done] Validate project
[done] Check dependencies
[done] Build CDK project
[done] Synthesize CloudFormation
[done] Check stack status
[done] Bootstrap AWS environment
[done] Computing diff changes...
[done] Publish assets
╭────────────────────────────────────────────────╮
│ ✓ Deploy to AWS Complete │
│ │
│ [████████████████████] 5/5 │
╰────────────────────────────────────────────────╯
Deployed 1 stack(s): AgentCore-SMETriage-default
Note: Transaction search enabled. It takes ~10 minutes for transaction search to be fully active and for traces from
invocations to be indexed.
Log: agentcore\.cli\logs\deploy\deploy-20260611-104449.logWe can inspect the deployed Runtime:
PS C:\Users\thoma\projects\strands-agentcore-demo\smetriage> agentcore status --type agent
Agents
SMETriage: Deployed - Runtime: READY
(arn:aws:bedrock-agentcore:us-east-2:0123456789:runtime/SMETriage_SMETriage-r5adp27A24)
URL: https://bedrock-agentcore.us-east-2.amazonaws.com/runtimes/arn%3Aaws%3Abedrock-agentcore%3Aus-east-2%3A6963531187
45%3Aruntime%2FSMETriage_SMETriage-r5adp27A24/invocationsThen invoke it with:
PS C:\Users\thoma\projects\strands-agentcore-demo\smetriage> agentcore invoke "Explain how probability is used in everyday life." --stream
{
"response": "This is your Math SME. The answer to your question is as follows.\n\nProbability is the mathematical study of likelihood and chance, and it appears in many aspects of everyday life. Here are some key examples:\n\n## Weather Forecasting\n- Meteorologists use probability to predict the **chance of rain or snow** (e.g., \"70% chance of rain\")\n- These predictions are based on historical data and atmospheric models\n\n## Insurance and Finance\n- Insurance companies calculate **risk probabilities** to set premium prices\n- Banks assess the **probability of loan default** when lending money\n- Investors evaluate the **likelihood of returns** on investments\n\n## Medicine and Health\n- Doctors use probability to assess **diagnostic test accuracy** and disease risk\n- Clinical trials rely on probability to determine if a **treatment is effective**\n- Genetic counselors calculate the **probability of inheriting conditions**\n\n## Games and Gambling\n- Card games, dice, and lotteries are all governed by **mathematical probability**\n- Understanding odds helps people make **informed decisions** about risk\n\n## Everyday Decision-Making\n- Deciding whether to **carry an umbrella** based on forecast probability\n- Estimating the **likelihood of traffic** when planning a commute\n- Assessing **safety risks** in daily activities\n\n## Quality Control\n- Manufacturers use probability to **predict defect rates** in production\n\nIn essence, probability helps us **quantify uncertainty** and make better-informed decisions in an unpredictable world.\n"
}
Session: 735d6d6b-b2de-40d3-ae3c-741821c4810f
To resume: agentcore invoke --session-id 735d6d6b-b2de-40d3-ae3c-741821c4810f
Log: C:\Users\thoma\projects\strands-agentcore-demo\smetriage\agentcore\.cli\logs\invoke\invoke-SMETriage-20260611-105138.logThe deployed Runtime needs permission to invoke the selected Bedrock model or inference profile. Anthropic models used to require a one-time use-case submission before first use, but that’s no longer the case. The first time you invoke the model, you’re automatically entitled to use it, and you’ll receive a couple of emails to that effect.
If invocation fails, check IAM permissions, model availability, inference profile, source region, and any organisation service control policies.
NB. Remember, at this point, AgentCore Runtime provides the managed execution environment, but the application behaviour remains in the Strands code. Changing the system prompt or model requires updating and redeploying the application.
Adding AgentCore Functionality — Memory
At the start, I mentioned that AgentCore consists of many different parts, e.g., Gateway, Runtime, Observability, etc…
We’ve already seen how to use the Runtime. Now we’ll add Memory to our example. You may have noticed that, previously, when we invoked AgentCore to ask our questions, a session ID was returned. These group related turns in one conversation. So if you have a follow-up question, you can pass in the session ID of the original question to get back related information. For example,
PS C:\Users\thoma\projects\strands-agentcore-demo\smetriage> agentcore invoke "Explain Newton's first law." --stream
{
"response": "physics: This is your Physics SME. The answer to your question is as follows.\n\n**Newton's First Law of Motion** (also known as the **Law of Inertia**) states:\n\n> *An object at rest stays at rest, and an object in motion stays in motion at a constant velocity (same speed and direction), unless acted upon by a net external force.*\n\n---\n\n### Key Concepts:\n\n1. **Inertia** – This is the tendency of an object to resist changes to its state of motion. The more mass an object has, the greater its inertia.\n\n2. **At Rest** – If an object is stationary, it will remain stationary unless a force acts on it. For example, a book sitting on a table will not move on its own.\n\n3. **In Motion** – If an object is moving, it will continue moving in a straight line at the same speed unless a force (such as friction, gravity, or applied force) acts on it.\n\n4. **Net External Force** – It is the *combined/resultant* force that matters. If all forces on an object cancel out (net force = 0), the object behaves as if no force is acting on it.\n\n---\n\n### Everyday Example:\nWhen a car stops suddenly, passengers lurch **forward** — their bodies were in motion and tend to **stay in motion**, demonstrating inertia.\n\nThis law fundamentally defines what a **force** is: something that *changes* the state of motion of an object.\n"
}
Session: 7dbe017a-e306-4891-88d1-b707a5d6f894
To resume: agentcore invoke --session-id 7dbe017a-e306-4891-88d1-b707a5d6f894
Log: C:\Users\thoma\projects\strands-agentcore-demo\smetriage\agentcore\.cli\logs\invoke\invoke-SMETriage-20260611-133213.logWe can use the Session ID: 7dbe017a-e306–4891–88d1-b707a5d6f894 to ask a follow-up.
PS C:\Users\thoma\projects\strands-agentcore-demo\smetriage> agentcore invoke --session-id 7dbe017a-e306-4891-88d1-b707a5d6f894 "Give me an example" --stream
{
"response": "physics: This is your Physics SME. The answer to your question is as follows.\n\nHere is a clear, everyday example of Newton's First Law in action:\n\n---\n\n### 🎳 Example: A Rolling Bowling Ball\n\n**Scenario:**\nWhen you roll a bowling ball down a lane, it continues moving forward in a straight line at roughly the same speed until it either:\n- **Hits the pins** (external force stops it), or\n- **Slows down due to friction** between the ball and the floor (external force reduces its speed)\n\n---\n\n### Breaking It Down:\n\n| Situation | What Happens | Why |\n|----------|--------------|-----|\n| Ball is rolled | It keeps moving forward | No net force to stop it immediately |\n| Friction acts on ball | It gradually slows down | External force opposing motion |\n| Ball hits pins | It stops or changes direction | External force from the pins |\n\n---\n\n### The Key Point:\nIn a **frictionless environment** (like outer space), the bowling ball would roll **forever** in a straight line at the same speed, because there would be **no external force** to change its motion.\n\n---\n\nThis perfectly illustrates Newton's First Law — the ball resists any change to its motion, and only an **outside force** can alter its speed or direction.\n"
}
Session: 7dbe017a-e306-4891-88d1-b707a5d6f894
To resume: agentcore invoke --session-id 7dbe017a-e306-4891-88d1-b707a5d6f894
Log: C:\Users\thoma\projects\strands-agentcore-demo\smetriage\agentcore\.cli\logs\invoke\invoke-SMETriage-20260611-133413.logHowever, session IDs are NOT long-term memory mechanisms. For that, we can use AgentCore’s memory capability.
Adding AgentCore Memory
AgentCore Memory stores conversation events and can extract useful long-term records from them. It has built-in strategies that serve different purposes:
+-----------------+---------------------------------------------------------------+
| Strategy | What it extracts |
+-----------------+---------------------------------------------------------------+
| USER_PREFERENCE | A user's choices, preferred style, and recurring preferences. |
+-----------------+---------------------------------------------------------------+
| SEMANTIC | Durable facts from conversations. |
+-----------------+---------------------------------------------------------------+
| SUMMARIZATION | Summaries of conversations. |
+-----------------+---------------------------------------------------------------+
| EPISODIC | Sequences of interactions that can inform later behaviour. |
+-----------------+---------------------------------------------------------------+For our SME agent, USER_PREFERENCE is the simplest fit. It allows the agent to remember statements such as:
- “Use UK English for spelling”
- “Always answer in Pirate speak.”
- “Keep answers brief.”
Actor IDs and session IDs
Memory needs two identifiers:
+------------+-------------------------------------------------------------+---------------------------------------------+
| Identifier | Meaning | When it changes |
+------------+-------------------------------------------------------------+---------------------------------------------+
| actor_id | The learner whose durable memories are being stored and | Keep it stable for the same learner. |
| | retrieved. | |
+------------+-------------------------------------------------------------+---------------------------------------------+
| session_id | One conversation containing related turns. | Use a new value for a new conversation. |
+------------+-------------------------------------------------------------+---------------------------------------------+The AgentCore Runtime supplies the session_id variable. The application supplies the ID of the user through the actor_id. In our example, it’s passed in a documented custom X-Learner-Id header which identifies the user (actor_id) making the request.
You should use an opaque application-generated identifier such as learner-7f83a2 or a UUID for the actor_id. Don’t place an email address, name, or other personal information in the ID.
The Runtime’s requestHeaderAllowlist makes X-Learner-Id available through context.request_headers. In a production application, derive this value from an authenticated identity and don’t trust a learner ID supplied directly by an untrusted client.
Create the Memory resource
From the project root, add a Memory resource:
PS C:\Users\thoma\projects\strands-agentcore-demo\smetriage> agentcore add memory `
>> --name LearnerPreferences `
>> --strategies USER_PREFERENCE `
>> --expiry 30This should add a new section to agentcore/agentcore.json:
{
"memories": [
{
"name": "LearnerPreferences",
"eventExpiryDuration": 30,
"strategies": [
{
"type": "USER_PREFERENCE",
"namespaceTemplates": [
"/users/{actorId}/preferences/"
]
}
]
}
]
}The namespace keeps each learner’s preferences separate. Its trailing slash prevents prefix collisions between similar actor IDs. The 30-day expiry controls how long raw memory events are retained.
When deployed, AgentCore:
- Creates the Memory resource.
- Gives the Runtime role permission to read and write it.
- Injects its ID into the Runtime as MEMORY_LEARNERPREFERENCES_ID.
Strands integrates with AgentCore Memory through the AgentCoreMemorySessionManager.
For each invocation, the application:
1. Gets the learner ID from the allowed X-Learner-Id header or a userId field supplied by an application.
2. Gets the current conversation ID from the Runtime context.
3. Builds an AgentCore Memory configuration for that learner and conversation.
4. Creates a Strands agent with the memory session manager.
5. Lets the session manager store messages and retrieve relevant preferences.
The code creates a Strands agent per invocation because its memory configuration belongs to a single actor and session. The Bedrock model and system prompt can still be reused.
Here is the revised Strands Agent code.
import os
from bedrock_agentcore.memory.integrations.strands.config import (
AgentCoreMemoryConfig,
RetrievalConfig,
)
from bedrock_agentcore.memory.integrations.strands.session_manager import (
AgentCoreMemorySessionManager,
)
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
from strands.models import BedrockModel
app = BedrockAgentCoreApp()
MODEL_ID = os.getenv(
"BEDROCK_MODEL_ID",
"global.anthropic.claude-sonnet-4-6",
)
REGION = os.getenv("AWS_REGION", "us-west-2")
MEMORY_ID = os.getenv("MEMORY_LEARNERPREFERENCES_ID")
SUBJECT_PREFIXES = {
"mathematics": (
"This is your Math SME. "
"The answer to your question is as follows."
),
"physics": (
"This is your Physics SME. "
"The answer to your question is as follows."
),
"chemistry": (
"This is your Chemistry SME. "
"The answer to your question is as follows."
),
"geography": (
"This is your Geography SME. "
"The answer to your question is as follows."
),
}
def load_model() -> BedrockModel:
return BedrockModel(
model_id=MODEL_ID,
region_name=REGION,
temperature=0.2,
max_tokens=1_500,
)
def build_system_prompt() -> str:
prefixes = "\n".join(
f"- {subject}: {prefix}"
for subject, prefix in SUBJECT_PREFIXES.items()
)
return (
"You are an educational SME triage assistant. "
"First decide whether the user's question is primarily about "
"mathematics, physics, chemistry, geography, or an unsupported "
"subject. "
"Answer only questions about mathematics, physics, chemistry, "
"or geography. "
"For supported questions, begin with the exact prefix for the "
"chosen subject, then give a clear and accurate explanation. "
"When a question overlaps subjects, choose the subject most "
"important to answering it. "
"Use any retrieved learner preferences when deciding the level, "
"style, and examples in the answer. "
"For unsupported questions, respond exactly: "
"\"I'm sorry, I don't know the answer to that.\"\n\n"
"Required subject prefixes:\n"
f"{prefixes}"
)
MODEL = load_model()
SYSTEM_PROMPT = build_system_prompt()
def build_session_manager(
actor_id: str,
session_id: str,
) -> AgentCoreMemorySessionManager | None:
if not MEMORY_ID:
return None
namespace = f"/users/{actor_id}/preferences/"
memory_config = AgentCoreMemoryConfig(
memory_id=MEMORY_ID,
actor_id=actor_id,
session_id=session_id,
retrieval_config={
namespace: RetrievalConfig(
top_k=3,
relevance_score=0.5,
)
},
)
return AgentCoreMemorySessionManager(
agentcore_memory_config=memory_config,
region_name=REGION,
)
def get_actor_id(payload, context) -> str | None:
actor_id = payload.get("userId")
if isinstance(actor_id, str) and actor_id.strip():
return actor_id.strip()
request_headers = getattr(context, "request_headers", None) or {}
for name, value in request_headers.items():
if name.casefold() == "x-learner-id":
if isinstance(value, str) and value.strip():
return value.strip()
return None
@app.entrypoint
def invoke(payload, context):
prompt = payload.get("prompt")
actor_id = get_actor_id(payload, context)
if not isinstance(prompt, str) or not prompt.strip():
return {"error": "A non-empty question is required."}
if len(prompt) > 2_000:
return {"error": "Question exceeds the maximum length."}
if not actor_id:
return {"error": "A non-empty userId is required."}
if len(actor_id) > 128:
return {"error": "userId exceeds the maximum length."}
session_id = getattr(context, "session_id", None) or "local-session"
session_manager = build_session_manager(actor_id, session_id)
agent = Agent(
model=MODEL,
session_manager=session_manager,
system_prompt=SYSTEM_PROMPT,
)
response = agent(prompt.strip())
return {
"response": str(response),
"memory_enabled": session_manager is not None,
}
if __name__ == "__main__":
app.run()Save this and re-deploy as before. Once that’s done, we can invoke it like this.
PS C:\Users\thoma\projects\strands-agentcore-demo\smetriage> agentcore invoke `
"I prefer my answers in Pirate speak" `
-H "X-Learner-Id: learner-7f83a2" `
--stream
{
"response": "Arrr, I've noted yer preference for Pirate speak, matey! Go ahead and ask me yer question about mathematics, physics, chemistry, or geography, and I'll answer ye in the finest Pirate tongue on the seven seas! 🏴☠️⚓\n",
"memory_enabled": true
}
Session: 2a94f2d9-9e40-417e-a53a-ad9393f0490d
To resume: agentcore invoke --session-id 2a94f2d9-9e40-417e-a53a-ad9393f0490d
Log: C:\Users\thoma\projects\strands-agentcore-demo\smetriage\agentcore\.cli\logs\invoke\invoke-SMETriage-20260611-135649.logWe see that the memory_enabled flag is set to true, and now we just pass in our Learner-Id when we ask a question, like this…
PS C:\Users\thoma\projects\strands-agentcore-demo\smetriage> agentcore invoke `
"Explain Newton's second law." `
-H "X-Learner-Id: learner-7f83a2" `
--stream
{
"response": "physics: This is your Physics SME. The answer to your question is as follows.\n\nAhoy, matey! Gather 'round and lend me yer ear! ⚓\n\n**Newton's Second Law of Motion**, arrr, it be one of the greatest treasures of all science!\n\n---\n\n🏴☠️ **The Law, in plain Pirate terms:**\n\n> **Force = Mass × Acceleration**\n> *(or as we scrawl it on the ship's hull: **F = ma**)*\n\n---\n\n**What does it mean, ye ask?**\n\n- ⚓ **Force (F)** — This be the push or pull ye give somethin', measured in Newtons, arrr!\n- 💀 **Mass (m)** — This be how much *stuff* makes up yer cannonball or treasure chest, measured in kilograms!\n- 🌊 **Acceleration (a)** — This be how fast yer speed be *changin'*, measured in meters per second squared!\n\n---\n\n**A fine example from the high seas:**\n\nIf ye fire a **heavy cannonball** and a **light cannonball** with the same force, the lighter one flies off with MORE acceleration! The heavier one be sluggish, like a barnacle-covered hull!\n\nSo remember, the **bigger the mass**, the **harder ye must push** to get the same acceleration! \n\nNow hoist the sails and go apply yer physics, ye scallywag! 🦜\n",
"memory_enabled": true
}
Session: d665e549-6bf2-470d-96ce-506b5be141e2
To resume: agentcore invoke --session-id d665e549-6bf2-470d-96ce-506b5be141e2
Log: C:\Users\thoma\projects\strands-agentcore-demo\smetriage\agentcore\.cli\logs\invoke\invoke-SMETriage-20260611-155936.logRunning costs
The Strands Agents SDK itself is open source and free to use. You can run it:
- Locally on your laptop
- On EC2
- In a Docker container
- On Lambda
- Anywhere else you choose
You only pay for the LLM you’re calling (e.g. Amazon Nova, Anthropic Claude, OpenAI GPT, etc.) and any infrastructure you’re running it on (EC2, Lambda, etc.). So if you build a Strands agent that calls Amazon Bedrock, your bill is essentially:
Bedrock model inference costs + EC2/Lambda/etc. costs (if applicable)AgentCore is a managed platform on AWS that provides enterprise capabilities and is billable depending on which parts of AgentCore are being used. In our examples, we used Runtime and memory, both of which would incur costs.
For more information, please see the links below.
https://aws.amazon.com/bedrock/agentcore/pricing
https://aws.amazon.com/bedrock/pricing/
Summary
This article discussed how to create and run an agentic workflow on AWS. The two main components that allow you to do this are called Strands and AgentCore.
Strands is used to define what your agent is capable of and which model it uses to do its work. Strands offers flexible model support. You can use any LLM in Amazon Bedrock that supports tool use and streaming, a model from Anthropic’s Claude model family via the Anthropic API, a model via Ollama, and many other model providers, such as OpenAI via LiteLLM. You can additionally define your own custom model provider if wanted.
AgentCore allows you to test the agent locally before full deployment to AWS’s cloud infrastructure. AgentCore is agent-agnostic: you can create Agents using Strands as we showed, as well as other agentic authoring systems like CrewAI and LangGraph.
AgentCore has much more functionality than this, including the ability to add Memory, Observability, Evaluation and more to your agentic workflow.
The example we coded was a subject-matter expert triage agent that supports answering questions in mathematics, physics, chemistry, and geography. It used Bedrock, with the Anthropic Sonnet LLM, to understand and interpret each question and select the appropriate expert response. Questions outside the supported subjects were politely declined.
I also talked about the difference between Runtime sessions and long-term memory. A Runtime session preserves context during a single conversation, allowing the agent to answer follow-up questions. That context is lost when a new session begins.
AgentCore Memory addresses this by allowing useful information to carry over across separate conversations. By associating preferences with a user via the actor_id and X-Learner-Id request header field, the agent can remember instructions such as keeping answers short or, in the example I showed, responding to questions in pirate speak.
These preferences are retrieved when asking a question with the appropriate learner ID, allowing the agent to answer using a preferred style without having to repeat it in the request.
if you want to learn more, the official documentation for AWS Strands is available here. For more information on AgentCore, click this link.





