Multi-Agent Systems and Bedrock AgentCore: Runtime, Memory, and HITL (AWS GenAI Developer Pro, Module 8)

Module 8 of 16 15 min read D2 · 26% Lab code ↗

This is Module 8 of AWS GenAI Pro Mastery, a free 16-module course that takes you from your first Amazon Bedrock call to AWS-certified.

A long-time CloudCart customer writes in: “this is the third time I’m asking — just refund order #1042 already.” Relay, as you built it in Module 7, can act on this. It has a lookup_order tool to read the order book and, in a richer build, a tool that could move money. So picture the worst case. Relay does not know this is the third time, because it forgot the first two the moment those terminals closed — it has no memory. It has a tool that could execute the refund with nobody watching — there is no human gate on a financial action. And it only runs while your laptop is awake — there is no managed runtime. Three gaps: no memory, no guard on a money-moving action, no place to live. This module closes all three. Relay becomes a deployed service that remembers across turns, hands the refund to a billing specialist, and asks a human before it touches the money.

In this module

You’ll learn how to:

  • Design a motivated multi-agent system: recognize when to specialize (and when one well-tooled agent wins), pick a topology, and weigh its cost in latency, tokens, and complexity (skills 2.1.1 multi-agent facet, 2.1.4).
  • Coordinate agents by specialization and handoff: route a billing ticket from the generalist Relay to a Billing specialist that shares its tools and memory (skill 2.1.4).
  • Persist agent state with AgentCore Memory: short-term (session) versus long-term (cross-session), and what you store versus what you must not (skill 2.1.1 managed-memory facet).
  • Deploy Relay on Bedrock AgentCore Runtime via the agentcore CLI, and place AgentCore against a DIY runtime and Bedrock Agents classic (skill 2.1.1 managed runtime).
  • Gate a sensitive action behind a human-in-the-loop (HITL) approval: a refund proposes, parks in awaiting_approval, and waits for a human to approve or escalate (skill 2.1.5).

You’ll build: Relay deployed on Bedrock AgentCore Runtime with persistent memory, a Billing specialist it hands off to, and a refund action gated behind human approval (awaiting_approval).

Exam domains covered: D2 — Implementation and Integration (26% of the exam). This module covers the multi-agent, memory, and HITL parts of Task 2.1; Module 7 covered the single-agent core, Module 11 the public API and deployment. Domain 2 is shared with Modules 7 and 11.

Prerequisites: Modules 1–7. An AWS_PROFILE that resolves to your course account, and us-east-1 everywhere. You need the Module 7 agent working — the DynamoDB tables relay-orders / relay-tickets, the Module 5 Knowledge Base relay-kb, and the CloudCart MCP server on AWS Lambda.

Where you are in the build

This is one stop on a 16-module road. M1–M7 are behind you; M8 is here; M9–M16 are ahead.

  • ✅ M1–M7 — secured account, structured triage, the converse() model router, RAG ingestion, a cited Knowledge Base, multimodal intake, and a Strands agent that reasons over tools.
  • 👉 M8 — the deployed multi-agent system. Relay runs on AgentCore Runtime, remembers across turns, hands off to a Billing specialist, and gates a refund behind human approval.
  • ⬜ M9–M16 — guardrails, security, deployment, cost, evaluation, observability, and the capstone.

Relay before this module: a single Strands agent, well-tooled, running on your laptop, stateless, with AgentAction.approved stuck at None everywhere. Relay after: deployed on AgentCore Runtime, with persistent memory, a handoff to the Billing specialist, and a refund gated in awaiting_approval — the moment approved finally becomes effective. The public API and the approval endpoint stay in Module 11.

When one agent isn’t enough: multi-agent topologies

Through Module 7, Relay was one agent. That was the right call, and it usually still is. A single well-tooled agent is simpler to reason about, cheaper to run, and easier to debug than a swarm. Every extra agent you add costs you something concrete: one more model turn (latency), the same context handed to a second model (tokens), and one more thing to trace when a run goes wrong (complexity — observability lands in Module 14). So the first rule of multi-agent design is the one the official AIP-C01 blueprint (Task 2.1) leans on hardest: add an agent only when the specialization is genuinely justified.

A refund is that case. It needs a different tone — apologetic, precise about money — its own rules (what is refundable, full versus partial), and a guard rail the generalist must not carry: it has to propose the refund for a human, never execute it. Bolting all of that onto the generalist’s system prompt would muddy a prompt that also has to answer shipping questions and reset passwords. A second agent with its own prompt is the clean separation. That second agent is the Billing specialist (its canonical name in this course — no synonyms): a Strands agent with a refund-tone system prompt and a refund tool.

