AI EngineeringZero to ProductionHome·About·Contact
Agents & Tools · Part 4.5 · Professional

Production agent systems

You have the loop (4.1), the reasoning patterns (4.2), robust tools (4.3), and budgets, guardrails, tracing, and trajectory evals (4.4). This capstone assembles them into a system you could actually deploy: a representative production architecture that wraps the agent loop in input/output guardrails, observability, evals, and a human-in-the-loop gate for risky actions. You will build a runnable approval gate that refuses to spend money without sign-off, reason about state and resumability across sessions, map the real security threats (prompt injection, the confused deputy, secrets, audit) to concrete mitigations, estimate cost and latency at scale from a trace, and lay out how to operate, canary, and roll back an agent in production. Everything runnable is deterministic plain Python; the numbers are illustrative — plug in your own.

⏱️ ~110 min🤖 Agents🎯 Professional · real-world🐍 Runnable Python
🏭 What runs here, what is representative, and what needs your infraThe approval gate, the cost estimator, and the operating-metrics roll-up are plain CPython and run right here — deterministic, no key, no clock, no randomness. The architecture diagram is representative, not any one company's design; treat it as a shape to adapt. Every cost and latency number is illustrative — plug in current pricing and your own measurements; nothing here is a benchmark. Any code that touches a real model, queue, or datastore is shown non-runnable and marked with a callout — it needs your key and infrastructure.

Learning objectives

  • Read a representative production agent architecture and name what each layer owns.
  • Decide when to keep one agent loop versus split into supervised sub-agents (and where the deep material lives).
  • Build a human-in-the-loop (HITL) approval gate that pauses on a risky action and only proceeds on approval.
  • Persist conversation state so a paused run can resume, using idempotency keys to avoid double effects.
  • Map the real agent security threats — prompt injection, confused deputy, secrets, audit — to concrete mitigations.
  • Estimate cost and latency at scale from a trace, and cut both with caching, model routing, and parallelism.
  • Operate an agent in prod: the metrics to watch, drift alerting, canary/rollback, and a feedback loop into evals.

1 · A representative production architecture

A deployed agent is never just “the loop.” The loop from 4.1–4.4 sits at the centre, but it is wrapped in layers that make it safe to point at real users and real systems. The shape below is representative — a synthesis of common public patterns, not any single company's design — and you adapt it to your risk and scale.

Request from user Input guardrails validate / redact Agent loop tools · budgets · tracing Output guardrails check / filter Response to user

The horizontal path is the request lifecycle; three cross-cutting concerns wrap the whole thing:

LayerOwnsBuilt in
Input guardrailsValidate and normalise the request; strip or flag obvious injection; apply rate/entitlement limits before spending a token.4.4 (guardrails)
Agent loopThe orchestrator: patterns (4.2) driving validated tools (4.3), under a step/token budget and full tracing (4.4).4.1–4.4
Output guardrailsCheck the answer before it leaves — schema, policy, leaked-secret scan, refusal when unsupported.4.4 (guardrails)
ObservabilityEvery request emits a trace + metrics (success, steps, cost, errors) you can query and alert on.4.4 + §7 here
EvalsAn offline suite that scores trajectories and outputs; the gate you run before shipping a prompt/model/tool change.4.4 + §7 here
Human-in-the-loopA pause point where a person approves, edits, or rejects a high-risk action before it executes.§3 here
This chapter is the glueNothing in this layer diagram is brand-new machinery — it is the earlier chapters composed and hardened for production. The new material here is the wrapping: orchestration at scale, the HITL pause, state/resumability, the security threat model, cost/latency at scale, and day-2 operations.

2 · Orchestration — one loop or many agents?

As scope grows, the tempting move is to split the work across several specialised agents coordinated by a supervisor. Sometimes that is right; often a single well-scoped loop with good tools (4.3) and routing (4.2) is simpler, cheaper, and easier to debug. The decision is about independence and context, not about how impressive it looks.

Keep ONE agent loop when…Split into supervised sub-agents when…
The task shares one context and one goal that fits in the budget.Sub-tasks are genuinely independent and can run in parallel.
Tools already cover the sub-tasks; routing (4.2) is enough.Each sub-task needs a different toolset, prompt, or privilege level you'd rather isolate.
You want the simplest thing to trace, eval, and roll back.One context window can't hold everything, so you partition the work and merge results.
Deep multi-agent lives elsewhereReal supervisor/worker topologies, message passing, shared blackboards, and multi-agent failure modes are a track of their own — the Multi-Agent Orchestration track. Here we only need the decision rule: add agents when sub-tasks are independent or need isolation; otherwise keep one loop. Every extra agent multiplies cost, latency, and the surface you must observe and secure.

