Chapter 1: From Completion to Control Loop
Northline sells headphones. Jordan Lee bought a pair — order O-1001. Eight days later, carrier tracking is still “in transit.” Jordan writes support: Where is my order?
Maya takes the ticket. She could reply by hand once. She cannot do that for every stalled shipment. She opens a case in ShopOps — the system this book builds — and asks it to reach out: pull the live order and shipment, draft a status email to Jordan, and only escalate to refund or reship if the facts justify it.
ShopOps writes the draft. A model proposes the wording. Maya (or a later approval queue) decides whether anything is sent. Jordan only receives mail after that gate.
Jordan asks → Maya opens O-1001 → load facts → draft email → approve → send (or escalate)
That chain is the product. Skipping it is the trap.
If Maya’s request becomes a single model call — “Write a helpful email about this delayed order” — you get a completion. Maybe a good one. That is still not an agent, and it skips the chain above.
Why not? Because nothing in that call is grounded. The text is not tied to a live order or shipment record. Nothing checked whether a refund or reship is even on the table. Nothing decided whether the system is allowed to draft versus send. And if you later bolt on tools without a step budget, nothing stops a confused policy from calling tools forever.
ShopOps’s first rule is blunt on purpose: the system may draft customer email; it may not send without an approval path. That is a control problem. Prompting alone will not carry it.
First principles
A completion is one model call → one continuation.
A chain is a fixed sequence of calls.
A workflow is code-owned control flow; the model fills in steps.
An agent, in this book, is a stateful decision system under uncertainty: it loops over an environment, chooses actions from a constrained space, updates state from observations, and terminates.
observe → decide → act → observe → … → terminate
Why “terminate” is in the sentence: without it you do not have an agent. You have a heater for tokens.
| Term | Meaning |
|---|---|
| Environment | Tools, OMS, carrier APIs, email gateway, humans, clocks |
| Observation (o) | What came back after an action |
| Action (a) | Something from a closed (or tightly constrained) action space |
| State (s) | Structured case state — not only chat text |
| Episode / case | One bounded unit of work (here: O-1001 / Jordan) |
| Step budget | Hard cap so while True cannot burn the company |
Drop these misconceptions early:
- A tool-calling model is not, by itself, a production agent.
- More autonomy is not always better. For customer messages and refunds, less autonomy is often the product.
Concrete example: first contact on O-1001
Same cast. Jordan asked. Maya opened the case. We want the smallest loop that does the middle of the story:
- Load Jordan (customer record).
- Read order O-1001 + shipment status.
- Draft the email to Jordan.
- Propose send — then park for Maya’s approval instead of emailing.
No vendor SDK. Mock model. A few tools. Hard max steps. That is enough to teach the shape.
Diagram
What people confuse:
┌─────────────────────────────┐
│ Autonomous fleet (later) │ broad rights — not our default
├─────────────────────────────┤
│ Agent = control loop │ state + policy + env + terminate
├─────────────────────────────┤
│ Workflow / chain │ code owns the order of steps
├─────────────────────────────┤
│ Completion │ one sample
└─────────────────────────────┘
flowchart TD
Start([case O-1001]) --> Decide[policy: choose action]
Decide --> Act[execute tool / env]
Act --> Obs[observation]
Obs --> Update[update state]
Update --> Check{done or max_steps or awaiting_approval?}
Check -->|no| Decide
Check -->|yes| Stop([park or finish])
Notice: parking on approval is a success mode, not a crash. The loop must know how to stop spinning.
Implementation
cd manuscript/code/shopops
pip install -e ".[dev]"
PYTHONPATH=. python examples/ch01_loop.py
Core loop (shopops/loop.py):
def run_loop(state, policy=None, env=None):
policy = policy or scripted_policy
env = env or mock_env
actions = []
while not state.done and state.step < state.max_steps:
if state.awaiting_approval:
break # park — do not spin
action = policy(state)
actions.append(action)
observation = env(state, action)
transition(state, action, observation)
if action.kind in ("done", "escalate"):
break
return LoopResult(state=state, steps=state.step, actions=actions)
Scripted policy (stand-in for a tool-calling model) — read this as control flow, not as “AI magic”:
def scripted_policy(state: State) -> Action:
if state.customer_name is None:
return Action(kind="get_customer")
if state.order_total_cents is None:
return Action(kind="get_order")
if state.shipment_status is None:
return Action(kind="get_shipment")
if state.draft_email is None:
return Action(kind="draft_email", args={"body": f"Hi {state.customer_name}, ..."})
if not state.email_sent and not state.awaiting_approval:
return Action(kind="send_email", args={"body": state.draft_email})
return Action(kind="done")
Why the environment blocks send without an approval ticket: so the loop can learn the difference between “proposed” and “done.” Typical O-1001 output:
steps: 5
actions: ['get_customer', 'get_order', 'get_shipment', 'draft_email', 'send_email']
customer: Jordan Lee
order_total_cents: 12900
shipment_status: in_transit_stalled
awaiting_approval: True
email_sent: False
That awaiting_approval: True line is the point of the chapter.
Failure modes
Bare while True. No max_steps, no terminal actions → infinite cost and duplicate side effects.
Completion-as-agent. One-shot email skips order lookup, shipment status, and approval.
Tool loop without park. Send “fails,” policy retries send every tick → thrash the gateway and the review queue.
State only in chat. “Did we already draft?” becomes a linguistic accident.
Autonomy inflation. Giving the model real send_email / issue_refund credentials because “agents should act” is how you get fraud and angry customers.
Production considerations
- Cap steps per case; metric when the cap hits.
- Separate propose tools from commit tools (different scopes).
- Prefer park (
awaiting_approval) over busy-wait. - Log actions and observations (full traces in Part VII).
- Keep the loop shape stable when you swap mock LLM → real client.
Chapter summary
- Jordan asks about O-1001; Maya opens ShopOps; the system drafts; approval gates send.
- A completion is not an agent; an agent is a control loop that can stop.
- First contact rule: draft yes, send only through approval.
- Smallest useful loop: customer → order → shipment → draft → propose send → park.
- Code:
shopops/loop.py,examples/ch01_loop.py.
Exercises
- Add
escalatewhendays_since_shippedexceeds a threshold; assert reasonescalated. - Force an infinite tool loop (
get_orderforever); measure steps untilmax_steps. - Rewrite as a fixed 3-step workflow (no
while); list what you lost. - Random legal actions for 100 runs; report how often the case parks cleanly.
References
- Yao et al., 2022. ReAct. arXiv:2210.03629 — reason/act as a research pattern; we treat the systems wrapper as the product.
- Anthropic. Building Effective Agents. 2024. [VERIFY SOURCE] — workflows vs agents framing.