This cookbook guides you through how to deploy a Strands Agent to Amazon Bedrock AgentCore Runtime with built-in observability. The implementation uses Amazon Bedrock Claude models and sends telemetry data to Braintrust through OpenTelemetry.
By the end of this cookbook, you’ll learn how to:
- Build a Strands Agent with web search capabilities using Amazon Bedrock Claude models
- Deploy the agent to Amazon Bedrock AgentCore Runtime for managed, scalable hosting
- Configure OpenTelemetry to send traces to Braintrust for observability
- Invoke the agent through both SDK and boto3 client
Key components
- Strands Agent: Python framework for building LLM-powered agents with built-in telemetry support
- Amazon Bedrock AgentCore Runtime: Managed runtime service for hosting and scaling agents on AWS
- OpenTelemetry: Industry-standard protocol for collecting and exporting telemetry data
Architecture
The agent is containerized and deployed to Amazon Bedrock AgentCore Runtime, which provides HTTP endpoints for invocation. Telemetry data flows from the Strands Agent through OTEL exporters to Braintrust for monitoring and debugging. The implementation uses a lazy initialization pattern to ensure proper configuration order.
Getting started
To get started, make sure you have:
- Python 3.10+
- AWS credentials configured with Bedrock and AgentCore permissions
- A Braintrust account and API key
- Docker installed locally
- Access to Amazon Bedrock Claude models in us-west-2
You’ll also want to install required dependencies from the requirements.txt file:
%pip install --force-reinstall -U -r requirements.txt --quiet
Agent implementation
The agent file (strands_claude.py) implements a travel agent with web search capabilities. The implementation uses a lazy initialization pattern to ensure telemetry is configured after environment variables, integrates Amazon Bedrock Claude models through the Strands framework, and includes web search via DuckDuckGo for real-time information. The agent is configured to send traces to Braintrust via OpenTelemetry:
%%writefile strands_claude.py
import os
import logging
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent, tool
from strands.models import BedrockModel
from strands.telemetry import StrandsTelemetry
from ddgs import DDGS
logging.basicConfig(level=logging.ERROR, format="[%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
logger.setLevel(os.getenv("AGENT_RUNTIME_LOG_LEVEL", "INFO").upper())
@tool
def web_search(query: str) -> str:
"""
Search the web for information using DuckDuckGo.
Args:
query: The search query
Returns:
A string containing the search results
"""
try:
ddgs = DDGS()
results = ddgs.text(query, max_results=5)
formatted_results = []
for i, result in enumerate(results, 1):
formatted_results.append(
f"{i}. {result.get('title', 'No title')}\n"
f" {result.get('body', 'No summary')}\n"
f" Source: {result.get('href', 'No URL')}\n"
)
return "\n".join(formatted_results) if formatted_results else "No results found."
except Exception as e:
return f"Error searching the web: {str(e)}"
# Function to initialize Bedrock model
def get_bedrock_model():
region = os.getenv("AWS_DEFAULT_REGION", "us-west-2")
model_id = os.getenv("BEDROCK_MODEL_ID", "us.anthropic.claude-3-7-sonnet-20250219-v1:0")
bedrock_model = BedrockModel(
model_id=model_id,
region_name=region,
temperature=0.0,
max_tokens=1024
)
return bedrock_model
# Initialize the Bedrock model
bedrock_model = get_bedrock_model()
# Define the agent's system prompt
system_prompt = """You are an experienced travel agent specializing in personalized travel recommendations
with access to real-time web information. Your role is to find dream destinations matching user preferences
using web search for current information. You should provide comprehensive recommendations with current
information, brief descriptions, and practical travel details."""
app = BedrockAgentCoreApp()
def initialize_agent():
"""Initialize the agent with proper telemetry configuration."""
# Initialize Strands telemetry with 3P configuration
strands_telemetry = StrandsTelemetry()
strands_telemetry.setup_otlp_exporter()
# Create and cache the agent
agent = Agent(
model=bedrock_model,
system_prompt=system_prompt,
tools=[web_search]
)
return agent
@app.entrypoint
def strands_agent_bedrock(payload, context=None):
"""
Invoke the agent with a payload
"""
user_input = payload.get("prompt")
logger.info("[%s] User input: %s", context.session_id, user_input)
# Initialize agent with proper configuration
agent = initialize_agent()
response = agent(user_input)
return response.message['content'][0]['text']
if __name__ == "__main__":
app.run()
Next we’ll use the starter toolkit to configure the AgentCore Runtime deployment with an entrypoint, the execution role, and a requirements file. We’ll also configure the starter kit to auto-create the Amazon ECR repository on launch.
During the configure step, your Dockerfile will be generated based on your application code.
When using the bedrock_agentcore_starter_toolkit to configure your agent, it configures AgentCore Observability by default. To use Braintrust, you need to disable AgentCore Observability by setting disable_otel=True.
from bedrock_agentcore_starter_toolkit import Runtime
from boto3.session import Session
boto_session = Session()
region = boto_session.region_name
agentcore_runtime = Runtime()
agent_name = "strands_braintrust_observability"
response = agentcore_runtime.configure(
entrypoint="strands_claude.py",
auto_create_execution_role=True,
auto_create_ecr=True,
requirements_file="requirements.txt",
region=region,
agent_name=agent_name,
disable_otel=True,
)
response
Deploy to AgentCore runtime
Now that we have a Dockerfile, let’s launch the agent to the AgentCore Runtime. This will create the Amazon ECR repository and the AgentCore Runtime.
To enable observability, we need to configure the OpenTelemetry endpoint and authentication. The agent will send traces to Braintrust using the OTEL protocol.
# Braintrust configuration
otel_endpoint = "https://api.braintrust.dev/otel"
braintrust_api_key = (
"<braintrust-api-key>" # For production, key should be securely stored
)
braintrust_project_id = "<braintrust-project-id>"
otel_auth_header = f"Authorization=Bearer {braintrust_api_key}, x-bt-parent=project_id:{braintrust_project_id}"
launch_result = agentcore_runtime.launch(
env_vars={
"BEDROCK_MODEL_ID": "us.anthropic.claude-3-7-sonnet-20250219-v1:0", # Example model ID
"OTEL_EXPORTER_OTLP_ENDPOINT": otel_endpoint,
"OTEL_EXPORTER_OTLP_HEADERS": otel_auth_header,
"DISABLE_ADOT_OBSERVABILITY": "true",
}
)
launch_result
Check deployment status
Wait for the runtime to be ready before invoking:
import time
status_response = agentcore_runtime.status()
status = status_response.endpoint["status"]
end_status = ["READY", "CREATE_FAILED", "DELETE_FAILED", "UPDATE_FAILED"]
while status not in end_status:
time.sleep(10)
status_response = agentcore_runtime.status()
status = status_response.endpoint["status"]
print(status)
print(f"Final status: {status}")
Invoke the agent
Finally, we can invoke our AgentCore Runtime with a payload.
invoke_response = agentcore_runtime.invoke(
{
"prompt": "I'm planning a weekend trip to Orlando. What are the must-visit places and local food I should try?"
}
)
from IPython.display import Markdown, display
display(Markdown("".join(invoke_response["response"])))
Logging in Braintrust
When you invoke the agent, logs are automatically generated for each invocation. Each agent interaction is captured in its own trace, with individual spans for tool calls and model interactions. To view your logs, navigate to your Braintrust project and select the Logs tab.
The trace view shows the full execution tree, including all agent interactions, tool calls (such as web_search), and model invocations with their latency and token usage.
The table view provides a summary of all traces with key metrics like duration, LLM duration, tool calls, and errors.
The traces include detailed information about agent invocation, tool calls, model interactions with latency and token usage, and complete request/response payloads.
Cleanup
When you’re finished, you can clean up the resources you’re not using anymore. This step is optional, but a best practice.
import boto3
# Delete the AgentCore Runtime and ECR repository
agentcore_control_client = boto3.client("bedrock-agentcore-control", region_name=region)
ecr_client = boto3.client("ecr", region_name=region)
# Delete the runtime
runtime_delete_response = agentcore_control_client.delete_agent_runtime(
agentRuntimeId=launch_result.agent_id,
)
# Delete the ECR repository
response = ecr_client.delete_repository(
repositoryName=launch_result.ecr_uri.split("/")[1], force=True
)
print("Cleanup completed")
Next steps
Now that you have a working Strands Agent deployed to Amazon Bedrock AgentCore Runtime with full observability, you can build on this foundation:
- Add more tools to expand agent capabilities beyond web search
- Create custom scorers to evaluate agent performance and accuracy
- Build evaluation datasets from production logs to continuously improve your agent
- Use the playground to test and refine agent behavior before deploying updates