How do two agents work together? The pattern here is supervisor / handoff. A supervisor is the role that owns the conversation and decides whether to route work elsewhere; the generalist Relay is the supervisor. A handoff is that routing: when a ticket is a billing/refund case, Relay hands the work to the Billing specialist. In this lab the handoff trigger is a deterministic triage-plus-keyword pre-route (is_refund_request, cheap and testable), not a model deciding to delegate — a model-driven supervisor that reasons about which specialist to call is the richer Agent Squad form. Critically, a handoff routes the work — it does not clone it. The specialist reasons with the same CloudCart tools (lookup_order, create_ticket, over the MCP server) and the same memory; only the prompt and the extra refund tool differ. The blueprint also names AWS Agent Squad, an AWS framework for orchestrating several specialized agents behind one coordinator — the more general form of this idea. You should recognize the term on the exam; this lab builds the simpler two-agent supervisor/handoff directly with Strands, so Agent Squad is a concept you can name, not a dependency you install.

flowchart TD
    C["Customer ticket"] --> R["Relay — generalist (supervisor)"]
    R -->|"billing + refund:<br/>handoff"| B["Billing specialist"]
    R -.->|"everything else:<br/>stays with the generalist"| R
    R --> KB["search_kb (local KB tool)"]
    R --> TOOLS["lookup_order · create_ticket (MCP)"]
    B --> TOOLS
    B --> REFUND["refund (sensitive) — proposes only"]
    R --> MEM["AgentCore Memory (shared)"]
    B --> MEM

The handoff is deliberately conservative. It fires only when triage classifies the ticket as billing and the message actually reads like a refund. A billing question that is not a refund — “what plan am I on?” — stays with the generalist, so you never pay for a handoff you do not need. Tested live: “Where is order 1042?” handed off to nobody (handed_off=False, the generalist answered); “just refund order 1042” handed off to the Billing specialist.

⚠️ Common misconception: “more agents = better.” Coordination has a real cost — every handoff is an extra billed model turn, more duplicated context, and more surface to debug. A single well-tooled agent beats a swarm far more often than tutorials suggest. Reach for a second agent only when one agent’s prompt is being pulled in two genuinely different directions, the way refund-tone-and-rules pull against generalist support.

Memory: from stateless loop to persistent agent

Module 7’s agent loop was stateless. Each run started from zero; close the terminal and the conversation was gone. That is why the customer’s “third time asking” landed on an agent that had no idea it was the third time. Persistence fixes that, and there are two scopes you must keep distinct.

Short-term memory is the session: the thread of the conversation in progress. When the same customer sends a second message in the same session, short-term memory lets the agent see the first. Long-term memory is cross-session: the facts and preferences about a customer that should survive between separate tickets — that they had a refund last week, that they are on the Pro plan. Short-term is the conversation; long-term is the relationship.

AgentCore Memory is the managed store for both. Short-term lives as session events (the turn-by-turn messages, keyed by a session id); long-term lives as records that a configured strategy distills from those events (in the lab, a semantic strategy named relay_customer_facts). You wire it through a memory client: create_event appends a turn, get_last_k_turns reads the recent ones back. In the live run, a second message in session s1 (“did my refund go through?”) recalled the prior conversation, and AgentCore Memory showed three stored turns for that actor and session.

The discipline matters as much as the mechanism. What you store: short, useful facts and summaries — the order id, the resolution, a stable preference. What you do not store: raw PII in the long-term, durable store. A customer’s name, email, or card never go to a cross-session record as-is; the lab’s long-term write is the resolution summary, which carries no personal data. (Redacting PII at the input edge is Module 10; here it is a one-line boundary in the code.) And memory is best-effort: a memory outage degrades the run to stateless — it never fails the ticket.

The cost shape drives a teardown rule. Short-term events cost cents. Long-term memory is billed per month (~$0.75 per 1,000 records, as of June 2026 — re-verify on the Bedrock pricing page), which makes it the only idle-billed item in the entire lab. So teardown.py purges it.

Deploying on Bedrock AgentCore Runtime

