Agentic AI · AWS Strands · Bedrock AgentCore CLI

Beacon: a CLI-First Support Agent on Strands + AgentCore

A coordinator and three specialists for a fictional DTC outdoor-gear brand — provisioned end to end with the new @aws/agentcore-cli, not a single hand-rolled boto3 script. The new teaching point: AgentCore Memory branching, so three specialists can work a ticket at once without stepping on each other's context.

July 2026 agentcore-cli 0.21 Memory Branching Gateway · Code Interpreter · Browser
Contents
  1. The problem: a support desk with four shapes of work
  2. Why CLI-first, this time
  3. Architecture
  4. Memory branching for parallel specialists
  5. Semantic, preference, and summary memory
  6. Sandboxed refund math
  7. A live browser, on purpose
  8. Gateway: a Lambda and an OpenAPI spec become tools
  9. Measuring it honestly
  10. Deploy, observe, tear down
  11. What you get
01

The problem: a support desk with four shapes of work

Beacon is the support agent for Northwind Outfitters, a fictional DTC outdoor-gear brand. A day of tickets looks like: "where's my order," "I want to return these boots, what's my refund," "my tracking hasn't moved in five days, is that normal," and, occasionally, "I was charged for an order I never authorized — get me a person."

Those four shapes of work don't share a tool set or even a shape of correctness. An order-status lookup is a single read. A refund is arithmetic against a policy — restocking fee thresholds, a 30-day window, final-sale exceptions — and arithmetic is exactly what you don't want an LLM doing silently in its head. A tracking-stall complaint needs a live check against the carrier's own service-alerts page, because carriers don't publish a clean "are we delayed" API for a workshop to call. And the fraud complaint needs a human, now, with enough context that they don't have to re-read the whole conversation.

One coordinator agent could try to do all four itself. It would work, in the same way a 400-line system prompt with twelve tools works — right up until you need to reason about which of those tools actually ran, or until two of those ticket shapes need to happen at the same time without their working context bleeding into each other.

02

Why CLI-first, this time

This is the third workshop in the series, after Atlas Deal Desk and StockPilot. Both of those used the Python bedrock-agentcore-starter-toolkit and a folder of boto3 provisioning scripts — bin/setup.py creating an S3 bucket, an IAM role, a Memory store, by hand, one API call at a time.

AWS has since shipped @aws/agentcore-cli, and it changes the provisioning story completely. agentcore create scaffolds a declarative agentcore.json — a flat resource model where runtimes, memories, gateways, and credentials are independent top-level arrays — plus an auto-managed CDK project underneath it. agentcore add memory --strategies SEMANTIC,USER_PREFERENCE,SUMMARIZATION,EPISODIC writes a memory resource into that JSON. agentcore add gateway-target --type lambda-function-arn wires a Lambda into an MCP gateway. No boto3, no hand-written CloudFormation — agentcore deploy synthesizes the CDK stack and ships it.

The discipline this repo holds itself to

Provision via agentcore add; write code only for what the CLI doesn't model. That turned out to be exactly one thing here: the mock order database and the Lambda that reads it. Everything AgentCore-native — Memory, Gateway, the two gateway targets, the inbound/outbound auth — is declared in agentcore.json, not hand-rolled.

Concretely, this repo's bin/ folder is three scripts, not the six or seven you'd expect from the older toolkit: setup_backend.py (DynamoDB table + seed data), deploy_order_lambda.py (zip and ship the Lambda — the CLI only tracks its ARN, it doesn't own the Lambda's code), and teardown.py. Memory, Gateway, and both gateway targets never appear in bin/ at all.

03

Architecture

Your AWS account · provisioned by agentcore.json, not boto3 CUSTOMER MESSAGE "NW-10102 is late AND I want a refund on NW-10095" COORDINATOR · Strands Agent (Claude on Bedrock) handles simple lookups itself · dispatch_specialists for the rest escalate_to_human · load_skill Billing Code Interpreter (refund math) branch: "billing" Shipping Browser tool · async warehouse check branch: "shipping" Returns Code Interpreter (refund preview) branch: "returns" — asyncio.gather: all three run concurrently — BeaconGateway (MCP) OrderRefundLookup (Lambda) · CarrierStatusApi (OpenAPI) → DynamoDB order DB · real carrier website BeaconMemory SEMANTIC · USER_PREFERENCE · SUMMARIZATION · EPISODIC + one isolated branch per specialist per session AgentCore Runtime — /invocations (SSE) · /ping CUSTOM_JWT inbound auth · CodeZip build, no Docker Observability OTEL spans → CloudWatch, no extra vendor
Beacon end to end. The coordinator handles simple lookups itself; anything cross-domain fans out to Billing/Shipping/Returns concurrently, each on its own AgentCore Memory branch.
04

Memory branching for parallel specialists

Atlas Deal Desk used AgentCore Memory but not branching — its four sub-agents ran, returned a compact summary, and were done. Beacon adds the piece Atlas didn't need: three specialists that can be invoked more than once across a session, concurrently with each other, and whose own turn-by-turn history has to stay theirs.

