The situation
Support engineering has a next-step list for the assistant that currently only answers questions. The new asks are actions:
- Look up a customer’s subscription by email or ID. Hits an internal subscriptions API.
- Pause or resume a subscription. Same API, different endpoint, side-effecting.
- Issue a refund for a specific charge. Hits the billing service; writes to the ledger.
- Send a confirmation email after any action. Hits the notifications service.
Four tools. Each lives behind an internal HTTPS API with OAuth2 client credentials. Each has a JSON schema. Each has a blast radius: the lookup is safe; the pause is reversible; the refund is money changing hands. The assistant has to know which tool to call, pass the correct arguments, handle errors, and stop and confirm with the user before anything with a blast radius runs.
The team has six weeks and two engineers. They already have a working retrieval assistant from the previous iteration.
What actually matters
An agent loop has the same shape wherever it’s implemented. The model is given a set of tools, each with a name, description, and input schema. The user asks something. The model decides whether to call a tool, and if so, which one and with what arguments. The caller (us, or a framework, or Bedrock) invokes the tool, gets a result, feeds it back to the model. The model either calls another tool, asks the user a question, or produces a final answer. Repeat until done.
That framing exposes the decisions. The first is tool definition: how tools are described to the model, and how tightly their schemas are enforced. The second is invocation: when the model says “call tool X with arguments Y,” who actually executes that? A Lambda? A local Python function? A remote service? The third is error recovery: when a tool fails, does the agent see the error and retry, or does the whole conversation crash? The fourth is confirmation and guardrails: how the system stops before a side-effecting action and asks the user, and how we prevent the model from calling refund when the user said pause. The fifth is observability: traces of which tools ran, with what inputs, what outputs, how long, how much. The sixth is session state: conversation history and intermediate tool results need to persist across turns without ballooning the PromptThe input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot..
Another thing worth thinking about is where the danger lives. The language model is non-deterministic. A tool that sends money can’t be called non-deterministically. The architecture has to make it structurally impossible for the model to skip a confirmation step for a side-effecting action, not because the prompt told it not to, but because the code won’t let it.
Then there’s debuggability in production. When an agent does the wrong thing, calls the wrong tool, passes the wrong arguments, confuses two customer IDs, we need to see the model’s reasoning, the tool inputs and outputs, the retry attempts, and the final user response. Not just at dev time; every production invocation.
What we’ll filter on
- Tool-definition overhead, how much glue code per tool we write?
- Side-effect safety, is confirmation a structural guarantee or a prompt hope?
- Observability out of the box, traces, metrics, replays without building it ourselves?
- Flexibility, custom tool-selection logic, custom reasoning loops, dynamic tool sets?
- Deployment shape, managed service, container, Lambda, all of the above?
The landscape
-
Bedrock AgentCore. The operational building blocks for running an agent you wrote yourself. A serverless runtime executes the agent code with per-session isolation; a gateway exposes existing APIs and Lambda functions as tools through one uniform interface, so the four internal endpoints become callable without being rewritten as bespoke agent actions; an identity capability brokers scoped credentials so the agent reaches the subscriptions and billing APIs without a standing key in the code; observability emits traces of every step, tool call, and result. The reasoning loop is ours, which means the confirmation gate is ours to build and ours to guarantee. Framework-agnostic and model-agnostic. Ticks 1, 3, 4, 5; 2 becomes a property of code we write rather than a checkbox.
-
Bedrock Agents Classic. The predecessor, where AWS orchestrated the loop and
requireConfirmationon an action group made the gate a config property. It moved to maintenance in June 2026 and closed to new customers at the end of July; existing agents keep running. A team already holding one has a working answer and should read this for the gate design rather than the wiring. For a build starting now it is not selectable, so it stays out of the comparison below. -
A framework agent on our own infrastructure. LangChain or LangGraph, deployed to Lambda, Fargate, or EKS that we operate. LangChain defines tools as Python callables with type-annotated arguments; LangGraph models the agent as a directed graph of nodes. The loop is explicit code we own, exactly as on AgentCore, but session isolation, credential brokering, and tracing come with it as work rather than as services. LangSmith gives traces for a separate subscription. Ticks 4 cleanly; 1, 3, and 5 become ours to build.
-
Custom tool router. We write the loop ourselves against a foundation model’s native tool-use API. Claude’s
toolsparameter, Nova’s equivalent, invoking tools with whatever runtime we like. The model returns a structuredtool_useblock; we parse it, run the tool, feed the result back as atool_resultblock, repeat until the model returns plain text. Maximum control, maximum code, and every operational concern is ours. Ticks 4 entirely; gives up 1 and 3. -
Step Functions + Bedrock. Not an agent, strictly. Step Functions as the orchestrator, Bedrock as a step, tool invocations as other steps. Works when the flow is largely deterministic with a language-model step in the middle: classify the request, then follow a hand-drawn state machine. It does not handle free-form multi-turn reasoning. Useful shape for certain problems; wrong shape for an open-ended support assistant.
-
The older chain pattern. A fixed sequence of model calls. Worth naming to rule out: when the control flow is hard-coded it is not really an agent, and the support assistant’s branching is wide enough that a chain would become a mess of if-statements.
Evaluation
Side by side
| Option | Tool-def overhead | Side-effect safety | Observability | Flexibility | Deployment |
|---|---|---|---|---|---|
| Bedrock AgentCore | Existing APIs via gateway | Our loop, our gate | Traces built in | High | Managed serverless runtime |
| Framework on our infra | Typed Python function | Our loop, our gate | LangSmith (separate) | High | Lambda / Fargate / EKS we run |
| Custom tool router | Schema + dispatch code | Our loop, our gate | Whatever we build | Total | Anything |
| Step Functions + Bedrock | State-machine steps | Explicit states | Native | Low (not free-form) | Managed |
Reading it against the situation: side-effect safety is non-negotiable because a refund moves money, observability is non-negotiable because agent mistakes cost customer trust, and the flow is open-ended enough that Step Functions is the wrong shape. That leaves three options which all put the gate in code we write, so the question stops being who guarantees the confirmation and becomes how much operational machinery do we build around the loop. With two engineers and six weeks, the answer is as little as possible. AgentCore supplies the runtime, the tool interface, the credential brokering, and the traces, and leaves us the part that actually needs our judgement: the dispatcher that refuses to execute a side-effecting tool without a confirmation token.
The three agent loops, laid out
The solution
Tools through the gateway. The four internal endpoints already exist behind OAuth2 client credentials, and they stay as they are. AgentCore’s gateway exposes them through one uniform tool interface, so the agent discovers and calls subscriptions.lookup, subscriptions.pause, billing.refund, and notifications.sendEmail without any of them being rewritten as bespoke agent actions. The schema each tool advertises is what the model reads when deciding which to call, so the descriptions get the same care a public API reference would: what the tool does, what each argument means, and what it returns.
The confirmation gate, in our dispatcher. Agents Classic did this with configuration, a requireConfirmation flag on an action group. AgentCore has no such flag, so the gate is code we write, and it has to be as hard to bypass as the flag was. The tool dispatcher carries a table of which tools are side-effecting. When the model asks for one, the dispatcher does not call it. It returns a pending-confirmation result to the application, which surfaces the prompt to the customer, and the tool executes only when the reply arrives carrying a confirmation token the dispatcher itself issued. The model never sees the token and cannot mint one. subscriptions.lookup is read-only and runs straight through; pause, resume, and refund cannot execute without a token, whatever the model says. That gate is a branch in our code, which makes it testable: a unit test that asks the dispatcher to refund without a token and asserts it refuses is the single most valuable test in the codebase.
Identity, scoped and brokered. AgentCore’s identity capability issues scoped credentials for the calls the agent makes, instead of a standing key embedded in the code. The authenticated customer is the other half: their ID comes from the session the application established, never from an argument the model supplied. A model that has been talked into passing userId: "u_999" gets a refund attempt against its own session identity and fails, because the dispatcher reads the identity from the session context and ignores the parameter.
Isolation, memory, and traces. The runtime executes each conversation in its own session context, so one customer’s run shares nothing with another’s. Managed memory keeps the conversation coherent across turns and reconnects without us designing a datastore and a retention policy. Observability emits a trace per run: which steps executed, which tool was called with which arguments, what came back, how long it took, and where a run failed. When a customer says the assistant did the wrong thing, that trace is the answer.
Guardrails. Bedrock Guardrails apply at invocation, on both the input and the output: denied topics, PII redaction in logs, toxicity filtering. Blocked content returns a configured message instead of reaching a tool.
Worked example
Customer: “I want to cancel my subscription and get a refund for the last month.”
- The application starts a session, having already authenticated the customer as
u_123. The agent sees the message, the gateway’s tool list, and the System promptThe instruction block that frames the model’s behaviour for a session, separate from the user’s messages.. - The model reasons that it needs to find the subscription, pause it, find the recent charge, and refund it. It asks for
subscriptions.lookup. The dispatcher checks its table, sees a read-only tool, and calls straight through with the session’s identity. The subscription comes back. - The model asks for
subscriptions.pause. Side-effecting. The dispatcher returns a pending-confirmation result instead of calling anything, and the front-end renders “Confirm pausing subscriptionsub_xyz?” as a button. - The customer confirms. The reply carries the dispatcher’s token; the call runs; the subscription is paused.
- The model asks for
billing.refundon the last charge, $49. Pending again: “Confirm refunding $49 of chargech_abc?” - The customer confirms. The refund runs and the ledger is written.
- The model asks for
notifications.sendEmail. Low blast radius, self-service, runs without a gate. - The model produces a final response: the subscription is paused, $49 refunded, confirmation email sent.
The trace has eight entries. The session can be replayed. The refund could not have happened without two tokens the model had no way to produce.
What’s worth remembering
- An agent is a loop: reason, call tool, observe, repeat. Every framework has one, so the work is defining tools well, gating side effects, and capturing traces.
- A confirmation gate belongs in the dispatcher, not the prompt. Side-effecting tools return pending instead of executing, and run only against a token your code issued and the model never sees.
- Authenticated identity comes from the session, never from a tool argument. Read the customer ID from session context and ignore whatever the model passed.
- AgentCore is the operational half, not the loop: runtime isolation, a gateway that makes existing APIs callable, brokered credentials, and traces. You keep the reasoning and the judgement.
- Agents Classic closed to new customers in July 2026. Teams already running it keep their
requireConfirmationguarantee; a build starting now rebuilds that gate in code, and should test it.
The assistant ships with four tools, three of them gated, full traces, and a refund flow that needs the customer to press a button twice before money moves. The model can still ask for the wrong thing; the architecture stops that wrong thing from costing the company.