A script on your laptop is not a service. It stops when you close the terminal, holds no session, and isolates nothing. Bedrock AgentCore Runtime is the managed answer: each session runs in an isolated microVM, sessions can last up to 8 hours, runs are isolated per session, and — the line that decides the cost story — idle is free. You are billed per second of active consumption (~$0.0895 per vCPU-hour, as of June 2026), but a deployed agent that is doing nothing costs nothing. Compare that to a long-running container you pay for around the clock.

Bedrock AgentCore is a suite, not a single thing. AgentCore Runtime hosts the agent; AgentCore Gateway turns APIs and Lambda functions into MCP tools; AgentCore Memory is the store from the previous section; AgentCore Identity authenticates the agent’s tool access. This lab uses Runtime and Memory; Gateway and Identity are GA too, and you will see Identity again in production. The deployment tool is the standalone agentcore CLI — installed outside your Python dependencies, like the AWS CLI. It is the current GA tooling; the legacy starter toolkit is contaminated and never used here. Bedrock AgentCore has been GA since 13 October 2025 (us-east-1 included); a couple of components — Agent Registry, Payments — remain in preview as of June 2026, so the lab builds on GA components only.

Should you reach for AgentCore at all? The blueprint frames this as a placement question — AgentCore versus DIY versus classic — so here is the decision to internalize.

AgentCore RuntimeDIY on Lambda / ECSBedrock Agents (classic)
Who manages the runtimeAWS (managed microVM)You (Lambda/ECS, scaling, isolation)AWS (managed, first-generation)
Agent state / memoryBuilt-in AgentCore MemoryYou build it (DynamoDB, etc.)Managed sessions, less control
Long sessions (>15 min)Yes — up to 8 h per sessionLambda caps at 15 min; ECS needs you to run itLimited
Tool identity / authAgentCore Identity (sibling suite component)You wire IAM/SigV4 yourselfManaged within the service
Framework portabilityRun your Strands (or other) agent as-isFully portable, fully your problemTied to the Bedrock Agents model
When to chooseLong-running, stateful agents you don’t want to operateFull control, existing infra, simple/short tasksExisting classic agents; recognize, don’t default to it

The course posture, which matches where AWS is investing: AgentCore Runtime plus Strands in practice; Bedrock Agents classic as exam knowledge. You should be able to place classic agents — first-generation managed agents, action groups, create_agent/invoke_agent — and know that the current path is AgentCore. There is no classic-agent code in this lab.

⚠️ Common misconception: “AgentCore replaces Strands Agents.” It does not. AgentCore is the managed runtime where the agent executes — microVM, sessions, memory, identity. Strands Agents is the framework that defines the agent’s reasoning loop, tools, and handoff. They compose: you write the agent in Strands, then deploy it on AgentCore. One is where the agent runs; the other is what the agent is.

Human-in-the-loop: gating sensitive actions

The point of human-in-the-loop (HITL) is selectivity. You do not put a human in front of every action — that is friction with no safety payoff, and it does not scale. You gate the sensitive ones. For Relay, reading an order is not sensitive, citing a doc is not sensitive, writing a ticket is not sensitive. Moving money is. So exactly one action is gated: refund.

Here is the pattern, step by step, and it is built entirely on contracts frozen since Module 7. The Billing specialist’s refund tool proposes — it returns a structured proposal and moves no money. The agent records that as an AgentAction(approved=None) — the approved field, frozen at M7 and stuck at None until now, finally becomes effective: None means proposed/awaiting. The HITL gate then transitions the TicketRecord to awaiting_approval (a status frozen in the M7 enum, exercised for the first time here) and persists it without executing anything. A human decides later: approve sets approved=True, executes the refund, and moves the ticket to answered; reject sets approved=False, escalates (escalated=True), and moves the ticket to escalated. No money moves until a human says so. That human decision is also a feedback signal — every approval and rejection is data you can later use to improve routing and the agent itself (a thread Module 13 picks up).

sequenceDiagram
    participant C as Customer
    participant R as Relay (generalist)
    participant B as Billing specialist
    participant H as Human reviewer
    participant T as relay-tickets
    C->>R: "third time asking — just refund order 1042"
    R->>B: handoff (billing + refund)
    B->>B: lookup_order(1042) · refund(1042, 12900) PROPOSED
    B->>T: TicketRecord{status: awaiting_approval, approved: None}
    Note over T: gated — nothing executed
    H->>T: approve → execute refund → answered (approved: True)
    H-->>T: reject → escalate → escalated (approved: False)

In this module the approval is local and programmatic — a command, not an HTTP call. The public POST /tickets/{id}/approve endpoint and the relay-events event bus (detail-type relay.approval_required) arrive in Module 11; the human decision and the status transitions are identical, Module 11 only wraps them in an API and an event. That keeps M8 followable on its own.