AgentCore Memory's branching primitive is exactly a git branch for a conversation: a session has a "main" line of events, and create_event(..., branch={"name": ..., "rootEventId": ...}) forks a new line off any point in it. Give each specialist its own branch name, fork it off the coordinator's current turn, and three specialists can work the same ticket at once without any of them seeing the others' scratch work — while still being able to read their own prior turns if the coordinator calls them again later in the same session.

def open_branch(client, memory_id, actor_id, session_id, branch_name):
    # Fork off the current tip of "main" — unused after branch creation,
    # record() only sends rootEventId on a branch's very first event.
    root_event_id = _tip_of_main(client, memory_id, actor_id, session_id)
    return SpecialistBranch(client, memory_id, actor_id, session_id, branch_name, root_event_id)

# in the coordinator's dispatch tool:
results = await asyncio.gather(*(_run_specialist(d, request) for d in chosen_domains))

The eval harness's one honestly-different task (a "my order is late AND I want a refund" ticket that needs both Shipping and Returns) doesn't get a better answer from this — both specialists are perfectly capable of running one after another. What changes is wall-clock time, because asyncio.gather runs them at once instead of in sequence, and auditability, because each specialist's reasoning lands on a branch you can pull up in isolation afterward instead of interleaved into one conversation.

The honest framing

This repo's own eval suite says so directly, in the module docstring: multi-agent mode isn't "smarter" on any single task — it's faster on the cross-domain one and more auditable everywhere memory is deployed. That's the claim the --compare flag exists to check, not oversell.

05

Semantic, preference, and summary memory

BeaconMemory carries four strategies, declared once in agentcore.json via agentcore add memory --strategies SEMANTIC,USER_PREFERENCE,SUMMARIZATION,EPISODIC. The coordinator gets all four for free: AgentCoreMemorySessionManager's retrieval_config retrieves matching records from every configured namespace and injects them into the model's context automatically, every turn.

The three specialists deliberately don't use that session manager — they're on isolated branches, not the main session, so the automatic injection doesn't reach them. Rather than let that mean "specialists forget who the customer is," a small SpecialistMemoryHooks hook queries the SEMANTIC and USER_PREFERENCE namespaces directly on a specialist's first turn:

class SpecialistMemoryHooks(HookProvider):
    """Give a specialist the same semantic-facts + preference recall the
    coordinator gets for free, since specialists skip the session manager
    entirely (they use isolated memory branches instead — see branch_hook.py)."""

    def _inject_context(self, event: BeforeInvocationEvent) -> None:
        context_items = recall(self.client, self.memory_id, self.actor_id, query_text)
        if context_items:
            last_message["content"].insert(0, {"text": f"<customer_context>{...}</customer_context>"})

Escalation handoff works the same way, from the other direction: tools/escalation.py is one line — flag agent.state["escalation"] — and a separate hook, EscalationSummaryHooks, notices that flag after the turn ends and composes the actual handoff note, pulling from whatever the SUMMARIZATION strategy has already consolidated plus the raw tail of the branch if that consolidation hasn't caught up yet. The tool that decides "this needs a human" never has to know how a handoff document gets built.

06

Sandboxed refund math

Billing and Returns share one rule: never compute a refund amount in the model's head. A 15% restocking fee past a 14-day window, a 30-day eligibility cutoff, a final-sale exception — that's exactly the kind of arithmetic-with-conditionals an LLM will occasionally get subtly wrong, silently, with total confidence. So it runs as a short Python snippet inside AgentCore Code Interpreter, a fresh microVM per call:

@tool
def calculate_refund(order_total_usd, items_returned_value_usd,
                     days_since_delivery, restocking_fee_pct=0.0) -> str:
    code = REFUND_CODE_TEMPLATE.format(...)   # plain arithmetic, no policy judgment
    with code_session(REGION) as client:
        result = client.invoke("executeCode", {"code": code, "language": "python"})
    return json.dumps(json.loads(_extract_stdout(result)))

The eligibility decision (is this order even in the return window) comes from a separate Gateway/Lambda tool reading the actual delivered date; the sandbox only ever computes the dollar amount from numbers the model has already been given. Splitting "decide" from "compute" is what makes both halves individually auditable — you can trace exactly which policy branch fired and exactly what arithmetic ran, as two separate spans.

07

A live browser, on purpose

"Is there a shipping delay" is the one question in this workshop that doesn't have a clean API to call, because real carriers don't publish one for a workshop's benefit. That's the actual justification for AgentCore Browser here, not a contrived example: Shipping drives a real Chromium session, over CDP, against UPS/FedEx/USPS's own public service-alerts pages, and reads whatever they say right now.

with browser_session(REGION) as client:
    ws_url, headers = client.generate_ws_headers()
    with sync_playwright() as playwright:
        browser = playwright.chromium.connect_over_cdp(ws_url, headers=headers)
        page.goto(url, wait_until="domcontentloaded")
        text = page.inner_text("body")   # best-effort scrape, not structured data
Grading this honestly

The eval task for this tool grades on whether it was called, not on what the page said — a real carrier's alert banner is outside anyone's control on any given day. That's a deliberate, stated trade-off in the eval harness, not an oversight.