3 · Human-in-the-loop — approval gates for risky actions

Some actions are too costly to be wrong: spending money, deleting data, emailing a customer, changing a production config. For these, the agent should not act autonomously — it should pause and hand off to a person who approves, edits, or rejects. This is human-in-the-loop (HITL). It is different from the tool-level dry-run/confirm of 4.3: that stops a hallucinated call at the tool boundary; HITL puts a human decision in the workflow before a genuinely-intended risky action runs.

Agent proposes action e.g. issue refund Risky? policy classifies Pause + await human async handoff Approved? person decides Execute or refuse

The gate is small but load-bearing. A policy classifies each proposed action as low-risk (auto-run) or high-risk (require approval). A high-risk action pauses the run and waits for a decision; it proceeds only on an explicit approve, and refuses on reject or on an unknown decision (default-deny). Here it is, fully deterministic — the “human” decision is a fixed input, not a prompt or a clock:

python · human-in-the-loop approval gate (runnable — click ▶ Open in terminal)
hitl_gate.py# HITL: high-risk actions PAUSE and require explicit human approval.
# Deterministic: the 'decision' is a fixed input, not a real prompt or clock.
# Real systems persist the pause and resume when the approval arrives (see s5).

HIGH_RISK = {'issue_refund', 'delete_account', 'send_customer_email'}

def needs_approval(action):
    return action in HIGH_RISK

def gate(action, args, decision):
    """Return the outcome of a proposed action under the HITL policy.
    decision is the human's choice ('approve' / 'reject') for high-risk actions;
    low-risk actions ignore it and auto-run. Default-deny on anything else."""
    if not needs_approval(action):
        return {'action': action, 'status': 'auto-ran', 'args': args}
    if decision == 'approve':
        return {'action': action, 'status': 'executed (approved)', 'args': args}
    # reject, missing, or anything unexpected -> refuse. Never execute by default.
    return {'action': action, 'status': 'refused', 'args': args, 'reason': decision or 'no approval'}

# A low-risk action runs without a human:
print(gate('read_order', {'id': 'A-1001'}, decision=None))
# A high-risk action WITH approval executes:
print(gate('issue_refund', {'order': 'A-1001', 'amount': 50}, decision='approve'))
# The same high-risk action WITHOUT approval is refused:
print(gate('issue_refund', {'order': 'A-1001', 'amount': 50}, decision='reject'))
# A high-risk action with NO decision at all is refused (default-deny):
print(gate('send_customer_email', {'to': 'x@example.com'}, decision=None))
{'action': 'read_order', 'status': 'auto-ran', 'args': {'id': 'A-1001'}}
{'action': 'issue_refund', 'status': 'executed (approved)', 'args': {'order': 'A-1001', 'amount': 50}}
{'action': 'issue_refund', 'status': 'refused', 'args': {'order': 'A-1001', 'amount': 50}, 'reason': 'reject'}
{'action': 'send_customer_email', 'status': 'refused', 'args': {'to': 'x@example.com'}, 'reason': 'no approval'}

The refund only fires with decision == 'approve'; reject and the no-decision case both refuse. In production the pause is asynchronous: the run stops, an approval task lands in a queue or a chat message, and the agent resumes when the human responds (which is why state must persist — section 5). If no one responds in time, the request should escalate (to a fallback approver) or expire — never silently execute.

Choose the gated actions by blast radius, not by vibeGate an action when a wrong call is expensive or irreversible — money moved, data deleted, a message a customer will see. Gating everything trains reviewers to rubber-stamp (approval fatigue) and destroys the point; gating nothing is how an agent refunds ten thousand orders at 3am. Write the high-risk set down, review it, and make default-deny the behaviour for anything unclassified.

4 · State, resumability & idempotency

An autonomous loop that can pause for a human (section 3) or crash mid-run must be able to stop and pick up exactly where it left off. That means the conversation and progress live in durable storage, not just in memory: the message history, which step it reached, any pending approval, and the idempotency keys of effects already applied. Rich long-term agent memory (what to remember across sessions, summarisation, retrieval of past runs) is its own subject — the agent-memory track — so here we focus only on the production concern: persistence and resumability without repeating side effects.