Reproduced exactly as it is frozen in the course spec, this is the schema the gate exercises:

# AgentAction — frozen M7; field `approved` EFFECTIVE M8 (None / True / False)
class AgentAction:
    tool: str
    tool_input: dict
    result: str
    approved: bool | None = None

The TicketRecord status enum is also frozen whole at M7 — received | triaged | awaiting_approval | answered | escalated | closed | failed — even though M7 exercised only four of them. M8 turns on awaiting_approval; no field is added, no status is renamed.

Build it: deploy, hand off, and gate a refund

The lab takes Module 7’s agent byte-for-byte and adds four things: a Billing specialist, a handoff plus a HITL gate, a local approval path, and an AgentCore deployment with memory. The whole package’s relay/ still passes every smoke test from Modules 1–8 offline. This lab cost me about $0.02 on June 2026 prices — a handful of Amazon Nova 2 Lite (smart-tier) runs, where a refund ticket is the priciest path: the handoff is a deterministic pre-check (no model turn), but the Billing specialist then runs a longer reasoning loop than a generalist answer — lookup_orderrefundcreate_ticket, three smart-tier turns against one. Run teardown.py when you are done; it purges the one idle-billed item (long-term Memory).

The Billing specialist (relay/specialists.py, NEW). A second Strands agent with its own refund-tone prompt and a refund tool that proposes and never executes:

@tool
def refund(order_id: str, amount_cents: int, reason: str) -> str:
    """Propose a refund for a CloudCart order. This does NOT move money. ..."""
    proposal = {"proposed": True, "order_id": oid, "amount_cents": cents,
                "reason": reason_text, "status": "awaiting_approval"}
    return ("Refund PROPOSED and submitted for human approval (not yet executed): "
            + json.dumps(proposal))

It runs on the smart tier (resolved through relay.config — no model ID in the file) and shares the generalist’s CloudCart tools and the same AgentAction journal, so the handoff produces one audit trail.

The handoff and the gate (relay/agent.py, MODIFIED by addition). The Module 7 agent above the additions banner is untouched. The router hands off only refund cases; the gate touches only the sensitive action:

def gate_sensitive_actions(actions: list[AgentAction]) -> bool:
    """Apply the HITL gate IN PLACE; return True if anything was gated."""
    gated = False
    for action in actions:
        if config.is_sensitive_tool(action.tool):
            action.approved = None   # proposed, NOT executed — a human decides later
            gated = True
        elif action.approved is None:
            action.approved = True   # reading an order / writing a ticket needs no human
    return gated

When gate_sensitive_actions returns True, handle_with_handoff sets the status to awaiting_approval and persists without executing the refund.

The human decision (relay/approve.py, NEW). The local/programmatic approval:

def approve(ticket_id, decision, *, load=None, persist=None, resource=None) -> TicketRecord:
    record = load(ticket_id)                       # the awaiting_approval ticket
    idx = find_pending_refund(record.actions)      # the approved-is-None refund action
    action = record.actions[idx]
    action.approved = bool(decision)
    if decision:                                   # APPROVE: execute + journal, -> answered
        execution = _execute_refund(action, resource=resource)
        record.actions.append(AgentAction(tool=config.REFUND_TOOL_NAME,
            tool_input=action.tool_input, result=execution, approved=True))
        new_status, escalated = "answered", record.escalated
    else:                                           # REJECT: no money moved, -> escalated
        new_status, escalated = "escalated", True
    # ...persist the updated TicketRecord through the single relay-tickets writer...

Memory and the deployed runtime (relay/run.py + agentcore/, NEW). run_relay(payload) -> response — the function in relay/run.py, invoked as python -m relay.run — is the frozen invoke contract Module 11’s worker reuses, shape-for-shape:

payload  = {"customer_message": str, "ticket_id": str|None, "triage_intent": str|None,
            "customer_id": str|None, "session_id": str|None}
response = {"ticket_id": str, "status": str, "answer_text": str,
            "handed_off": bool, "gated": bool, "record": dict}

build_app() returns the BedrockAgentCoreApp the runtime serves; setup.py creates the Memory store and records its id; the agentcore CLI deploys it:

uv run python setup.py                                       # tables + MCP + AgentCore Memory
agentcore configure --config-file agentcore/agentcore.yaml   # configure the runtime
agentcore launch                                             # deploy (microVM, idle FREE)
agentcore invoke '{"customer_message": "just refund order 1042", "customer_id": "dana", "session_id": "s1"}'