08

Gateway: a Lambda and an OpenAPI spec become tools

Order and refund data lives behind a Lambda; the carrier status API is described by a plain OpenAPI schema. Neither specialist calls either directly — both are declared as BeaconGateway targets, and Gateway turns them into MCP tools the agents pick up through one shared MCP client:

agentcore add gateway-target --name OrderRefundLookup --gateway BeaconGateway \
  --type lambda-function-arn --lambda-arn <arn> --tool-schema-file gateway/order_lambda/tool-schema.json

agentcore add gateway-target --name CarrierStatusApi --gateway BeaconGateway \
  --type open-api-schema --schema gateway/carrier_openapi.json \
  --outbound-auth oauth --credential-name CarrierApiOAuth

The Lambda target needs a tool-schema file because a Lambda's input/output shape isn't otherwise discoverable; the OpenAPI target needs nothing extra — Gateway derives the tool list from the spec itself, including the outbound OAuth credential the carrier API requires, held by AgentCore Identity and never written into agent code. Inbound auth is the mirror image: Beacon's own Runtime is configured with a CUSTOM_JWT authorizer, so only verified customer sessions reach it in the first place.

ConcernThe old way (Atlas/StockPilot)Beacon
Expose a backend as toolsHand-written tool functions calling an internal APIagentcore add gateway-target derives tools from a Lambda schema or an OpenAPI spec
Hold the outbound credentialEnvironment variable, or a Secrets Manager call in codeAgentCore Identity, via --outbound-auth oauth --credential-name ...
Gate who can invoke the agentNot modeled — left to whatever fronted the LambdaauthorizerType: CUSTOM_JWT on the Runtime itself
09

Measuring it honestly

Six tasks, one suite, run against both a single-agent coordinator and the full multi-agent one: an order-status lookup, refund-eligibility math, a live carrier-delay check, an escalation trigger, memory recall of a seeded preference, and the one cross-domain ticket that touches both Shipping and Returns.

# single-agent vs. multi-agent, side by side
uv run --project app/beacon python evals/run.py --compare

Five of those six tasks are built to pass identically in both modes — dispatch_specialists is available as a tool either way, it just runs its chosen specialists one after another instead of concurrently when multi-agent mode is off, with no memory branch forked. Only the cross-domain task is expected to show a difference, and it's a timing one: concurrent specialists finish faster than sequential ones. Grading tool-call presence rather than tool-output correctness for the two AWS-dependent tools (refund math via Code Interpreter, carrier lookup via Browser) was a deliberate choice too — those sandboxes swallow their own connectivity errors into an AGENTCORE_ERROR string instead of raising, so "was it called" stays a stable signal even against a live but momentarily unreachable service.

10

Deploy, observe, tear down

Local iteration is two terminals, no separate invoke flag to remember:

# terminal 1
agentcore dev --logs

# terminal 2
agentcore dev "What's the status of order NW-10042?" --stream

Deploying is one command; CDK synthesizes the whole stack — Runtime, Memory, Gateway, both gateway targets — from agentcore.json and creates the execution role with exactly what those declared resources need:

agentcore deploy
agentcore invoke "What's the status of order NW-10042?"
agentcore traces list
The one manual IAM step

Code Interpreter and Browser use the AWS-managed default sandbox rather than a customer-owned one, so nothing in agentcore.json declares them as a project resource — which means CDK doesn't know to grant them. That's the single inline policy this repo's README asks you to add by hand after your first deploy; everything else is automatic.

Every Runtime invocation and Gateway tool call is already an OpenTelemetry span in CloudWatch — agentcore logs and agentcore traces list read from the same place the console does. Teardown mirrors setup: agentcore remove all && agentcore deploy tears down everything AgentCore-native, and bin/teardown.py deletes the one thing the CLI never touched — the DynamoDB order table and its Lambda.

11

What you get

Beacon ships as the same kind of hands-on workshop as its predecessors: a starter/ tree — a full copy of the reference agent with eight functions stubbed — and a complete implementation to diff against.

EXERCISES 1–4
Memory retrieval → branching → recall → handoff
Wire all four BeaconMemory strategies into the coordinator, fork and append to a specialist's branch, recall shared context outside the main session, and compose an escalation handoff note.
EXERCISES 5–8
Sandbox → browser → fan-out → escalate
Run refund math in Code Interpreter, drive a real browser session for a carrier lookup, gather specialists concurrently vs. sequentially, and flag an escalation from inside a tool.

The throughline across all three of these workshops hasn't changed: every piece — a sub-agent, a memory branch, a gateway target, a skill — is something you can add, version, test, and reason about on its own. What's different here is who's holding the provisioning pen. Atlas and StockPilot proved the architecture is worth the composition tax. Beacon is the proof that, with the current tooling, you no longer have to hand-write the plumbing to get it.

Code

The full repository — the coordinator, three specialists, the memory hooks, the Gateway targets, the eval harness, the Streamlit UI, and the CLI-driven agentcore.json — is on GitHub: github.com/laghao/beacon-support-agentcore. Runs entirely on Amazon Bedrock + AgentCore. MIT licensed.