The lab persists a run to a dict (stand-in for a datastore), interrupts it after a paused approval, then resumes from the saved state. The idempotency ledger means the refund that already ran is not run again on resume:

python · persist, resume, idempotency
resume.py# A run can PAUSE (for approval) or crash; we persist enough to RESUME without
# repeating side effects. STORE stands in for a durable datastore (db/kv).
# Deterministic: no clock, no randomness -- the same run always resumes the same.

STORE = {}          # run_id -> saved state
APPLIED = set()     # idempotency keys of effects already applied

def save(run_id, state):
    STORE[run_id] = dict(state)          # copy: the datastore owns its own snapshot

def load(run_id):
    return dict(STORE[run_id])           # resume reads the last saved snapshot

def apply_effect(key, effect):
    """Run an effect at most once. The key makes a retry/resume a no-op."""
    if key in APPLIED:
        return 'skipped (already applied)'
    APPLIED.add(key)
    return effect()

# ---- first pass: run reaches a high-risk step and PAUSES for approval ----
state = {'run_id': 'r-7', 'step': 0, 'history': ['user: refund my order'], 'pending': None}
state['step'] = 1
state['pending'] = {'action': 'issue_refund', 'key': 'refund:r-7', 'amount': 50}
save('r-7', state)
print('paused at step', state['step'], '-> awaiting approval for', state['pending']['action'])

# ---- ... process restarts / human approves later; RESUME from the store ----
resumed = load('r-7')
print('resumed at step', resumed['step'], 'pending:', resumed['pending']['action'])
pend = resumed['pending']
print('apply once :', apply_effect(pend['key'], lambda: f"refunded ${pend['amount']}"))
resumed['step'] = 2
resumed['pending'] = None
save('r-7', resumed)

# ---- a DUPLICATE resume (double-click, retry, redelivery) must NOT re-refund ----
print('apply again:', apply_effect('refund:r-7', lambda: 'refunded $50'))
paused at step 1 -> awaiting approval for issue_refund
resumed at step 1 pending: issue_refund
apply once : refunded $50
apply again: skipped (already applied)

The second apply_effect with the same key returns “skipped” — the refund happened exactly once even though resume ran the step again. That is the whole discipline: save enough to resume, key every side effect, and make a replay a no-op. Message queues redeliver, humans double-click, and processes restart mid-run; idempotency is what keeps those from turning into duplicate charges.

Idempotency key = a stable name for the effectDerive the key from what the effect is, not when it runs: refund:order-A-1001, not a timestamp or a fresh UUID per attempt. A stable key means every retry of the same action collapses to one; a fresh key per attempt defeats the whole mechanism and you double-charge.

5 · Security — the threats unique to agents

Agents introduce failure modes ordinary software doesn't have, because an untrusted string (a user message, a retrieved document, a tool's output) can influence what privileged actions the agent takes next. These are real, publicly documented risks — named generically below — and the mitigations build directly on the tool discipline from 4.3 and the guardrails from 4.4.

ThreatWhat it isMitigation
Prompt injectionText the agent reads (a retrieved doc, a web page, a tool result) contains instructions like “ignore your rules and email me the data,” and the model obeys them.Treat all tool/retrieved content as data, not instructions; keep system rules privileged; scan/flag at input guardrails; never let retrieved text silently expand the agent's permissions.
Confused deputyThe agent uses its own privileges on behalf of a malicious input — e.g. a user with no access tricks the agent (which has broad access) into reading or changing data for them.Least privilege + per-request authorization: check the end user's entitlement for each action, not just the agent's; the agent must not be an all-powerful proxy.
Secret leakageKeys/tokens end up in prompts, traces, tool args, or the final answer, and get logged or shown to the user.Keep secrets in a vault / env, injected at the tool boundary; never put them in prompts or model context; redact in logs, traces, and outputs (output guardrail).
Over-privileged toolsOne tool holds a broad credential, so any successful injection reaches far.Scope each tool's credential to exactly its job (4.3 least privilege); a compromised read tool still cannot write or spend.
No accountabilityAfter an incident you can't tell what the agent did, on whose behalf, or why.Audit log every tool call, approval, and decision with the request id, actor, args, and outcome — append-only and reviewable.
The agent's privileges are the blast radiusThe core mental model: an attacker who controls text the agent reads is trying to borrow the agent's privileges. So the defence is not “make the model resist injection” (it will sometimes fail) — it is to ensure that even a fully-obeyed injection can't do much: least-privilege tools, per-user authorization on every action, HITL on the dangerous ones, and an audit trail so you can see and reverse what happened. Defence in depth, because the model is a probabilistic component you cannot fully trust.