The end-to-end run. A refund ticket hands off, proposes, and parks — nothing executes:

uv run python -m relay.run "this is the third time I'm asking — just refund order 1042"
--- Relay invocation ---
  ticket_id : ticket-8fed19bf
  status    : awaiting_approval
  handed off: True -> the Billing specialist
  gated     : True (refund awaiting human approval)

--- agent actions (the trail across the handoff) ---
  1. lookup_order({'order_id': '1042'}) [approved] -> {"status": "in_transit", ...}
  2. refund({'order_id': '1042', 'amount_cents': 12900, ...}) [PROPOSED (awaiting approval)]
  3. create_ticket({...,'status':'answered'}) [approved]   # the model picks a status it knows;
                                                            # the gate overwrites the record to awaiting_approval

--- final answer ---
I've submitted a refund of $129.00 for order 1042 for human approval — you'll be notified once it's confirmed.

[HITL] A refund is AWAITING APPROVAL — nothing was charged back yet.
       Approve: uv run python -m relay.approve ticket-8fed19bf --approve
       Reject : uv run python -m relay.approve ticket-8fed19bf --reject

Approving executes the refund and closes the ticket; rejecting escalates it; a second message in the same session recalls the first:

uv run python -m relay.approve ticket-8fed19bf --approve   # -> answered, order 1042 refunded
uv run python -m relay.approve <ticket_id> --reject        # -> escalated, no money moved
uv run python -m relay.run "did my refund go through?" --customer dana --session s1

In the live run, --approve flipped order 1042 to refunded=true, refund_amount_cents=12900 and journaled an execution action with approved=True; a fresh --reject left the money untouched and set escalated=True. Offline, all of this runs on a scripted model and a moto DynamoDB backend — 161 tests pass, 7 skip, with no AWS calls.

Try it yourself. (1) Add a Shipping specialist: mirror relay/specialists.py with its own tone and a request_reshipment tool, and route triage.intent == "shipping" to it — watch the routing follow the triage. (2) Expire the session memory: run two messages in one session, then start a new --session id and ask the follow-up again — the agent no longer remembers. That is short-term (session) versus long-term (cross-session) in action.

In production

A few things change once this is a real service. Tool authentication is the first: the lab fronts the MCP Lambda with a Function URL for simplicity, but a deployed agent needs a real identity layer for its tools — that is AgentCore Identity. Memory retention is a recurring cost and a privacy decision. Long-term records bill monthly and may carry customer data, so set an expiry, write only non-PII facts, and own a purge policy — a Domain 4 concern as much as a Domain 2 one. Long sessions and recovery are why the microVM matters — an 8-hour session that survives a transient fault is the runtime’s job, not yours.

You will also want to observe the multi-agent flow: which agent handled a ticket, the latency it added, and the escalation rate. Each handoff is a real signal and a real cost — a refund ticket runs a longer specialist reasoning loop (one extra tool turn) than a single-tool generalist answer — and you’ll wire that into AgentCore Observability and a CloudWatch dashboard in Module 14. And the human approval queue needs an operating model: who approves, against what SLA, and what happens to a refund that sits in awaiting_approval too long. The gate is cheap to build and easy to neglect.

Exam corner

What the exam tests here. The multi-agent, memory, and HITL parts of Task 2.1. That covers designing autonomous systems with managed memory and state (2.1.1’s multi-agent and managed-memory facets, where the blueprint names AWS Agent Squad as the multi-agent framework), coordinating ensembles of agents through specialization and handoff (2.1.4), and human-in-the-loop approval points and feedback (2.1.5). The two-agent supervisor/handoff orchestration you build here is also the advanced-GenAI-app pattern of skill 2.5.5 (Strands/Agent Squad orchestration). Expect at least one scenario where the right answer is not to add an agent, and one that turns on placing AgentCore against DIY and classic.

