DEV Community

Cover image for How to Build an AI Agent on AWS Using AWS AgentCore
Tendong Brain Nkengafac
Tendong Brain Nkengafac

Posted on

How to Build an AI Agent on AWS Using AWS AgentCore

Introduction

In under 100 lines of Python, you can build an AI agent that picks its own tools, reasons with Amazon Nova Pro on Amazon Bedrock, and runs as a serverless HTTP endpoint on Amazon Bedrock AgentCore Runtime. This article walks through that build step by step, from an empty folder to a deployed agent you can call from AWS Lambda.

I built this while following the Build Your First AI Agents with AWS AgentCore workshop from EduCloud Academy. I have expanded the session into a complete, reproducible guide, checked the code against the current SDK releases, and added the pitfalls I hit along the way.

Why agents need a serverless home. An agent's resource use is unpredictable: one request may answer instantly, the next may chain five tool calls and several model invocations. Paying for idle servers makes little sense for that pattern. AgentCore Runtime runs each session in an isolated microVM, scales to zero, and bills only for active consumption.

What AgentCore adds beyond hosting. AgentCore is a set of services for running agents in production: Runtime (hosting), Gateway (turning APIs and MCP servers into agent tools), Identity (safe access to external services on a user's behalf), Memory, Observability and more. This guide uses Runtime; the others plug into the same agent later.

What you will build

  1. A Strands agent with two community tools (calculator, current_time) and one custom tool (letter_counter).
  2. Debug logging that shows every model call and tool choice.
  3. An explicit Amazon Nova Pro model on Bedrock.
  4. An AgentCore-compatible HTTP service, tested locally with curl.
  5. A cloud deployment on AgentCore Runtime, invoked from the CLI and from Lambda.

Tested with: Python 3.10+, strands-agents 1.57, strands-agents-tools 0.8, bedrock-agentcore 1.24 and bedrock-agentcore-starter-toolkit 0.3.13 (October 2026). These SDKs move fast, so check the versions if a command differs.

Architecture

Every request takes the same path: a caller sends JSON to the AgentCore endpoint, the Strands agent loops between Nova Pro and its tools, and a plain JSON answer comes back.

Architecture diagram showing an AI agent workflow using Strands Agents, Amazon Bedrock AgentCore, tools, and AWS services.

The upper half is the request path; the lower half is the one-time deployment path that packages agent.py into a container AgentCore can run.

Component Role in this build
Strands Agents SDK Open-source Python framework that runs the agent loop and tool calls
Amazon Bedrock (Nova Pro) Managed foundation model that does the reasoning, via the Converse API
bedrock-agentcore SDK Wraps the agent in an HTTP service with /invocations and /ping
AgentCore Runtime Serverless host: one isolated microVM per session, billed on active use
AgentCore starter toolkit CLI that configures, builds (CodeBuild), stores (ECR) and deploys the agent
AWS Lambda / Amazon ECS Example callers that invoke the deployed agent through the AWS SDK
Amazon CloudWatch Receives the runtime's logs and traces

Prerequisites and project setup

You need an AWS account, credentials configured locally, Python 3.10 or newer, and access to Amazon Nova Pro in your Region.

Requirement How to check or set it up
AWS CLI v2 with credentials aws sts get-caller-identity returns your account ID
Python 3.10+ python3 --version
Bedrock model access Bedrock console → Model access → enable Amazon Nova Pro (us-east-1 used here)
IAM permissions (local dev) bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream on the Nova Pro inference profile
IAM permissions (deploy) Rights to create IAM roles, ECR repositories, CodeBuild projects and AgentCore runtimes (the toolkit creates these for you)

Create the project and a virtual environment:

mkdir first-agent && cd first-agent
python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
Enter fullscreen mode Exit fullscreen mode

Create requirements.txt:

strands-agents
strands-agents-tools
bedrock-agentcore
Enter fullscreen mode Exit fullscreen mode

Install the dependencies, plus the starter toolkit, which provides the agentcore CLI used for deployment:

pip install -r requirements.txt
pip install bedrock-agentcore-starter-toolkit
Enter fullscreen mode Exit fullscreen mode

The toolkit is a development tool, so it stays out of requirements.txt; the runtime container only needs the three packages above. Install everything inside the active virtual environment. In the workshop, the most common stumble was a package installed globally and missing at runtime.

The final project is deliberately small:

first-agent/
├── agent.py           # agent, tools, model and AgentCore entrypoint
├── requirements.txt   # runtime dependencies
└── invoke_lambda.py   # optional: calling the deployed agent from Lambda
Enter fullscreen mode Exit fullscreen mode

Step 1: A Strands agent with tools

A Strands agent is a model, a list of tools and a prompt; the framework runs the loop that lets the model decide which tool to call. Start with two community tools and one custom tool.

The custom tool counts letters in a word. It is a classic example because language models are unreliable at counting characters, while a five-line function is exact. The @tool decorator turns the function's signature and docstring into the tool specification the model reads, so a clear docstring matters.

# agent.py (Step 1)
from strands import Agent, tool
from strands_tools import calculator, current_time


@tool
def letter_counter(word: str, letter: str) -> int:
    """Count how many times a single letter appears in a word (case-insensitive).

    Args:
        word: The word or text to search.
        letter: The single letter to count.
    """
    if not isinstance(letter, str) or len(letter) != 1:
        raise ValueError("letter must be exactly one character")
    return word.lower().count(letter.lower())


agent = Agent(tools=[calculator, current_time, letter_counter])

if __name__ == "__main__":
    agent("What is the current time in UTC?")
    agent("What is 3111696 divided by 74088?")
    agent("How many times does the letter 'r' appear in 'strawberry'?")
Enter fullscreen mode Exit fullscreen mode

Run it:

python agent.py
Enter fullscreen mode Exit fullscreen mode

If you see ModuleNotFoundError: No module named 'strands_tools', the tools package went to another environment. Activate .venv and rerun pip install -r requirements.txt.

Expected output (the agent streams its answer to the terminal; wording varies between runs):

Tool #1: current_time
The current time in UTC is 2026-10-04T14:32:07+00:00.

Tool #2: calculator
3111696 divided by 74088 equals 42.

Tool #3: letter_counter
The letter 'r' appears 3 times in 'strawberry'.
Enter fullscreen mode Exit fullscreen mode

Each prompt was routed to a different tool. That routing is the core agent loop: the model reads the tool specifications, chooses one, Strands executes it, and the result goes back to the model to write the final answer.

A note on the model. No model is configured in this snippet, but the agent is still calling one. When you omit model, Strands defaults to a Claude model on Amazon Bedrock (global.anthropic.claude-sonnet-4-6 in version 1.57). That default needs Bedrock access for that model, which is a good reason to set the model explicitly, as Step 2 does.

Step 2: Debug logging and an explicit Bedrock model

Two changes make the agent production-ready in spirit: logs that show its decisions, and a model you chose on purpose.

Logging. Setting the strands logger to DEBUG prints model configuration, tool registration and each step of the event loop. That visibility pays off most in multi-agent systems, where the trace shows exactly which agent or tool misbehaved.

Model. BedrockModel wraps the Bedrock Converse API. Here it targets Amazon Nova Pro through the US cross-region inference profile us.amazon.nova-pro-v1:0, which routes requests across US Regions for better availability. A low temperature keeps tool-heavy answers consistent.

# agent.py (Step 2): additions shown at the top of the file
import logging
import os

from strands import Agent, tool
from strands.models import BedrockModel
from strands_tools import calculator, current_time

# Strands debug logs show every model call and tool selection
logging.getLogger("strands").setLevel(logging.DEBUG)
logging.basicConfig(
    format="%(levelname)s | %(name)s | %(message)s",
    handlers=[logging.StreamHandler()],
)

AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
MODEL_ID = os.getenv("MODEL_ID", "us.amazon.nova-pro-v1:0")  # cross-region inference profile

# ... letter_counter tool from Step 1 ...

model = BedrockModel(model_id=MODEL_ID, region_name=AWS_REGION, temperature=0.3)

agent = Agent(
    model=model,
    tools=[calculator, current_time, letter_counter],
    system_prompt="You are a concise assistant. Use a tool whenever one fits the request.",
)
Enter fullscreen mode Exit fullscreen mode

Reading the Region and model ID from environment variables means you can switch to another model, such as Claude or Nova Lite, without touching code.

Expected output. The first lines below are what Strands 1.57 prints at start-up; the event-loop lines are abridged and vary by version.

DEBUG | strands.models.bedrock | config=<{'model_id': 'us.amazon.nova-pro-v1:0', 'include_tool_result_status': 'auto', 'temperature': 0.3}> | initializing
DEBUG | strands.models.bedrock | region=<us-east-1> | bedrock client created
DEBUG | strands.tools.registry | tool_name=<calculator>, tool_type=<function>, is_dynamic=<False> | registering tool
DEBUG | strands.tools.registry | tool_name=<current_time>, tool_type=<function>, is_dynamic=<False> | registering tool
DEBUG | strands.tools.registry | tool_name=<letter_counter>, tool_type=<function>, is_dynamic=<False> | registering tool
DEBUG | strands.event_loop.event_loop | ... | streaming messages
Tool #1: letter_counter
DEBUG | strands.event_loop.event_loop | ... | tool execution completed
The letter 'r' appears 3 times in 'strawberry'.
Enter fullscreen mode Exit fullscreen mode

With the trace on, you can see the full path of a request: model configured, tools registered, model asked, tool chosen and executed, answer produced. When something breaks later, this is the first place to look.

Step 3: Wrap the agent for AgentCore Runtime and test locally

AgentCore Runtime expects an HTTP service with two routes: POST /invocations for requests and GET /ping for health checks, on port 8080. The bedrock-agentcore SDK provides both through four lines of code: import the app, initialise it, decorate an entrypoint, and run it.

Here is the complete agent.py:

"""First AI agent: Strands Agents + Amazon Bedrock (Nova Pro) on AgentCore Runtime."""
import logging
import os

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent, tool
from strands.models import BedrockModel
from strands_tools import calculator, current_time

# Strands debug logs show every model call and tool selection
logging.getLogger("strands").setLevel(logging.DEBUG)
logging.basicConfig(format="%(levelname)s | %(name)s | %(message)s", handlers=[logging.StreamHandler()])
logger = logging.getLogger("first-agent")

AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
MODEL_ID = os.getenv("MODEL_ID", "us.amazon.nova-pro-v1:0")  # cross-region inference profile


@tool
def letter_counter(word: str, letter: str) -> int:
    """Count how many times a single letter appears in a word (case-insensitive).

    Args:
        word: The word or text to search.
        letter: The single letter to count.
    """
    if not isinstance(letter, str) or len(letter) != 1:
        raise ValueError("letter must be exactly one character")
    return word.lower().count(letter.lower())


model = BedrockModel(model_id=MODEL_ID, region_name=AWS_REGION, temperature=0.3)

agent = Agent(
    model=model,
    tools=[calculator, current_time, letter_counter],
    system_prompt="You are a concise assistant. Use a tool whenever one fits the request.",
)

app = BedrockAgentCoreApp()


@app.entrypoint
def invoke(payload: dict) -> dict:
    """AgentCore entrypoint: receives the JSON body of each /invocations request."""
    prompt = payload.get("prompt", "Hello! What can you do?")
    logger.info("Received prompt: %s", prompt)
    result = agent(prompt)
    # AgentResult is not a clean API response; return plain text instead
    return {"result": str(result)}


if __name__ == "__main__":
    app.run()  # serves POST /invocations and GET /ping on port 8080
Enter fullscreen mode Exit fullscreen mode

Test locally with two terminals. In the first, start the service:

python agent.py
Enter fullscreen mode Exit fullscreen mode

In the second, check health, then send a prompt:

curl http://localhost:8080/ping

curl -X POST http://localhost:8080/invocations \
  -H "Content-Type: application/json" \
  -d '{"prompt": "How many times does the letter s appear in Mississippi?"}'
Enter fullscreen mode Exit fullscreen mode

Expected output:

{"status":"Healthy","time_of_last_update":1791145026}

{"result": "The letter 's' appears 4 times in 'Mississippi'."}
Enter fullscreen mode Exit fullscreen mode

The pitfall: returning the raw result. The workshop demo hit a runtime error at exactly this point, because the handler returned the agent's response object as-is. agent(prompt) returns an AgentResult, which holds the message plus metrics, traces and event-loop state. Depending on the SDK version and what that state contains, returning it directly either fails to serialise or leaks a large internal object to your callers. When I tested with bedrock-agentcore 1.24, the raw return succeeded but the response carried every metric and trace field. Returning {"result": str(result)} gives callers a small, stable contract; str() on an AgentResult yields the final text.

Step 4: Deploy to AgentCore Runtime and invoke it

The starter toolkit turns agent.py into a running cloud endpoint with two commands: configure and deploy. By default it builds an ARM64 container in AWS CodeBuild, so you do not need Docker on your machine.

Configure. Point the toolkit at your entrypoint. It detects requirements.txt and offers to create the IAM execution role and ECR repository for you; accepting the defaults is fine for a first deployment.

agentcore configure --entrypoint agent.py --name first_agent
Enter fullscreen mode Exit fullscreen mode

This writes a .bedrock_agentcore.yaml file holding the agent's settings. Keep it under version control, but never commit credentials.

Deploy. In toolkit 0.3, this command replaced the older agentcore launch, which many tutorials still show.

agentcore deploy
Enter fullscreen mode Exit fullscreen mode

When it finishes, the toolkit prints the agent runtime ARN. Check the status at any time:

agentcore status
Enter fullscreen mode Exit fullscreen mode

Expected output (abridged; the account ID and ARN suffix are placeholders):

Deployment completed successfully
Agent ARN: arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/first_agent-AbCdEf1234
Enter fullscreen mode Exit fullscreen mode

Invoke from the CLI:

agentcore invoke '{"prompt": "What is 15% of 2,480?"}'
Enter fullscreen mode Exit fullscreen mode
Response:
{"result": "15% of 2,480 is 372."}
Enter fullscreen mode Exit fullscreen mode

Invoke from AWS Lambda. Any AWS service that can call an API can use the agent, including Lambda and Amazon ECS. The bedrock-agentcore client's invoke_agent_runtime operation needs the runtime ARN, a JSON payload and a session ID of at least 33 characters. Requests that share a session ID reach the same isolated session, which keeps conversation context.

# invoke_lambda.py: Lambda handler that calls the deployed agent
import json
import logging
import os
import uuid

import boto3

logger = logging.getLogger()
logger.setLevel(logging.INFO)

client = boto3.client("bedrock-agentcore", region_name=os.environ.get("AWS_REGION", "us-east-1"))
AGENT_ARN = os.environ["AGENT_RUNTIME_ARN"]  # set in the Lambda configuration


def lambda_handler(event, context):
    prompt = event.get("prompt", "Hello!")
    session_id = event.get("session_id") or f"session-{uuid.uuid4()}"  # must be 33+ characters
    logger.info("Invoking agent, session=%s", session_id)

    response = client.invoke_agent_runtime(
        agentRuntimeArn=AGENT_ARN,
        runtimeSessionId=session_id,
        payload=json.dumps({"prompt": prompt}).encode(),
        contentType="application/json",
        accept="application/json",
    )
    body = json.loads(response["response"].read())  # response is a streaming body
    return {"statusCode": 200, "session_id": session_id, "answer": body.get("result")}
Enter fullscreen mode Exit fullscreen mode

The Lambda execution role needs bedrock-agentcore:InvokeAgentRuntime on the runtime ARN. If your Lambda runtime's bundled boto3 predates AgentCore, package a current boto3 with the function.

Expected output for the test event {"prompt": "How many vowels are in 'Cameroon'?"}:

{
  "statusCode": 200,
  "session_id": "session-3f6c1a9e-2b7d-4c1e-9a55-0d8e7f4b2c11",
  "answer": "There are 4 vowels in 'Cameroon': a, e, o, o."
}
Enter fullscreen mode Exit fullscreen mode

Troubleshooting, cost and cleanup

Most first-run failures come from environments, permissions or model access, not from the agent code.

Symptom Likely cause Fix
ModuleNotFoundError: strands_tools Package installed outside the virtual environment Activate .venv, reinstall from requirements.txt
AccessDeniedException calling the model Model access not enabled, or IAM policy missing Enable Nova Pro in Bedrock Model access; grant bedrock:InvokeModel*
ValidationException on the model ID Inference profile not available in your Region Use a profile valid for your Region, or set MODEL_ID
Raw metrics or a serialisation error in the response Handler returns the AgentResult object Return {"result": str(result)}
agentcore launch not found Toolkit 0.3 renamed the command Use agentcore deploy
Invocation works locally but fails in the cloud Execution role lacks Bedrock permissions Add bedrock:InvokeModel* to the runtime's execution role
ValidationException on runtimeSessionId Session ID shorter than 33 characters Prefix a UUID, as in the Lambda example

What it costs. You pay for three things: AgentCore Runtime (billed on active CPU and memory consumption per session, nothing while idle), Bedrock model tokens for Nova Pro, and small amounts of ECR storage, CodeBuild minutes and CloudWatch logs. For a tutorial-sized test, model tokens are usually the largest line. Check the Amazon Bedrock AgentCore pricing and Amazon Bedrock pricing pages for your Region, and set an AWS Budgets alert before you experiment.

Clean up when you are done, so nothing keeps billing:

agentcore destroy
Enter fullscreen mode Exit fullscreen mode

This removes the runtime and the resources the toolkit created, such as the ECR repository and CodeBuild project. Confirm in the console that no AgentCore runtime remains, and delete the CloudWatch log groups if you no longer need them.

Conclusion and next steps

You now have an agent that chooses between three tools, reasons with Amazon Nova Pro, and runs on AgentCore Runtime behind an HTTP endpoint that Lambda can call. The code is under 100 lines; the platform handles isolation, scaling and session management.

Technical takeaways

  • Tools beat prompting for exact work. Counting, arithmetic and time lookups belong in functions; the model's job is to choose and explain.
  • Turn on debug logging from day one. The trace shows each decision, and it becomes essential once you add more agents.
  • Design the response contract. Return a small, explicit JSON shape from your entrypoint, never the framework's internal objects.
  • Test locally before deploying. Two terminals and curl catch most bugs in seconds instead of after a cloud build.

Lessons from the workshop beyond the code. The second half of the session was about how to grow as a cloud and AI engineer, and three points stayed with me. Real skill matters more than collecting certificates. Structured learning paths still matter, because AI tools and endless content make it easy to drift without depth. And public building, through weekend projects and blog posts like this one, creates opportunities that waiting never will.

Where to go next

  1. Add AgentCore Memory so the agent remembers users across sessions.
  2. Expose an existing API or MCP server as tools through AgentCore Gateway.
  3. Use AgentCore Identity to let the agent act on a user's behalf in services like GitHub or Slack.
  4. Add the Code Interpreter and Browser built-in tools for more autonomous tasks.
  5. Wire up AgentCore Observability dashboards in CloudWatch.

References

Top comments (0)