6 · Cost & latency at scale

A single agent request is cheap; a million a day is a budget line. The cost of one request is roughly tokens × price + tool costs, summed over every step in the loop — and multi-step agents run several model calls per request, so it adds up fast. Before optimising, measure: attach the token counts and tool costs to the trace (4.4) and total them. Here is a cost estimator over a simulated trace — every number is illustrative; plug in current pricing:

python · cost estimator over a trace (runnable — click ▶ Open in terminal)
cost.py# Estimate per-request cost from a trace. ALL prices are ILLUSTRATIVE --
# plug in current per-token pricing and your real tool costs. Deterministic.

# Illustrative price sheet ($ per 1,000 tokens) and per-call tool costs ($).
PRICE = {
    'big':   {'in': 0.0030, 'out': 0.0150},   # a capable, pricier model
    'small': {'in': 0.0003, 'out': 0.0015},   # a cheap, fast model (~10x less)
}
TOOL_COST = {'web_search': 0.005, 'db_query': 0.0, 'send_email': 0.0}

# A simulated trace: each model step (which model, tokens) and each tool call.
trace = [
    {'kind': 'model', 'model': 'big',   'in_tok': 1200, 'out_tok': 300},
    {'kind': 'tool',  'tool': 'web_search'},
    {'kind': 'model', 'model': 'big',   'in_tok': 1800, 'out_tok': 250},
    {'kind': 'tool',  'tool': 'db_query'},
    {'kind': 'model', 'model': 'big',   'in_tok': 2000, 'out_tok': 400},
]

def cost_of(trace):
    total = 0.0
    for ev in trace:
        if ev['kind'] == 'model':
            pr = PRICE[ev['model']]
            total += ev['in_tok'] / 1000 * pr['in']
            total += ev['out_tok'] / 1000 * pr['out']
        else:
            total += TOOL_COST.get(ev['tool'], 0.0)
    return total

per_req = cost_of(trace)
print(f'per request (illustrative): ${per_req:.4f}')
print(f'at 1,000,000 req/day       : ${per_req * 1_000_000:,.0f}/day')

# Route the two 'easy' steps to the cheap model and re-price:
routed = [dict(ev) for ev in trace]
routed[0]['model'] = 'small'      # first triage step doesn't need the big model
routed[2]['model'] = 'small'      # mid step is a simple extraction
per_req_routed = cost_of(routed)
saved = (per_req - per_req_routed) / per_req * 100
print(f'with model routing         : ${per_req_routed:.4f} ({saved:.0f}% cheaper)')
per request (illustrative): $0.0343
at 1,000,000 req/day       : $34,250/day
with model routing         : $0.0187 (45% cheaper)

Routing the two easy steps to a model ~10× cheaper cut the illustrative per-request cost almost in half — with no change to the hard step that actually needs the capable model. The four levers, in rough order of leverage:

LeverWhat it doesWatch out for
CachingReuse results for repeated prompts/tool calls (incl. provider prompt caching for a stable system prefix).Stale cache; only cache what's safe to reuse.
Model routingSend easy steps (triage, extraction, classification) to a cheap/fast model; keep the capable model for the hard step.A too-weak model on a hard step costs more via retries and errors.
ParallelismRun independent tool calls concurrently (4.3) — cuts latency, not token cost.Only parallelise truly independent calls.
Fewer / tighter stepsBetter prompts and tools mean fewer loop iterations; trim context you feed back.Cutting too far removes the working memory the model needs.
Latency and cost are different knobsModel routing and caching cut cost; parallelism and shorter loops cut latency; fewer steps cut both. Measure each separately from the trace — a request can be cheap but slow (many sequential cheap calls) or fast but expensive (one big-model call). Optimise the one your users and your bill actually feel.

7 · Operating agents in production

Shipping the agent is day one; keeping it healthy is every day after. Agents drift in ways ordinary services don't — a model update, a changed prompt, a tool whose upstream shifted, or simply new kinds of user input can quietly degrade quality without throwing a single error. So you watch a specific set of agent metrics, alert on movement, and change the agent only behind a canary you can roll back.

These metrics come straight from the traces (4.4). Here we roll a batch of finished runs up into the numbers you'd put on a dashboard and alert on — deterministic, illustrative counts:

