{"slug": "i-built-a-customer-support-agent-on-amazon-bedrock-agentcore-and-it-refused-to", "title": "I built a customer support agent on Amazon Bedrock AgentCore — and it refused to say my name", "summary": "A developer built a customer support agent on Amazon Bedrock AgentCore that combines an MCP Gateway, a Bedrock Knowledge Base, AgentCore Memory, Code Interpreter, and a Browser Tool, reasoning with Amazon Nova 2 Lite via the Strands Agents SDK. The writeup documents three implementation gotchas, including that Gateway target names reject underscores and must use hyphens only, and one bug that passed every test.", "body_md": "I spent ten years in asset management, most of it translating what portfolio managers and quants built into something clients could actually follow. Now I build agents. It turns out the job is weirdly similar: an agent is mostly a translation layer between a person asking a vague question and a set of systems that only understand precise ones.\n\nThis is the second project of the **AWS AI & ML Scholars — Future Agent Engineer** track: a customer support agent for a fictional e-commerce store, running on **Amazon Bedrock AgentCore**. Here's how it's put together, the three things that tripped me up, and one bug that passed every test.\n\n👉 **Repo:** [github.com/Mialy333/agentcore-customer-support-agent](https://github.com/Mialy333/agentcore-customer-support-agent)\n\nOne conversational entrypoint, six capabilities:\n\n| Ask | What answers it | \n|---|---|\n| \"Where's my order ORD-001?\" | **AgentCore Gateway** (MCP) → API Gateway → Lambda | \n| \"Refund my Kindle\" | **AgentCore Gateway** (MCP) → Lambda | \n| \"What does Platinum tier get me?\" | **Bedrock Knowledge Base** (RAG) | \n| \"Do you remember me?\" | **AgentCore Memory** via a Strands hook | \n| \"What's my discount with 4,250 points?\" | **Code Interpreter** (sandboxed Python) | \n| \"What's on this web page?\" | **Browser Tool** | \n\nIt runs on **AgentCore Runtime** (serverless, direct code deploy, Python 3.13), uses the **Strands Agents SDK**, and reasons with **Amazon Nova 2 Lite**.\n\nThe whole agent fits in a single `main.py`. The entrypoint is short because each capability is a tool or a hook:\n\n``` python\n@app.entrypoint\nasync def invoke(payload, context=None):\n    user_input = payload.get(\"prompt\", \"\")\n    actor_id = payload.get(\"customer_id\", \"anonymous\")\n    session_id = payload.get(\"session_id\") or str(uuid.uuid4())\n\n    memory_hook = MemoryHook(actor_id, session_id, memory_client, MEMORY_ID)\n    browser = AgentCoreBrowser(region=REGION)\n    tools = [search_knowledge_base, calculate_loyalty_discount, browser.browser]\n\n    gateway_client = MCPClient(lambda: streamable_http_client(GATEWAY_URL))\n    with gateway_client:\n        all_tools = tools + gateway_client.list_tools_sync()\n        agent = Agent(model=model, tools=all_tools,\n                      hooks=[memory_hook], system_prompt=SYSTEM_PROMPT)\n        return agent(user_input).message[\"content\"][0][\"text\"]\n```\n\nNine tools reach the model: three local ones, plus six discovered at runtime from the Gateway over MCP. The agent doesn't know (or care) that `get_order` lives behind API Gateway while `initiate_refund` is a bare Lambda — it just sees an MCP tool list.\n\nThis was the part I found most \"production-shaped\". The two Lambdas have nothing in common:\n\n`order-tracker`` event[\"resource\"]` and `pathParameters`.` refund-processor`\n\n```\nraw_tool = context.client_context.custom.get(\"bedrockAgentCoreToolName\", \"\")\ntool = raw_tool.split(\"___\", 1)[-1]   # \"refund-processor___initiate_refund\" → \"initiate_refund\"\n```\n\nAgentCore Gateway wraps both as targets of one MCP server. For the REST API, each operation ID (`get_order`, `get_customer`, `get_customer_orders`) becomes a tool name. For the Lambda, you paste a JSON tool schema.\n\n**Gotcha #1 — target names reject underscores.** I named my targets `order_tracker` and `refund_processor`, matching the Lambda files. The Gateway was created fine… and then both targets failed: the name must match `([0-9a-zA-Z][-]?){1,100}`. Hyphens only. `order-tracker` and `refund-processor` went through.\n\nBefore writing any agent code, `npx @modelcontextprotocol/inspector` against the Gateway URL is the fastest way to confirm all six tools are actually exposed.\n\nThe knowledge base is a single `product_catalog.txt` (products, return windows, refund timelines, loyalty tiers) uploaded to S3 and synced into a **Managed Knowledge Base**. The tool is a thin wrapper around the `Retrieve` API:\n\n```\nresp = _bedrock_runtime.retrieve(\n    knowledgeBaseId=KB_ID,\n    retrievalQuery={\"text\": query},\n)\nchunks = [r[\"content\"][\"text\"] for r in resp.get(\"retrievalResults\", [])\n          if r.get(\"content\", {}).get(\"text\")]\nreturn \"\\n---\\n\".join(chunks)\n```\n\nAsked about the Platinum tier, the agent returned exactly what the catalog says — free same-day shipping, 15% discount, priority support — instead of whatever the model half-remembers about loyalty programs in general. That's the whole point of RAG: answers you can trace back to a document.\n\nAgentCore Memory is configured with two built-in strategies:\n\n`cs_agent/{actorId}/facts`\n`cs_agent/{actorId}/preferences`\nThe agent saves raw conversation events; AgentCore extracts durable facts and preferences from them in the background.\n\nOn the agent side, I used a Strands `HookProvider` with two callbacks:\n\n``` python\ndef register_hooks(self, registry):\n    registry.add_callback(MessageAddedEvent, self.retrieve_customer_context)\n    registry.add_callback(AfterInvocationEvent, self.save_support_interaction)\n```\n\n`Customer Context:` block to that message.`create_event`.\nThe design choice I like: the agent's reasoning loop never knows memory exists. It just receives a slightly richer message. Retrieval and saving each live in their own method, and tool-result messages are skipped so memory is only queried on real user turns.\n\nThe test: in session A, *\"Hi, I am Jane. I prefer concise responses.\"* Wait 30 seconds for extraction. In session B, *\"Do you remember my name and communication preference?\"*\n\nFirst run: it recalled \"prefers concise responses\"… and then declined to tell me my name, explaining it didn't have access to personal identification details for security reasons.\n\nNothing in my code blocked it. The memory record was right there in the context. **The model itself decided a name looked like sensitive data** and got cautious — about a name the customer had volunteered minutes earlier.\n\nThe fix was a few sentences in the system prompt: the `Customer Context` block contains facts the customer previously and voluntarily shared, and stating them back is expected behavior. After that:\n\nIt remembered Jane, the preference, *and* her two recent orders. Lesson: guardrails baked into a foundation model can misfire in directions you never asked for, and sometimes prompt engineering means removing caution rather than adding it.\n\nLoyalty math has just enough rules to go wrong: points redeem in 500-point blocks ($5 each), capped at 50% of the order, then a tier discount (Gold 10%, Platinum 15%) applies, then you earn points on what you paid.\n\nInstead of trusting the model, `calculate_loyalty_discount` generates a small Python program and runs it in the **AgentCore Code Interpreter** sandbox:\n\n```\nwith code_session(REGION) as session:\n    response = session.invoke(\"executeCode\",\n        {\"code\": code, \"language\": \"python\", \"clearContext\": True})\n```\n\nGold member, 4,250 points, $150 order:\n\nIf the sandbox is unavailable, the tool falls back to a tier-only calculation and says so in its output, rather than failing silently or letting the model guess.\n\n```\nbrowser = AgentCoreBrowser(region=REGION)\ntools = [..., browser.browser]\n```\n\nAgentCore runs a managed headless browser; the agent got the live page title from udacity.com. The only real work was IAM.\n\nThe course intentionally uses the Python **Bedrock AgentCore Starter Toolkit**:\n\n```\nuv run agentcore configure --entrypoint main.py --name customer_support_agent \\\n  --deployment-type direct_code_deploy --runtime PYTHON_3_13 --disable-memory\nuv run agentcore deploy\nuv run setup_permissions.py\n```\n\n`setup_permissions.py` reads the resource IDs from `main.py` *as text* (with `ast`, never importing the agent), checks the account and role match the deployment, then attaches one inline policy scoped to this Knowledge Base, this Memory and the managed browser. Least privilege, rerunnable.\n\n**Gotcha #3 — region drift.** One `configure` run silently picked up `us-west-2` while every resource lived in `us-east-1`. Read the configuration summary before you deploy.\n\nSide note: every invoke now prints a notice that the Starter Toolkit is no longer supported in favor of the npm `@aws/agentcore` CLI. Don't install both — they ship the same `agentcore` binary.\n\nAll six scenarios passed. Then I re-read the refund output more carefully:\n\nRefund ID ✅, `APPROVED` ✅, \"3–5 business days\" ✅… and **Refund Amount: $0**. Plus a confident \"you don't need to return anything, this was a digital product\" — about a physical Kindle.\n\nWhat happened: `amount` is optional in the refund tool's schema, the Lambda defaults it to 0, and the model never called `get_order` to look up the $139.99 total first. Then it filled the gap with a plausible story.\n\nThe test checked for the *shape* of the right answer, not for the answer being right. Fixes I'd ship in v2:\n\n`amount` required in the schema, or better, have the Lambda look the amount up server-side so the model can't get it wrong;\nThis is the kind of thing I'd rather find in a sandbox than in a customer's bank account.\n\n`agentcore destroy`, then Gateway, Memory, KB, vector store, S3, API Gateway, Lambdas).\nThe hardest part of AgentCore isn't the Python. It's getting Gateway, Memory, the Knowledge Base, the Code Interpreter and the Browser to agree with each other — same region, same protocol, same IAM role.\n\nAnd the most interesting failures weren't infrastructure failures. They were the model being *too* careful with a name, and *not careful enough* with a refund amount. Which is, honestly, the same lesson I learned explaining portfolios to clients: the risk is rarely in the system you can see; it's in the gap between what someone asked and what got executed.\n\nThe full code, architecture diagrams, all six test screenshots and a 43-step setup walkthrough are in the repo:\n\n👉 [github.com/Mialy333/agentcore-customer-support-agent](https://github.com/Mialy333/agentcore-customer-support-agent)\n\n*I'm Mialy, building at the intersection of AI agents and digital assets as @ellebuild. If you've hit other AgentCore gotchas, I'd love to hear them in the comments.*", "url": "https://wpnews.pro/news/i-built-a-customer-support-agent-on-amazon-bedrock-agentcore-and-it-refused-to", "canonical_source": "https://dev.to/mialy333/i-built-a-customer-support-agent-on-amazon-bedrock-agentcore-and-it-refused-to-say-my-name-1n6l", "published_at": "2026-10-12 08:30:00+00:00", "updated_at": "2026-10-12 08:40:14.669731+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "agent-protocols", "ai-products", "large-language-models"], "entities": ["Amazon Bedrock AgentCore", "Amazon Web Services", "Strands Agents SDK", "Amazon Nova 2 Lite", "Model Context Protocol", "AWS Lambda", "Amazon API Gateway", "Bedrock Knowledge Base"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/i-built-a-customer-support-agent-on-amazon-bedrock-agentcore-and-it-refused-to", "markdown": "https://wpnews.pro/news/i-built-a-customer-support-agent-on-amazon-bedrock-agentcore-and-it-refused-to.md", "text": "https://wpnews.pro/news/i-built-a-customer-support-agent-on-amazon-bedrock-agentcore-and-it-refused-to.txt", "jsonld": "https://wpnews.pro/news/i-built-a-customer-support-agent-on-amazon-bedrock-agentcore-and-it-refused-to.jsonld"}}