Quiz.

  1. A support flow handles uniform tickets well with one tooled agent, but refunds need a distinct tone, their own rules, and a propose-don’t-execute guard rail. What is the best design?

    • A. Keep one agent and add the refund rules to its system prompt.
    • B. Add a Billing specialist and hand off refund tickets to it; keep everything else on the generalist.
    • C. Replace the agent with a fleet of one agent per intent.
    • D. Move refunds to a deterministic Step Functions workflow with no model.
  2. Your agent can read an order, cite a doc, create a ticket, and issue a refund. Which action should sit behind a human-in-the-loop gate, and when?

    • A. All four actions, before every run.
    • B. Reading the order, because it touches a business system.
    • C. The refund only, at the moment it is proposed — the others run unguarded.
    • D. Creating the ticket, because it writes state.
  3. You need an agent that holds stateful sessions for up to an hour, isolates each session, manages tool identity, and you don’t want to operate the runtime. Which fits best?

    • A. AWS Lambda, because it scales automatically.
    • B. Bedrock AgentCore Runtime.
    • C. A self-managed ECS service you keep running 24/7.
    • D. A local script behind an Application Load Balancer.
  4. You must keep the thread of the current conversation and remember a customer’s history across separate tickets, on a tight budget and with PII concerns. How do you scope memory?

    • A. Put everything, including raw customer messages, in long-term memory.
    • B. Short-term (session) memory for the live conversation; long-term (cross-session) memory for non-PII facts only — and remember long-term bills monthly.
    • C. Long-term memory only; sessions are unnecessary.
    • D. No persistence; reconstruct context from the database each time.
  5. A team asks whether to start a new agent on Bedrock Agents classic or on AgentCore plus Strands. What is the right orientation?

    • A. Always Bedrock Agents classic — it is the managed service.
    • B. Always a DIY Lambda runtime for full control.
    • C. AWS’s current investment is AgentCore plus Strands; place classic as first-generation managed agents you should recognize, not default to.
    • D. They are interchangeable; pick by team preference.

Answers. 1 — B. Specialize only when justified, and a refund’s distinct tone, rules, and guard rail justify a Billing specialist reached by handoff; A muddies one prompt, C is a needless swarm, and D throws away the model where it still helps. 2 — C. HITL gates the sensitive action at the point of proposal; gating everything (A) is friction that does not scale, and B/D gate the wrong actions. 3 — B. Long, stateful, isolated sessions with managed tool identity and no runtime to operate is exactly AgentCore Runtime; Lambda caps at 15 minutes, ECS makes you run it, and a local script isolates nothing. 4 — B. Short-term is the session, long-term is cross-session and bills monthly, and raw PII never goes to the durable store. 5 — C. The current path is AgentCore plus Strands; classic is first-generation managed agents to recognize and place, not to default to.

Traps to avoid.

  • AgentCore is the runtime; Strands is the framework — they compose, one does not replace the other. AgentCore is microVM, sessions, memory, and identity; Strands is the reasoning loop, tools, and handoff. You write the agent in Strands and deploy it on AgentCore.
  • HITL does not mean “approve everything.” Gate the sensitive action only. Gating normal traffic adds friction, hides the real control behind noise, and does not scale.
  • AgentCore long-term memory is a recurring (monthly) cost. Memory scope and retention are Domain 2 and Domain 4 decisions. The runtime is idle-free; the long-term store is the one thing that bills while nothing runs — so you purge it.

Key takeaways

  • Add an agent only when the specialization is genuinely justified. One well-tooled agent usually beats a swarm; every extra agent costs latency, tokens, and complexity.
  • A handoff routes the work, it does not clone it. The Billing specialist shares the generalist’s tools and memory, with its own prompt and the extra refund tool.
  • Short-term memory is the session; long-term memory is cross-session. Store useful non-PII facts in long-term, never raw PII — and remember it bills monthly.
  • AgentCore is the RUNTIME, Strands is the FRAMEWORK — they compose. You write the agent in Strands and deploy it on AgentCore Runtime.
  • AgentCore Runtime for long-running, stateful agents you don’t want to operate; DIY Lambda/ECS otherwise; Bedrock Agents classic as exam knowledge. Idle is free on AgentCore.
  • HITL gates the sensitive action, not everything. A refund proposes, the ticket parks in awaiting_approval, and a human approves or escalates.
  • AgentAction.approved is effective from here. A refund is None (proposed) → True (approved/executed) or False (rejected/escalated) — never an unsupervised execution.

What’s next

Relay now remembers, specializes, and waits for human approval before refunds — but it still trusts everything it reads. A customer ticket can hide an instruction that hijacks the agent’s tools. Module 9 gives Relay guardrails on input and output, and proves the defense with an adversarial test suite.

Want the full AIP-C01 question bank and the next module in your inbox? Subscribe here — it’s free, like everything in this series.

Links: Module 8 lab · ← Module 7 · Course index · Module 9 →

References