Part 6 of 8 — AI Agents in Practice
Previous — Workflow, Agent, or Single LLM Call — How to Decide (Part 5)
The demo loop is not enough
Part 3 showed how the loop works: observe, decide, act, check, repeat. Part 5 helped you decide whether you needed that loop at all, or whether a workflow or a single call would do the job for less.
Say you have made that decision. The task genuinely needs an agent: the next step has to be chosen at runtime, inside boundaries you set. Now you have to build it, and the gap between an agent that works in a demo and one that holds up in production is wider than the loop diagram suggests.
In a demo, the loop is enough: the model observes, decides, calls a tool, sees a result, continues. In production, the same five words have to carry more weight. A production agent is not just a model and some tools. It is a loop wrapped in the things that keep the loop honest: working state that survives across turns, tool contracts that say what each call promises, a procedure for recurring work, an approval boundary in front of consequential actions, a verification step that confirms an action actually landed, a budget and stop rules that bound the cost of going wrong, and a trace that records what happened.
This article focuses on the shape of a production loop, not on a framework or platform. It uses one running example to show where a demo implementation needs to become stricter.
The loop still holds
Here is the loop from Part 3, unchanged:
observe → decide → act → check → repeat
Nothing about that changes in production. What changes is what each word contains.
In a demo, observe is "read the last tool result." In production, observe reads from working state, not just the conversation. In a demo, act is "call the tool." In production, act calls a tool with a contract and an idempotency key. In a demo, check is "did the tool return something." In production, check sometimes has to ask a harder question: did the world actually change the way the tool said it did? A 200 OK can mean "request accepted," not "done." Strictly, 202 Accepted is the HTTP status defined for a request accepted but not yet completed. In practice, production APIs also return 200 OK with an application status such as accepted, and the agent has to honor the contract of the API it actually receives. For an action like a refund, that gap matters.
That last one is the heart of this article. But the shape of the loop is exactly what it was three parts ago.
The loop did not change. The implementation got stricter.
A small build: cancel, then refund
Take one concrete task. A TechNova customer writes in about order #4471: they want to cancel it and get their money back. The order is still awaiting shipment, so the cancellation is legitimate. The agent's job is to cancel the order and issue the refund.
The naive loop is short. The agent calls cancel_order, gets back 200 OK, and calls issue_refund. Two actions, one after the other, both successful. In a demo, this works every time.
In production, this is a real risk, and it often stays invisible until it fails in a costly way.
The problem is what this API's 200 OK actually guarantees. It does not guarantee that the order is cancelled. In TechNova's contract, the response says the cancellation request was accepted. Those are not the same statement. Between accepted and cancelled, a number of things can happen: the cancellation might be queued behind other work and still pending. It might have been accepted by one service and not yet propagated to the one that owns refunds. It might have hit a retry and partially applied. It might be sitting in a fraud-review hold the agent cannot see. The response told you about the request you made. It did not tell you about the state of the world that resulted.
So the naive loop issues a refund against an order that may not actually be cancelled yet. Now you have moved money on the basis of an action that had not landed. That is the failure this article is built around, and it is worth stating as a principle:
A tool response describes the request, not necessarily the world. For an irreversible action, the agent must confirm the world before it acts on the assumption that the action succeeded.
We'll fix this later, when we tighten the loop. For now, hold the puzzle: the agent believed a 200 OK and acted on it, and belief is not confirmation. The diagram below shows TechNova's API contract: an HTTP 200 OK carrying an application-level accepted status.
The minimum architecture map
Before fixing the loop, it helps to see everything a production agent actually needs around it. The demo has a model and a tool. The production agent has more, and each piece earns its place.
The map has these parts:
- User request / input: the task that enters the loop.
- Working state: what the loop knows between steps, separate from the conversation text.
- Model decision: the step where the agent chooses the next action.
- Tool registry: the set of tools the agent is allowed to call. Not "any function." It is a scoped set. Scoping the menu the model sees is not the same as scoping what the systems behind those tools will permit; that boundary is Part 8.
- Tool schemas / contracts: for each tool, what it takes, what it returns, how it fails, and how to verify it worked.
- Skill / procedure: packaged knowledge of how a recurring task should be done.
- Approval gate: a boundary in front of consequential actions, where a human signs off on intent.
- Verification gate: the step that confirms an action landed before the loop continues.
- Stop rules / budget: limits on steps, cost, time, and silence.
- Trace log: a record of every step, for the agent now and for diagnosis later.
- Final response: what goes back to the customer.
None of these pieces is unusual, but each one does a specific job. The difference between a demo agent and a production one is almost entirely in this surrounding structure, not in the model. We will not build all eleven boxes in code. We will examine the ones the cancel-then-refund case actually exercises (tool contracts, the verification gate, state, and approval) and describe the rest. Note what is not on the map: this build never needs external reference knowledge (policy rules, product docs, anything it would have to retrieve), so there is no knowledge-retrieval (RAG) component here. A task that needed that reference material would add RAG alongside the tools; this one does not, and leaving it out is the point. The map shows what this agent uses, not everything an agent could.
Tool contracts
In a demo, a tool is often just a function with a one-line description in the prompt: "cancels an order." That is enough for the model to call it. It is not enough to call it safely.
A production tool needs a contract. Not necessarily a heavyweight spec. But at minimum, for every tool, it answers six questions:
- When to use: the name and description the model decides against. What the tool is for, and when not to reach for it. The remaining fields protect what happens after the model decides.
-
Input shape: what arguments, in what form. (
order_id, anidempotency_key.) - Output shape: what comes back on success, structurally. (A status, an identifier: something machine-checkable, not free text.)
- Failure modes: what the tool does when it cannot do the thing. (Does it raise? Return an error status? Time out silently?)
- Idempotency expectation: is calling it twice safe? For anything that moves money or mutates state, the contract should require an idempotency key so a retry does not double-apply, minted once per logical operation and reused on retries, not regenerated per attempt.
-
Verification method: how you confirm the action actually happened. For
cancel_order, that is "re-read order status and check it reached a terminal cancelled state."
That last field is the one demos skip and production cannot. A tool's own response is a claim about the request; the verification method is how you check the claim against the world.
This is also the point where a schema stops being documentation and becomes a control surface. A typed output shape, checked at runtime, does not just describe what the tool returns. It constrains what the agent is allowed to treat as a valid result, and it lets the next step check the result mechanically instead of hoping the prose lined up. Defining the contract is part of building the tool, not an afterthought to it.
The skill: packaging the procedure
Tools tell the agent what it can call. Knowledge retrieval (RAG, from earlier in the series) tells the agent what it knows. A skill tells the agent how a recurring piece of work should be done: the procedure, in order, with the checks that matter.
For the cancel-then-refund task, that procedure is worth packaging, because it is not obvious and it will be reused. Here is a compact version:
# cancel_order_skill
name: cancel_order
description: Cancel an order and issue any refund owed, safely, when cancellation is legitimate.
procedure:
1. Confirm the cancellation is allowed (order not yet shipped; request is legitimate).
2. Get human approval for the refund amount if it crosses the approval threshold.
3. Call cancel_order with an idempotency key.
4. Verify the cancellation landed: re-read order status; proceed only if terminal-cancelled.
5. Issue the refund with an idempotency key, but only after step 4 confirms.
6. Verify the refund landed: re-read refund status.
7. If any verification fails: do not continue. Wait, retry with backoff, or escalate.
Two things about this file are deliberate.
First, it is short. A skill is the title, the description, and the procedure, not a manual. In practice only the name and description need to sit in front of the agent at all times; the body is pulled in when the agent recognizes the task. The procedure carries the steps that are easy to get wrong (the verify steps), not the steps the model already knows.
Second, a useful way to create a skill is to write it after a successful manual run, not before. Run the task once, note where it fails or becomes unclear, and then capture the steps that worked. A skill written from imagination encodes what you think the work is. A skill written from a successful run encodes what the work actually turned out to be. The procedure should reflect what worked in practice, not only what you expected to work.
State
State is what the loop knows between steps that is not in the conversation text. The conversation is where the customer's words live; state is where the facts the loop has established live. Keeping them separate is what stops the agent from re-deriving the situation from chat history on every turn, and from believing something just because it was said.
For the cancel-then-refund task, the working state holds at least:
order_idcustomer_intentapproval_statuscancellation_statusrefund_statusverification_statusstep_countbudget_remaininglast_tool_result
The important fields here are the status fields. cancellation_status is not "what the tool returned." It is "what we have confirmed about the world." Those are allowed to disagree, and the whole point of the verification gate is to make the loop trust the confirmed field, not the raw tool result.
This working state is short-term memory in the sense Part 4 defined: the application-managed context for the current case, discarded when the case closes. It is deliberately not long-term memory. Nothing here is persisted across cases. What an agent should remember between cases, and what it is allowed to keep about a person, is a governance question we take up in Part 8.
Approval and verification are two different things
It is easy to assume that a human approval step makes a consequential action safe. It does not, on its own. Approval and verification answer different questions.
Approval is about intent. A human looks at "refund $740 on order #4471" and decides whether that should happen. That is a real and necessary gate for consequential actions, and it belongs in front of the refund.
Verification is about outcome. It asks whether the action that was supposed to happen actually happened in the world. Approval cannot answer that, because at approval time the action has not run yet.
So a system can have a human approve the refund, fire it, and still be wrong, if it issued the refund on the basis of an unverified cancellation. Approval signs off on the plan; verification confirms the result. A consequential action that requires approval needs both, in that order: approve the intent, take the action, verify the outcome, then continue.
Check becomes verify-before-commit
Now resolve the puzzle from the cancel-then-refund case.
For most steps, check is cheap. The agent read some data; the check is "did I get a sane result." For some reversible, low-stakes actions, the tool response may be enough. For actions such as cancel, refund, delete, or publish, it is not.
For irreversible or asymmetric-stakes actions, check has internal structure. It is not one question, it is two:
- verify: did the world actually change the way the tool's response implied?
- commit: given that it did, is it now safe to take the next consequential step?
This is not a new loop word. The loop is still observe → decide → act → check → repeat. What changed is that check, for a consequential action, expanded from a glance into a gate. The loop did not change. The implementation got stricter.
Verification only means something if it is independent of the step it checks. If the same call that performed the action also reports whether the action succeeded, the check inherits whatever blind spot produced the result in the first place. It confirms the request was made, which is the thing we already doubted. Independence is the property that makes verification worth doing.
There are three ways to get it, in rough order of cost and independence:
- Schema / postcondition check. Does the response structurally match what success looks like? Cheap, and worth doing always, but it only catches malformed responses. It cannot catch "the tool reported success while the world did not change," because it never looks at the world.
- Ground-truth re-read. After the action, query the state directly. For the cancellation, re-read the order status. The read path is separate from the write path, so it can catch the gap between "request accepted" and "state actually changed," provided the read reaches authoritative state rather than the same cache or stale replica that served the write. A second endpoint is not independent merely because it is a separate call. For consequential state changes, a ground-truth re-read is a strong default.
- Independent verifier. A separate judgment, sometimes from a different model, decides whether the action achieved its purpose. The most expensive option, and the only one available when there is no clean ground-truth to re-read; a second model can still share the first one's blind spots (for example, "did this message actually persuade the customer").
For cancel-then-refund, the answer is mechanism two. The agent calls cancel_order, then re-reads get_order_status, and issues the refund only if the order has reached a terminal cancelled state. The refund tool must still revalidate that precondition when it runs. The agent's verification gate prevents bad sequencing; the backend remains the final enforcement boundary, which is where Part 1 started. If the re-read comes back pending, unknown, or blocked, the loop does not commit. It waits and re-reads with backoff, keeping the same operation identity rather than re-firing the mutation, or escalates to a human.
A reader's comment on Part 3 helped sharpen this framing: confirm the world changed, not just what the tool reported.
The code shape
Here is the safe path as a structural sketch. This is the shape, not the implementation. The working version, with real error handling and backoff, lives in the repo, not the article.
# observe → decide → act → check(verify → commit) → repeat
# Consequential path: cancel an order, then refund — only after verifying.
cancel = call_tool("cancel_order", order_id=order_id,
idempotency_key=key("cancel", order_id))
# check: verify the world, not the tool's claim
status = verify_action_landed(
read=lambda: call_tool("get_order_status", order_id=order_id),
expected="cancelled", # terminal state, not "accepted"
retries=3, backoff="exponential",
)
if status == "verified":
# commit: now it is safe to take the next consequential step
call_tool("issue_refund", order_id=order_id,
idempotency_key=key("refund", order_id))
else:
escalate(order_id, reason="cancellation not verified")
Three things in this sketch are the whole point: the idempotency keys (so a retry does not double-apply), the verify_action_landed re-read between the two consequential actions, and the else branch that refuses to commit when verification fails. Everything else is detail.
One scoping note: the companion lab uses a deterministic controller so its traces are stable and it runs without an API key. Under Part 5's definition that controller is workflow-shaped: code owns the next-action choice. It is the production structure around the loop; swap the decision seam for a model and the choice becomes agentic while the state, contracts, verification gate, budgets, and traces stay the same.
You can run the complete deterministic example, compare the safe and naive traces, and inspect the tests in the Part 6 companion lab.
Budget and stop rules
A loop that can call tools can also run away. Budget and stop rules are how you bound the cost of that.
Stop rules cover four things, not one: step count (the loop has tried too many times), cost (it has spent its budget), time (it has run too long), and silence. The last is the one demos forget. A loop can hang on a step that never errors and never returns, a call that goes idle without closing, and a stop rule that only watches for errors and step caps will wait forever. Production loops need a watchdog that treats silence as a failure too.
Budget is worth thinking of as a blast-radius control, not just a bill. A per-run ceiling, checked before expensive calls rather than reconciled afterward, turns a worst case from a runaway into a clean stop. It is a boundary on what the agent can do, written in cost instead of permissions, the same family as scoping tools and gating consequential actions.
Observability
Everything the loop does should leave a trace. For each step, log: what the agent observed, what it decided, which tool it called, what the tool returned, what the verification read came back with, the resulting state, the cost and latency, and, when the loop ends, why it stopped.
The one line worth singling out is the gap between the tool response and the verification read. That gap is where the cancel-then-refund failure lives. A trace that records both, "tool said accepted; re-read said still pending," lets you see it later instead of guessing. This trace is also what Part 7 will build its diagnostics on; for now, the point is to capture it.
Bounded autonomy
It is tempting to read a working build and decide the lesson is "give the agent more freedom" or "add more agents." It is not. Everything that made this agent safe was a constraint, not a capability. The loop followed a fixed control structure, but the next action was chosen at runtime from the current state. Each tool was scoped to one job and carried a contract. Consequential actions were bounded by idempotency, approval where needed, and verification. The verification step refused to commit on an unconfirmed result. The stop rules were deliberate. The model chose actions; the structure decided which actions were possible at all. In production, reliability depends less on giving the model more freedom and more on placing clear boundaries around it.
What this article does not build
- This is not a framework. It is the shape of a loop; the working pieces live in a repo.
- This is not a vendor-specific platform. Native tool calling is the production baseline here; a protocol layer like MCP standardizes how tools are exposed; a hand-rolled ReAct loop is a teaching artifact, not a production recommendation.
- This is not a multi-agent system. One bounded agent is the whole scope here. When a second agent is justified is outside the scope of this article.
- This is not the diagnostics story (Part 7) or the governance story (Part 8). When the verification read comes back wrong, why it went wrong and how you classify it is Part 7. Who is allowed to touch what, and the audit trail, is Part 8.
What this article builds is the production loop shape: the five words, unchanged, with the implementation made strict enough to trust.
Three takeaways
A production agent is the loop plus its scaffolding.
A loop alone is a demo. A production agent is the loop plus working state, tool contracts, a packaged procedure, an approval boundary, a verification gate, a budget with stop rules, and a trace. The model is only one part of the system.Tool success is not ground truth.
A tool response describes the request, not the world. For irreversible actions,checkhas internal structure: verify that the world changed, then commit to the next step. Verification only counts when it is independent of the action it checks. For consequential state changes, a ground-truth re-read is a strong default.The safest agent is the most bounded one.
Reliability does not come from more freedom or more agents. It comes from constraints: scoped tools, contracts, idempotency, approval, verification, budgets, and stop rules. The model chooses actions; the structure decides which actions are possible at all.
Next: when the verification read comes back wrong, what kind of failure is it, and how do you tell? That is Part 7.
Source note: this article builds on the "keep it simple" and bounded-autonomy principles from Anthropic's Building Effective Agents (Schluntz & Zhang). The verify-before-commit gate, the production architecture map, and the "a tool response describes the request, not the world" framing are this series' own synthesis.
Part of AI in Practice: three practical series on MCP, RAG, and AI Agents, focused on why these patterns exist, where they break, and how to think through the engineering decisions behind them.



Top comments (0)