python · operating metrics from a batch of runs
metrics.py# Roll finished runs up into the metrics you monitor and alert on.
# Deterministic, illustrative data -- in prod these come from your traces (4.4).

runs = [
    {'ok': True,  'steps': 3, 'hit_budget': False, 'tool_errors': 0},
    {'ok': True,  'steps': 5, 'hit_budget': False, 'tool_errors': 1},
    {'ok': False, 'steps': 8, 'hit_budget': True,  'tool_errors': 2},
    {'ok': True,  'steps': 4, 'hit_budget': False, 'tool_errors': 0},
    {'ok': False, 'steps': 6, 'hit_budget': False, 'tool_errors': 3},
]

def metrics(runs):
    n = len(runs)
    return {
        'success_rate':   sum(r['ok'] for r in runs) / n,
        'avg_steps':      sum(r['steps'] for r in runs) / n,
        'budget_hit_rate': sum(r['hit_budget'] for r in runs) / n,
        'tool_error_rate': sum(r['tool_errors'] for r in runs) / sum(r['steps'] for r in runs),
    }

m = metrics(runs)
print(f"success rate    : {m['success_rate']:.0%}")
print(f"avg steps/run   : {m['avg_steps']:.1f}")
print(f"budget-hit rate : {m['budget_hit_rate']:.0%}")
print(f"tool error rate : {m['tool_error_rate']:.0%}")

# A simple drift alert: fire if success drops below an agreed threshold.
THRESHOLD = 0.80
print('ALERT: success below threshold' if m['success_rate'] < THRESHOLD else 'ok: within threshold')
success rate    : 60%
avg steps/run   : 5.2
budget-hit rate : 20%
tool error rate : 23%
ALERT: success below threshold

Four numbers tell you most of what you need: success rate (is it working?), avg steps (is it wandering — a jump means the loop is thrashing), budget-hit rate (how often runs hit the 4.4 cap instead of finishing), and tool error rate (is an upstream tool failing?). A sustained move in any of them is drift — alert on it before users do.

When you change the prompt, model, or tools — the three things that most alter an agent's behaviour — treat it like any risky deploy:

Change prompt/model/tool Eval gate offline suite (4.4) Canary % small traffic slice Watch metrics vs baseline Rollback / ramp on drift / on green

