The Brief
The assignment sounded straightforward on paper: build an AI customer support agent for an e-commerce platform using Amazon Bedrock AgentCore and the Strands SDK. The agent needed to do five things well:
- Track orders and process refunds through Lambda functions, exposed via an AgentCore Gateway using the Model Context Protocol (MCP)
- Answer product and policy questions using Retrieval-Augmented Generation (RAG) against a Bedrock Knowledge Base
- Remember customers across sessions using AgentCore Memory
- Calculate precise loyalty discounts using a sandboxed Code Interpreter
- Browse live web pages using the AgentCore Browser Tool
Eight TODOs in a starter main.py. Six required test scenarios. A $25 AWS sandbox budget. What could go wrong?
A lot, as it turned out — and that's exactly what made this project worth writing up.
Part 1: Writing the Code (The Easy Part)
Before touching a single AWS resource, I worked through all 8 TODOs in isolation. This turned out to be the right call — every piece of infrastructure we'd build later depended on this code being correct first, and testing logic without burning cloud budget is just good sense.
The agent skeleton
app = BedrockAgentCoreApp()
model = BedrockModel(model_id=model_id)
memory_client = MemoryClient(region_name=REGION)
_bedrock_runtime = boto3.client("bedrock-agent-runtime", region_name=REGION)
Four lines, four responsibilities: the AgentCore runtime wrapper, the LLM client, the memory client, and the low-level client for Knowledge Base retrieval.
The memory hook — the trickiest piece
MemoryHook needed to do two things automatically, via Strands' hook system:
- Before each response, retrieve relevant long-term memories for the customer and inject them into the prompt
- After each response, save the turn (user query + assistant reply) back to AgentCore Memory
The retrieval side walks the message list, skips anything that isn't a plain-text user message (tool results don't count), queries every memory namespace (SEMANTIC facts and USER_PREFERENCE), and prepends tagged results to the message:
last_message["content"][0]["text"] = (
f"Customer Context:\n{context_block}\n\n{user_query}"
)
The save side walks the messages backwards to find the last assistant reply, then keeps walking to find the last plain-text user message before it — because you can't just grab the last two messages; there might be tool calls in between.
The loyalty discount calculator
This one had a design decision baked into it: should the math run in plain Python inside the agent, or in AgentCore's Code Interpreter sandbox?
I went with the sandbox. The reasoning: LLMs are famously unreliable at arithmetic, and a customer-facing discount calculation needs to be exactly right, every time, auditable. Running it as a generated Python script inside a secure sandbox guarantees deterministic, traceable results — not "the model's best guess at the math."
with code_session(REGION) as code_client:
response = code_client.invoke(
"executeCode",
{"code": code, "language": "python", "clearContext": True},
)
But sandboxes can go down. So there's a fallback: if the Code Interpreter throws, a simpler tier-only discount gets computed locally in plain Python, and the response flags that a fallback was used. Graceful degradation over a hard failure.
The business rules, reverse-engineered
The point-redemption logic wasn't fully spelled out in the spec, so I had to make a reasonable call: 100 points = $1, redemption floors to the nearest 500 points, capped at 50% of the order total. I verified this by hand against the actual test case later (Gold member, 4,250 points, $200 order) and it checked out exactly: 4,000 points redeemed, $144 final total, $56 in savings, 394 points remaining.
Part 2: Standing Up the Infrastructure (Where It Got Interesting)
With the code done, it was time to build the AWS side: two Lambda functions, a REST API, an AgentCore Gateway with two targets, a Memory resource, and a Knowledge Base.
Lesson #1: Sandbox IAM roles are not what you expect
The first surprise: there was no reusable LabRole in this Vocareum sandbox, and attempting to even read some pre-existing roles (like vocareum) returned an explicit AccessDenied — not a missing-permission error, an explicit deny baked into the account's policy.
The fix was simple once I stopped fighting it: create my own narrowly-scoped IAM roles via CLI. One for Lambda execution, one for the Gateway, one (unused in the end) for the Knowledge Base.
Lesson #2: AgentCore Gateway has real CLI support — mostly
A pleasant surprise: aws bedrock-agentcore-control is a full CLI command group, meaning the entire Gateway — creation, targets, memory — could be scripted rather than clicked through a console. This was faster and more repeatable, which mattered a lot later when the whole thing needed rebuilding from scratch.
Setting up the API Gateway target for order tracking failed twice before succeeding:
- First failure:
toolFilterscan't be an empty array — the Gateway needs explicit filter entries telling it which routes to expose, not justtoolOverridesfor naming. - Second failure:
Failed to parse OpenAPI specification... .responses is missing. The Gateway introspects your live REST API by exporting its OpenAPI spec, and API Gateway requires a Method Response (a defined status code) on every method for that export to be valid. I'd wired up methods and integrations but never added method responses. Oneput-method-responsecall per route fixed it.
Lesson #3: Not everything is scriptable, and that's fine
Creating an OpenSearch Serverless collection via CLI hit another explicit deny — aoss:CreateSecurityPolicy was blocked outright for this sandbox identity. Rather than fight it, I switched to the AWS Console's "Quick create" Knowledge Base flow, which uses a newer AWS-managed vector store and provisions its own service role automatically. Sometimes the pragmatic move is knowing when to stop scripting and click a button.
Part 3: Deployment — Where Windows Became the Enemy
This is where the project got genuinely hard, and where most of the real engineering lessons live.
Attempt 1: Container deploy — blocked by sandbox policy
agentcore deploy defaults to building a Docker container via AWS CodeBuild. It got through IAM role creation, ECR repo setup, source upload — and then the CodeBuild job died in the PROVISIONING phase after six seconds, with an empty CloudWatch log stream. No error message. Nothing.
The root cause, after digging into the build environment config: privilegedMode: true. CodeBuild needs privileged (Docker-in-Docker) mode to build container images, and this sandbox's Service Control Policies silently block privileged build environments — the exact same "quietly restrict anything that could run arbitrary privileged workloads" pattern that blocked OpenSearch Serverless creation.
Attempt 2: Direct Code Deploy — no Docker required
The fix: switch deployment modes entirely. AgentCore's Direct Code Deploy ships your Python code straight to the runtime with no container build step — but the CLI only offers it once it detects a zip utility on your machine. Windows doesn't ship one by default.
winget install -e --id GnuWin32.Zip
$env:PATH += ";C:\Program Files (x86)\GnuWin32\bin"
The pywin32 rabbit hole
With zip installed, deployment got further — and then failed resolving dependencies for the Linux ARM64 target, because playwright (needed for the browser tool) pulls in pywin32 on Windows, and uv's cross-platform resolver got confused about which platform the marker should apply to.
My first fix attempt was actually backwards — I wrote an override marker of sys_platform == 'linux', which I intended to mean "never true," but which is actually true when resolving for the Linux target, making the problem worse. The correct fix uses a condition that's structurally never true on any platform:
[tool.uv]
override-dependencies = [
"pywin32; python_version < '0'",
]
The silent deploy failure
After all that, the agent deployed — and crashed on startup with AttributeError: 'NoneType' object has no attribute 'entrypoint'. Turns out one of the very first code edits (app = BedrockAgentCoreApp()) had never actually made it from my working copy into the real local file, despite several rounds of "please also apply this locally."
The lesson that stuck: when debugging across a human-in-the-loop copy-paste workflow, stop trusting incremental instructions once something goes wrong. Hand over the entire corrected file and have it replaced wholesale. It's slower per-step but eliminates an entire class of "did that edit actually land" bugs.
Part 4: Testing — 5 for 6, Then 6 for 6
Test 3 (Knowledge Base) — the silent permission gap
The agent responded to Platinum-tier questions with confidently hallucinated, generic answers and a vague apology about "access issues." No crash, no obvious error — because Strands catches tool exceptions and feeds them back to the model as text, which the model then smooths over conversationally.
Digging into the auto-created execution role's IAM policy confirmed it: no bedrock:Retrieve permission anywhere. The role had model-invoke permissions, memory permissions, logging permissions — but nobody had thought to grant Knowledge Base retrieval. One scoped inline policy later, Test 3 passed with real retrieved content matching the actual product catalog.
Test 4 (Memory) — the hidden default session
Long-term memory across sessions kept "working" suspiciously well on the first attempt — because agentcore invoke reuses the same session ID by default unless you explicitly pass one. Every test I'd run that day had silently shared one continuous session, which meant I was accidentally testing short-term conversational continuity, not genuine cross-session long-term memory retrieval.
The fix was trivial once spotted: pass explicit, distinct session_id values for Session A and Session B, with a 30-second wait between them for AgentCore Memory's async extraction to complete.
Test 6 (Browser Tool) — two layered bugs, stacked
This was the hardest bug of the project, and it took two separate root causes to fully resolve:
Bug 1 — a cross-platform packaging gotcha. The deployment zip was built on Windows. NTFS has no concept of a Unix executable bit, so when the bundled Playwright Node.js driver landed on the Linux ARM64 runtime, it had no execute permission at all:
PermissionError: [Errno 13] Permission denied: '/var/task/playwright/driver/node'
My first patch attempt — copying the driver to /tmp, chmod-ing it executable, and monkeypatching Playwright's compute_driver_executable function — didn't work. Same identical traceback after redeploying. The reason: Python's from module import name binds a separate reference in the importing module's namespace. I'd patched playwright._impl._driver.compute_driver_executable, but playwright._impl._transport had already done from ._driver import compute_driver_executable at import time, holding its own bound copy that my patch never touched. The fix required patching both modules' references to the function, not just the source module's.
Bug 2 — an IAM gap, hidden behind a logging level. Once the permission error cleared, the browser tool failed again — this time completely silently, because the actual exception was being logged at a level my logging.basicConfig(level=logging.WARNING) was suppressing. Temporarily cranking logging to DEBUG on the specific strands_tools.browser logger surfaced the real error in one deploy cycle:
AccessDeniedException ... StartBrowserSession ... on resource:
arn:aws:bedrock-agentcore:us-east-1:aws:browser/aws.browser.v1
The twist here: that ARN's account segment is the literal string "aws" — not my account ID — because it's AWS's own shared managed browser resource. My first attempt at the fix used my account ID in that slot, which silently failed to match and produced the exact same error, making it look like the fix hadn't worked at all.
With both bugs fixed, Test 6 finally passed:
"Learn the Latest Tech Skills; Advance Your Career | Udacity"
Part 5: The Review, The Rejection, and the Full Rebuild
The first submission came back with specific, fair feedback:
- Gateway tool loading had no error handling. The code called
list_tools_sync()directly with notry/except, meaning a Gateway outage would crash the whole turn instead of failing gracefully with a logged, user-facing message. - The Knowledge Base guard clause needed to be demonstrated, not just coded — a screenshot proving it fires correctly when
KB_IDis unconfigured, not just present in the source. - The memory test screenshots only showed recall, not the original information shared — the reviewer needed to see both halves of the workflow.
None of this was a logic failure in the agent itself. It was a documentation and defensive-coding gap. The actual fix for #1 was almost embarrassingly small:
try:
gateway_tools = mcp_client.list_tools_sync()
tools.extend(gateway_tools)
logger.info("Gateway connected successfully. Loaded %d tools.", len(gateway_tools))
except TimeoutError:
logger.exception("Gateway tool loading timed out")
except ConnectionError:
logger.exception("Gateway connection failed")
except Exception as exc:
logger.exception("Gateway tool loading failed: %s", exc)
But here's the complication: by the time this feedback arrived, every piece of AWS infrastructure from the original submission had already been torn down — deliberately, to avoid burning the sandbox budget while waiting for review. Worse, when I went to reconnect, the entire session had been revoked with a blanket AccessDenied on every API call — not "resources don't exist," but "you are not authorized to do anything," apparently triggered by Vocareum's own session lifecycle.
The only path forward was a full re-provision from scratch: new IAM roles, new Lambdas, new REST API, new Gateway with two new targets, new Memory resource, new Knowledge Base — all with fresh resource IDs. The silver lining: because every failure mode from the first pass was already diagnosed and documented, the rebuild went almost entirely clean on the first try. The only two issues that recurred were the two known fixes (KB retrieve permission, browser session permission), both pre-applied before testing even started this time.
All six tests passed on the rebuilt infrastructure — plus a bonus screenshot explicitly proving the KB guard clause firing correctly when KB_ID is blanked out, which forced a fresh, un-memorized question (CUST-123's memory had already absorbed the Platinum-tier facts from repeated testing, so the agent started answering from memory instead of calling the tool at all — a neat little reminder that long-term memory can make a tool look broken when it's actually just no longer being invoked).
The resubmission came back clean: every criterion marked Awesome, reflection praised for "considerable systems thinking," full marks across the board.
What Actually Mattered
Looking back, the five lessons that will stick with me longest aren't about AgentCore specifically — they're the kind of thing that applies to any serverless, cross-platform, permission-boundaried system:
-
Never build deployment packages containing native binaries on a different OS than the target runtime. NTFS can't represent the Unix executable bit — this isn't a Playwright quirk, it's a fundamental Windows/Linux packaging mismatch that will bite any native dependency, not just a browser driver.
-
from module import namecreates an independent reference. Monkeypatching the source module's attribute doesn't reach code that already imported the name directly — every importing module holding its own reference needs patching individually. -
AWS-managed shared resources use
awsas the literal account segment in their ARN, not your account ID. Get this wrong and you get the exact sameAccessDeniedException, making it look like your fix did nothing. -
Logging level is a debugging tool, not just an output filter. An exception swallowed by a framework and logged at a suppressed level looks indistinguishable from "nothing happened" — until you turn the volume up on the specific library that's failing.
-
Build least-privilege IAM roles incrementally, discovered through testing — not granted broadly up front. Every permission gap in this project (
bedrock:Retrieve,StartBrowserSession) was a role that had been auto-generated with a sensible default set of permissions that simply hadn't anticipated every tool the agent would eventually call. That's not a failure of planning; it's the normal shape of building an agent with multiple, evolving tool integrations — and testing is what surfaces it.
Final stack: Amazon Bedrock AgentCore (Runtime, Gateway, Memory, Code Interpreter, Browser), Strands SDK, AWS Lambda, API Gateway, Bedrock Knowledge Base with managed vector store — deployed via Direct Code Deploy, no containers required.