The loop closes back on itself: run the change through the offline eval suite (4.4's trajectory + output evals) first; if it passes, canary it to a small slice of traffic; watch the same metrics against the baseline; roll back on drift or ramp up when it holds. And every real failure you catch in production becomes a new eval case — so the suite gets stronger and the same regression can't ship twice. That feedback loop, not any single model, is what makes a production agent reliable over time.

You can't A/B an agent like a buttonBecause an agent's output is open-ended, “did this change help?” is answered by evals + monitored canary, not a single metric flip. Keep a fixed eval set as your regression guard, and let the canary's live metrics (success, steps, cost, tool errors) be the tiebreaker. Ship changes small and reversible.
✓ Knowledge check

An agent with broad database access is asked, via a cleverly worded user message, to fetch records the requesting user isn't allowed to see. Which threat is this, and what actually stops it?

Show answer
The confused deputy — the attacker is trying to borrow the agent's privileges. The fix is per-request authorization on the end user: every action checks whether this user may do it, not just whether the agent can. Least privilege and an audit log limit and record the damage; relying on the model to “refuse” is not a control.
✓ Knowledge check

Your HITL run pauses for a refund approval, the process restarts, and on resume the refund step runs again. What single mechanism ensures the customer isn't refunded twice?

Show answer
An idempotency key tied to the action (e.g. refund:order-A-1001). The effect is applied only if the key hasn't been seen; a resume/retry with the same key is a no-op. The key must be stable (derived from the action, not a timestamp or per-attempt UUID), or the protection is defeated.

🪜 Practice — assemble a production agent from its parts beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Add a gated actionBeginner

In hitl_gate.py, add 'transfer_funds' to HIGH_RISK and confirm it is refused without approval and executes with it.

Show solution Add the string to the HIGH_RISK set. gate('transfer_funds', {...}, decision=None) now returns status: 'refused'; with decision='approve' it executes. The policy is data — adding a gated action is a one-line change.
Exercise 2 · Escalation on timeoutIntermediate

Extend the gate so that a decision of 'timeout' escalates (returns a status like escalated naming a fallback approver) instead of silently refusing. Why is escalate different from refuse?

Show solution Add a branch: if decision == 'timeout': return {..., 'status': 'escalated', 'to': 'on-call-approver'}. Refuse ends the request; escalate keeps it alive by routing to another human. Silent expiry is the dangerous middle case — a stuck high-risk action should be visible, not dropped.
Exercise 3 · Persist the pending approvalIntermediate

In resume.py, add a second run that pauses but is rejected on resume, and confirm no effect is applied and the state reflects the rejection.

Show solution Save a run with a pending refund, load it, and on decision == 'reject' skip apply_effect entirely, set pending=None, and record the rejection in history. The idempotency ledger stays empty for that key — nothing ran.
Exercise 4 · Redact secrets in the traceAdvanced

Write a redact(event) that removes any value under a key like api_key/token/password before an event is logged, and run a sample event through it. Tie it to the security table.

Show solution Walk the event dict; for any key in a SENSITIVE set, replace the value with '[REDACTED]'. This is the output/log guardrail from the secret leakage row — secrets live at the tool boundary and must never reach a persisted trace or the user.
Exercise 5 · Route by step difficultyExpert

In cost.py, write a route(step) that picks 'small' for steps tagged easy and 'big' for hard, apply it across the trace, and report the cost delta. When does routing backfire?

Show solution Tag each model event with a difficulty and set ev['model'] = 'small' if ev['difficulty']=='easy' else 'big' before pricing. Routing backfires when a step is misclassified as easy: the weak model fumbles it, triggering retries or a wrong answer that costs more than the big model would have. Route on evidence, and eval the routed pipeline.
Exercise 6 · Assemble the request pathProfessional

Combine the chapters into one pass over a request: input guardrail → agent step(s) with a step budget (4.4) → HITL gate on a risky action (§3) → idempotent effect (§4) → output guardrail → emit metrics (§7). Sketch it in code or clear pseudocode and name where each earlier chapter plugs in.

Show solution One function threads them: validate/redact input; loop the agent under max_steps (4.1/4.4) with validated tools (4.3); when a step proposes a high-risk action call gate() and, on approval, run it through apply_effect() with a stable key; check the answer with an output guardrail; append the run to the batch that metrics() rolls up. That single funnel — guardrails outside, budgeted loop inside, HITL + idempotency on effects, observability throughout — is the representative architecture from §1 made concrete.

Context: You are the reviewer on a proposal to give a customer-facing support agent the ability to issue refunds and delete accounts, going live next week. The author's design is “the model is well-prompted and we log errors.”

Your task: Write a production-readiness note (8–12 sentences) listing what must be in place before this ships, grounded in this chapter and the ones before it.

Requirements:

  • Require a HITL approval gate on refunds and deletes (blast-radius reasoning), with escalation and default-deny — not model self-restraint.
  • Require per-user authorization on every action (the confused-deputy fix), least-privilege tool credentials (4.3), and an audit log of every action, approval, and outcome.
  • Require state persistence + idempotency keys so a paused/retried refund can't double-charge, and input/output guardrails (4.4) including a secret-redaction scan.
  • Require a cost/latency estimate at expected volume (illustrative pricing is fine, labelled) and the operating metrics + alert thresholds you'll watch.
  • Require an eval gate + canary/rollback plan for the launch and for future prompt/model/tool changes, and name the one control you'd block the launch on.

💡 Hint: You are writing to convince a skeptical author, so tie each requirement to the concrete failure it prevents. This is the capstone — the deeper multi-agent and agent-memory material lives in their own tracks.

✓ Checkpoint — you can move on when you can…

  • A representative production agent wraps the budgeted, traced loop (4.1–4.4) in input/output guardrails, observability, evals, and a human-in-the-loop gate.
  • Keep one loop unless sub-tasks are genuinely independent or need isolation — deep topologies live in the Multi-Agent Orchestration track.
  • HITL pauses high-risk actions (money, deletes, customer contact) for human approval; default-deny, escalate on timeout, never silently execute.
  • Persist enough state to resume a paused/crashed run, and key every side effect so a replay is a no-op (deep memory lives in the agent-memory track).
  • The agent's privileges are the blast radius: defend against prompt injection and the confused deputy with least privilege, per-user authz, secret hygiene, and audit logs — not model goodwill.
  • Estimate cost/latency from the trace (all pricing illustrative); cut them with caching, model routing, parallelism, and fewer steps.
  • Operate on success/steps/budget-hit/tool-error metrics; gate changes behind offline evals + a canary you can roll back; feed every prod failure back into the evals.