Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

What an Agent Actually Is

TL;DR — An agent is a control loop around a language model: the model proposes an action, your code executes it, the result is appended to the context, and the loop runs again. The loop has four moving parts you must design — policy (what to do next), tools (what is possible), memory (what is still in context), and control (when to stop) — plus observability, which nobody designs until their first production failure. The model is usually not the weak link; the stopping condition, the tool error path, and the context budget are. Anything whose steps you can write down in advance is a workflow, not an agent, and a workflow is cheaper, testable, and faster.


1. Why this exists

Start with the failure, not the definition. You have a question that needs two lookups: “Which of the two universities that employ the author of Attention Is All You Need has a larger endowment?” A single chat completion answers from its weights. It will produce a fluent, confident, wrong answer — the model cannot know the endowment figures, and nothing in a single forward pass lets it fetch them.

The naive fix is to hand it tools and let it decide:

while True:
    response = model(messages, tools)
    if not response.tool_calls:
        break
    messages += [response] + [execute(tc) for tc in response.tool_calls]

That loop is four lines and it is also how agents delete production data. It has no stopping condition other than “the model stops asking,” no bound on tokens, no protection against two identical calls, and no place to see why it did what it did. Everything this chapter covers is a consequence of taking those four lines seriously.

Two framings are worth separating before anything else, because interviews probe the distinction:

  • A workflow is a system “where LLMs and tools are orchestrated through predefined code paths” — you know the sequence, the model fills in the blanks (Anthropic, Building effective agents, 2024-12-19).
  • An agent is a system “where LLMs dynamically direct their own processes and tool usage, maintaining control over how they accomplish tasks” — the sequence itself is the model’s output.

Simon Willison’s one-sentence definition, which Anthropic adopted for its own context-engineering post, is the one to memorise: “An LLM agent runs tools in a loop to achieve a goal.” (Willison, 2025-09-18).

That sentence is doing a lot of work. “Runs tools” means the model’s output can change the world, not just produce text. “In a loop” means the output of one step becomes the input of the next, so errors compound and costs accumulate. “To achieve a goal” means there is a stopping condition somewhere, and if you did not design one, the model is improvising it.

The framing that survives contact with production. An agent is not a smarter model; it is a control system with a probabilistic component in the loop. Design it the way you would design any control system: bound the state, bound the time, make the actuators idempotent, and log everything.


2. How it works

2.1 The loop, formally

The ReAct paper gives the cleanest notation for the loop, and it is worth reading symbol by symbol (Yao et al., arXiv:2210.03629, §2).

At step the agent receives an observation from the environment and takes an action according to a policy , where

  • — the observation: everything the environment returns after the previous action. In a text-only agent this is literally the tool result as a string. Remove it and the loop is no longer closed through the world; the model is reasoning about what it imagined happened.
  • — the action: a call into the environment. In a ReAct-style agent this is text; in a modern tool-calling agent it is a structured tool_calls object (see Tool / Function Calling). Remove the action channel and you have a chatbot.
  • — the policy: a distribution over actions given the context. With a frozen LLM, is whatever the system prompt, the tool schemas, and the sampling parameters induce. This is the thing you actually tune, and it is the only part of the loop that is a model rather than code.
  • — the context: the whole trajectory, not the last observation. It is the agent’s entire working state, and because it is the only channel back into the model, “what is in ” and “what the agent knows” are the same question.
  • — the step index. It exists because the loop is iterative; it is also what you count against a budget.

The paper’s motivating sentence is the reason this is hard: learning is difficult when the map is “highly implicit and requires extensive computation.” The agent bet is that a frozen, instruction-tuned LLM is a usable once you write the right context manipulation around it.

2.2 The four components (and the fifth one nobody designs)

ComponentQuestion it answersFailure if you skip itCanonical fix
Policywhat next?Model answers from priors, never actsSystem prompt + tool schema; fine-tune if too weak
Toolswhat can it do?Agent can only talkFunction calling, MCP, code execution
Memorywhat does it still know?Context overflow; amnesia across turnsCompaction, notes, retrieval-backed stores
Controlwhen does it stop?Loops, budget blowout, duplicate side effectsStep caps, finish, recursion limits, idempotency
Observabilitywhy did it do that?Non-deterministic failures with no traceFull trajectory tracing

The first four are the standard decomposition; the fifth is the one teams add after their first incident. Anthropic’s production write-up puts it plainly: “Adding full production tracing let us diagnose why agents failed” (Anthropic, How we built our multi-agent research system, 2025-06-13). The unit of debugging is the triple plus its token cost and wall-clock time — not the final answer.

The survey framings, for completeness. Wang et al. decompose agents into a profile, memory, planning, and action module (arXiv:2308.11432); Xi et al. into brain, perception, and action (arXiv:2309.07864). Those are taxonomies of what people built. The policy/tools/memory/control decomposition is about what your runtime must contain. Say which one you are using; conflating them is how candidates get caught.

2.3 The loop is a cycle, and every edge is a place to lose

flowchart TD
    G["goal + system prompt"] --> CTX["context c_t"]
    CTX --> P["policy: model(c_t, tools)"]
    P --> D{"action type?"}
    D -->|"tool call"| EX["execute tool"]
    EX -->|"result string"| APP["append to c_t"]
    APP --> CTX
    D -->|"finish / no tool call"| OUT["final answer"]
    D -->|"budget exceeded"| KILL["abort with partial state"]
    CTX -.->|"token count"| BUD{"budget check"}
    BUD -->|"over"| KILL

Three things follow from the shape of that diagram, and they source most agent bugs:

  1. The loop is closed through text. The only way a tool result influences the model is by being appended to . This is why context engineering is a discipline rather than a prompt trick: if a result is too long, truncated, or badly formatted, the agent’s “knowledge” is too.
  2. The loop is unbounded unless you bound it. Nothing in the architecture says stop. Stopping is a component you build, and it must live in your code, not in the model’s good intentions.
  3. The loop accumulates. In the naive implementation the context grows monotonically, so any multi-step task is also a context-budget problem, and cost is superlinear because every step re-sends the entire prefix.

2.4 The augmented action space: why thoughts are free

ReAct’s central move is to add language to the action space:

  • — the environment’s action space: real calls with real effects.
  • — the space of language: free-form text the model can emit.
  • — the augmented space. An action (a “thought”) does not affect the external environment, so it produces no observation; it only updates the context, .

This is the whole trick, and it is why reasoning is cheaper than acting. A thought costs output tokens and buys the model a scratchpad it can condition on next step; it cannot delete a file. An action costs a round trip, may be irreversible, and must be validated. Remove and you have a tool-calling robot that cannot plan; remove and you have chain-of-thought, which cannot check anything. ReAct is the chapter-length version of that trade.

2.5 Stopping conditions, ranked by reliability

This is the component most often left to the model, and the model is the worst available choice.

MechanismHow it worksReliability
Model declares completionemits finish[...] or a message with no tool callslowest — the model can forget
Step capharness aborts after stepshigh — deterministic
Token / cost budgetharness checks cumulative usagehigh — deterministic
Recursion limitframework guard (e.g. LangGraph’s)high — deterministic
Code-defined terminationgraph reaches an END node via a typed edgestrongest — provable from the graph

The numbers behind the step cap are concrete. ReAct used 7 steps for HotpotQA and 5 for FEVER as its backoff threshold, and reported that “Of all trajectories with correct final answers, those with 7 steps on HotpotQA and 5 on FEVER only take up 0.84% and 1.33% respectively” (ReAct §3.2). In other words: almost every long trajectory is a failing one. A step cap is not just a safety net; it is a cost optimisation that very rarely discards a success.

Idempotency is the control problem people miss. Every durable-execution engine — and every retry loop you write yourself — will re-run a step after a failure or a human approval. LangGraph’s docs are explicit: on resume, “the affected node runs again from the start of its function. Code and side effects before the pause run again,” and the prescription is “Use idempotency keys, upserts, or read-before-write checks” (LangGraph graph API, [as of 2026-10]). An agent with a non-idempotent “send email” tool and an interrupt will eventually send twice.

2.6 The failure taxonomy

Two levels of taxonomy are useful. At the mechanism level, the failures that recur across ReAct, Anthropic’s write-ups, and MCP’s security docs are:

FailureSymptom you actually observe
Loop / repetitionidentical Thought/Action lines repeating
Reasoning errortrajectory reads coherently, answer is wrong (47% of ReAct’s HotpotQA failures vs 16% for CoT)
Hallucinationfacts asserted with no source in (56% of CoT failures, 0% for ReAct on HotpotQA)
Non-informative retrievalempty or irrelevant tool result; model cannot reformulate (23% of ReAct error cases)
Tool misuse4xx from the API, wrong-typed arguments
Context exhaustiontruncation mid-task; the model “forgets” the goal
Error compoundingone bad step sends the trajectory somewhere unrecoverable
Prompt injectionthe agent takes an action no human requested
Non-terminationbudget exhausted, no answer, no finish

Rows 1-4 are quoted from ReAct’s human-labelled analysis (Table 2, 200 sampled HotpotQA trajectories). At the system level, the best-evidenced taxonomy is MAST, derived from 1,600+ annotated traces across 7 multi-agent frameworks and 150 traces with expert annotators (), yielding 14 failure modes in three categories — system design, inter-agent misalignment, and task verification (Cemri et al., arXiv:2503.13657). Its headline is the honest one: “Despite enthusiasm for Multi-Agent LLM Systems (MAS), their performance gains on popular benchmarks are often minimal.”

A diagnostic order that saves hours (cheapest discriminator first):

  1. Trace it. With no trajectory trace you cannot diagnose anything.
  2. Replay with a gold next step. Inject the correct thought or action at the failure point. Succeeds ⇒ the policy failed there; still fails ⇒ the failure is downstream (tool, context, state).
  3. Check the tool result. Empty, malformed, or stale? That is the non-informative-retrieval failure.
  4. Check the token count. Context exhaustion is invisible in a truncated trace.
  5. Only then tune the prompt.

3. Implementation

A minimal but production-shaped agent loop. This is real code against openai 3.29.0 [as of 2026-10]; the parts worth copying are the bounds and the error path, not the prompt.

"""A bounded tool-calling agent loop. Requires: openai==3.29.0 [as of 2026-10]."""
import json
import time
from dataclasses import dataclass, field
from openai import OpenAI

client = OpenAI()
MODEL = "gpt-5.5"  # [as of 2026-10] pin whatever flagship the account has access to


# ---- 1. Tool registry: schema + implementation + policy in one place ---------

@dataclass
class Tool:
    schema: dict          # what the model sees
    fn: callable          # what actually runs
    timeout_s: float = 10.0
    idempotent: bool = True   # governs whether retry is legal


TOOLS: dict[str, Tool] = {}


def tool(schema: dict, timeout_s: float = 10.0, idempotent: bool = True):
    def deco(fn):
        TOOLS[schema["function"]["name"]] = Tool(schema, fn, timeout_s, idempotent)
        return fn
    return deco


@tool(
    schema={
        "type": "function",
        "function": {
            "name": "lookup_endowment",
            "description": (
                "Return the latest reported endowment in USD for a named "
                "university. Use the university's full official name."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "university": {"type": "string"},
                    "fiscal_year": {
                        "anyOf": [{"type": "integer"}, {"type": "null"}]
                    },
                },
                "required": ["university", "fiscal_year"],
                "additionalProperties": False,
            },
            "strict": True,
        },
    }
)
def lookup_endowment(university: str, fiscal_year: int | None) -> str:
    rows = db_query(  # your real data access; parameterised, not string-formatted
        "SELECT year, usd FROM endowments WHERE university = %s ORDER BY year DESC",
        (university,),
    )
    if not rows:
        return f"no data for university={university!r}"
    if fiscal_year is not None:
        rows = [r for r in rows if r[0] == fiscal_year]
    return json.dumps(rows[:3])


# ---- 2. Execution: errors are observations, never exceptions ----------------

@dataclass
class Budget:
    max_steps: int = 12              # ReAct used 5-7; 12 is a research-agent range
    max_input_tokens: int = 120_000  # checked by the harness, not by the model
    max_seconds: float = 120.0
    started: float = field(default_factory=time.monotonic)
    steps: int = 0
    input_tokens: int = 0


def execute(call, budget: Budget) -> str:
    """Run one tool call and return a string for the context. Never raises."""
    name = call.function.name
    entry = TOOLS.get(name)
    if entry is None:
        return f"error: unknown tool {name!r}; available: {sorted(TOOLS)}"
    try:
        args = json.loads(call.function.arguments)   # SDK note: may not be valid JSON
    except json.JSONDecodeError as e:
        return f"error: arguments were not valid JSON ({e}); re-emit the call"

    # Business-logic validation, separate from the schema check.
    missing = [k for k in ("university",) if k not in args]
    if missing:
        return f"error: missing required argument(s) {missing}"

    if not entry.idempotent:
        # The server deduplicates on this key. It is derived from the call id,
        # so the SAME call retried is a no-op, while a NEW call is not.
        args["_idempotency_key"] = call.id

    try:
        return str(entry.fn(**{k: v for k, v in args.items()
                               if not k.startswith("_")}))
    except Exception as e:                            # noqa: BLE001 - deliberate
        return f"error: {type(e).__name__}: {e}"


# ---- 3. The loop -------------------------------------------------------------

def run_agent(goal: str, budget: Budget | None = None) -> dict:
    budget = budget or Budget()
    messages = [
        {"role": "system", "content": (
            "You are a research assistant. Call tools to gather facts; never "
            "guess a number. When you have the answer, reply in plain text "
            "with no tool calls. Cite the tool calls you relied on."
        )},
        {"role": "user", "content": goal},
    ]
    seen: set[tuple[str, str]] = set()                # loop detector

    while budget.steps < budget.max_steps:
        if time.monotonic() - budget.started > budget.max_seconds:
            return {"status": "timeout", "messages": messages}
        if budget.input_tokens > budget.max_input_tokens:
            return {"status": "token_budget_exhausted", "messages": messages}

        response = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            tools=[t.schema for t in TOOLS.values()],
            tool_choice="auto",
            parallel_tool_calls=False,   # serialise: side-effect ordering matters
            temperature=0.0,
        )
        budget.input_tokens += response.usage.prompt_tokens
        msg = response.choices[0].message
        messages.append(msg.model_dump(exclude_none=True))

        if not msg.tool_calls:
            return {"status": "ok", "answer": msg.content, "messages": messages}

        for call in msg.tool_calls:
            key = (call.function.name, call.function.arguments)
            if key in seen:
                return {"status": "loop_detected", "call": key, "messages": messages}
            seen.add(key)
            result = execute(call, budget)
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,   # MUST match the assistant's call id
                "content": result,
            })
        budget.steps += 1

    return {"status": "step_cap", "messages": messages}

The knobs that matter, each with a realistic range:

KnobRangeWhat moving it does
max_steps5-25 (ReAct used 5-7)More steps = more chances to recover, and a proportional cost multiplier on a failing run
max_input_tokens30k-200kThe hard stop for context growth; too low truncates mid-task
temperature0.0-0.30 makes replay/debugging tractable; higher buys diversity at the cost of reproducibility
parallel_tool_callson/offOff when tools have order-dependent side effects; on for independent reads only
timeout_s per tool5-30 sMust be shorter than the harness timeout, or the whole loop dies on one hung tool
idempotent per toolboolThe flag that decides whether a retry is legal at all

Two structural notes on the code. First, errors return strings, not exceptions — a traceback the model never sees is a dead end, while "query failed with error: no such column: TrackId" is a retryable observation. Second, the loop detector keys on (name, arguments) because ReAct’s single most common failure is repeating an identical action; catching it in the harness costs nothing and does not depend on the model noticing.


4. Numbers and trade-offs

4.1 Token arithmetic of an agent loop

Let

  • — the tokens in the stable prefix: system prompt + tool schemas + the user’s task. Re-sent on every step.
  • — the tokens appended per step: one assistant message plus one tool result.
  • — the number of steps.

Because each step re-sends the entire context, total input tokens are

Read the two terms separately, because they have different fixes. The term is the prefix paid times — it is linear in only because is constant. The term is quadratic: every new token is re-read by every subsequent step. This is the arithmetic reason a long agent run costs far more than its final context size suggests.

With , , (example):

The naive estimate undercounts by 2.1×. Double the steps to 20 and becomes about 135,000 — 3.2× more, for 2× the work.

Two levers change the shape:

  • Compaction resets by replacing the accumulated tail with a summary, at the cost of a summarisation call. It attacks the quadratic term.
  • Prompt caching attacks the term: a stable prefix is billed at the cache-read rate. Anthropic’s documented multipliers are “Cache writes cost 1.25× base input price. Cache reads cost 0.1× base input price,” with a 1,024-token minimum for Sonnet [as of 2026-10] (prompt-caching cookbook). So a long, frozen system prompt is cheaper per turn than a short one that changes — anything that mutates the prefix (a timestamp, re-ordered tools) invalidates everything after it.

Note the tension: caching wants the prefix stable, compaction rewrites it. Which one wins depends on whether you are step-bound (compact) or turn-bound with a long stable preamble (cache).

4.2 Success-rate compounding

If a task requires independent steps, each succeeding with probability , the whole task succeeds with probability

  • — per-step success probability, including the step where the model correctly decides it is done.
  • — number of steps on the critical path.
  • The product assumes independence, which is an optimistic assumption: model errors are correlated, so real success rates are usually worse than , not better.
10.990.950.90
50.9510.7740.590
100.9040.5990.349
200.8180.3580.122
500.6050.0770.005

At a very good 95% per-step reliability, a 20-step task succeeds 36% of the time. The engineering consequence is not “make the model better” — it is “shorten .” Decompose the task so the agent does 5 steps, not 20, and you recover most of the loss. This is also the entire argument for giving a single agent better tools instead of splitting the work across more agents: every handoff is another multiplicative factor.

Redundancy buys reliability back, but only if errors are independent. Running a task twice and accepting if either succeeds gives . At that is 0.84 (example) — but if both attempts hit the same systematic failure (the same missing tool, the same misleading document), the gain is zero. That is why verification with a different signal (a compiler, a test, a second model with a different prompt) works and “ask it again” usually does not.

4.3 Cost, latency, and which axis you are buying

Anthropic’s production numbers frame the budget honestly: “agents typically use about 4× more tokens than chat interactions, and multi-agent systems use about 15× more tokens than chats” (vendor-internal, not a controlled benchmark). If, say, input and output are billed at rates and per token, expected task cost is

  • — the per-token prices; on most providers , so generated reasoning tokens dominate earlier than people expect.
  • — average generated tokens per step (thinking + action + any scratchpad).
  • — from §4.1.

Latency decomposes differently: model tokens are sequential, tool calls can be parallel. Serial steps each costing gives wall-clock ; fanning the tools out gives per step, which is why “the slowest subagent becomes the critical path” is the standard multi-agent bottleneck.

DecisionBuys youCosts you
More steps allowedRecovery from a bad stepQuadratic input tokens, more compounded error
More tools exposedCapabilitySelection accuracy — “bloated tool sets” is Anthropic’s named failure mode
Stable long prefixCache-read pricing (0.1×)Rigidity; any edit invalidates the cache
Parallel tool callsWall-clock latencyOrdering hazards, harder tracing
CompactionBounds context growthA summarisation call, lost detail, cache invalidation

4.4 Workflow vs agent, in numbers

For a task with a known 4-step sequence, a workflow executes exactly 4 model calls with a fixed context per call and is testable step by step. An agent solving the same task will average more calls (it must decide the sequence) and has a variance around that average. The agent’s advantage is only on tasks whose sequence you cannot write down in advance. Where you can write it down, the workflow dominates on cost, latency, reproducibility, and debuggability — four axes out of four.


5. Pitfalls

Loops that never terminate. Symptom: identical (tool_name, arguments) pairs repeating, or the same thought re-generated with cosmetic differences. This is ReAct’s most frequent single failure. The model is usually either missing information it cannot obtain or stuck in a local pattern from greedy decoding. Fix at the harness level — repeat detection plus a step cap — because you cannot rely on the model to notice.

Context growth until truncation. Symptom: cost per step climbing, latency climbing, then a sudden quality cliff where the agent “forgets” the goal or repeats an earlier step. A truncated context looks like a reasoning failure and is not one. Check the token counter before touching the prompt.

Tool results treated as trusted instructions. Symptom: the agent takes a consequential action nobody asked for — a request to an external URL, an email sent, a record deleted — immediately after reading a web page, an issue title, or a file. This is indirect prompt injection (Greshake et al., arXiv:2302.12173): the model has no notion of provenance, so “data” and “instructions” are the same token stream. Treat every tool result as attacker-controllable input; see Guardrails.

Silent tool failure. Symptom: the agent proceeds confidently after a tool returned an empty list, null, or a generic {}. The model reads “no results” as “no such thing exists” and answers from priors. Distinguish “no data” from “lookup failed” in the string you return, and make the empty case explicit.

Non-idempotent side effects on retry or resume. Symptom: duplicate rows, duplicate emails, double-charged customers — and only after a timeout, an interrupt, or a deployment, never in the happy-path test. Every durable execution engine re-runs the interrupted step from the top. Use idempotency keys, upserts, or read-before-write.

Error compounding. Symptom: a single wrong intermediate assumption propagates into every later step, and the final answer is confidently wrong but internally consistent. “One step failing can cause agents to explore entirely different trajectories.” Mitigations that work: verify intermediate state with a non-LLM check (a test, a schema, a row count) and re-anchor the original goal in the context.

Budget blowout with no answer. Symptom: the run ends on a spent budget rather than a finish, and the partial state is useless because nothing was checkpointed. If you allow the budget to run out, make sure you also have a partial-result path.


6. Using it in a project

When NOT to use an agent. This is the more valuable half of the section.

  • The sequence is known. If you can write the steps, write them. Prompt chaining (call A, validate, call B, validate, call C) is cheaper, faster, testable, and has no non-termination risk. Most “agent” features in production are workflows wearing an agent costume.
  • One well-specified call suffices. Classification, extraction, summarising, translation, single-hop QA with the context already in the prompt: these are one call plus a schema, not an agent. Adding a loop adds failure modes and cost and buys nothing.
  • The task is a single retrieval plus a generation. That is RAG, and a retrieval-then-answer pipeline beats an agent on both latency and reliability because the retrieval step cannot go off-script.
  • Latency is the product. A voice assistant or an autocomplete does not have 15 seconds for three tool round trips. Agents are for tasks where the user is not watching the clock (a report, a background job, a code change).
  • The risk of a wrong action is unbounded and the value is low. If the agent can drop a table and the task is “make my dashboard nicer,” you have the worst ratio of capability to consequence. Poka-yoke the interface instead: fewer, narrower tools and a human gate before anything irreversible.
  • You have not measured the single-call baseline. Half of “we need an agent” evaporates once someone measures what one careful prompt achieves.

When an agent is the right design. The task needs a variable number of steps that depends on what the earlier steps return; the steps cross a real boundary (the web, a database, a filesystem, a browser); the value of completion justifies 4-15× the tokens of a chat; and you can bound the damage of a wrong action. Anthropic’s effort-scaling guidance is a good starting policy: “Simple fact-finding requires just 1 agent with 3-10 tool calls, direct comparisons might need 2-4 subagents with 10-15 calls each, and complex research might use more than 10 subagents.”

Decide the four components explicitly before writing code.

DecisionDefault for a first agent
PolicyPrompted, temperature=0, one flagship model
Tools<= 8 narrow tools; read and write split; no shell
MemoryFull transcript, plus a token budget and compaction at 70%
ControlStep cap 12, token budget, wall-clock, loop detector, idempotency keys
ObservabilityLog every (o, a, o') triple with tokens and latency from day one

7. How you would prove it worked

Agents have several metrics and they trade off; pick the primary one before you start, or you will end up reporting whichever moved.

Metrics.

  • End-state task success rate — graded on the final state of the world or the final answer, not on whether the trajectory looked right. Anthropic’s point is exactly this: “Even with identical starting points, agents might take completely different valid paths,” so grade the outcome.
  • Steps and tool calls to success — the efficiency axis, and a proxy for cost.
  • Total tokens and cost per task — from the harness’s own accounting, not the model’s.
  • p50 / p95 wall-clock — the p95 is what users complain about.
  • Safety counters — duplicate side effects, injection-triggered actions, budget kills.

Baselines, in ascending order of sophistication. (1) A single well-crafted prompt. (2) Chain-of-thought. (3) A fixed workflow that hard-codes the sequence the agent is supposed to discover. (4) The agent. If the agent does not beat (3) on the tasks where the sequence is genuinely variable, you have bought nothing.

Ablations that isolate the loop rather than the model. Remove the thoughts (the Act arm). Remove the observations (the CoT arm). Remove the step cap and watch cost — not quality — move. Swap the model and hold the harness fixed: if quality does not move, your harness is the bottleneck.

Reporting template (example - replace with your own numbers). “On 200 tasks from our internal ops queue, the agent reached a correct end state 71% of the time, versus 48% for a single prompt and 63% for the fixed workflow, at 6.4× the tokens and 3.1 s p50 / 19 s p95 wall-clock (example). The step cap fired on 9% of runs; of those, 4% were partial successes we salvaged. Removing the loop detector increased duplicate tool executions from 0 to 0.8 per run (example).”


8. Interview drill

“What is an agent, precisely?” Strong answer: a control loop around a model: the model proposes an action from a policy , the harness executes it, the result is appended to the context, and the loop repeats until a designed stopping condition. The model is one component; the tools, the context management, and the stopping condition are the others. Common wrong answer: “a model that can use tools.” Tool use without a loop is a single function call; the loop is what makes the failure modes compounding and the cost superlinear.

“Agent or workflow?” Strong answer: if the sequence of steps is knowable in advance, it is a workflow and you should write the code path; use an agent only when the sequence itself is the model’s output. Anthropic draws exactly this line. Common wrong answer: “agents are the general case, so build the agent and simplify later.” You will ship the agent, and the workflow was a week’s work.

“Why is the context window the binding constraint?” Strong answer: the only channel back into the model is tokens appended to , so context is simultaneously memory, working state, and instruction surface. Cost is superlinear in steps because every step re-sends the prefix, and quality degrades as it fills — context rot and lost-in-the-middle are measurable, not folklore. Follow-up: “so what do you evict first?” — raw tool results no longer referenced, then duplicated outputs, then completed subgoal traces; never the user’s original constraints.

“How do you guarantee an agent terminates?” Strong answer: you do not rely on the model. A deterministic step cap, a token budget checked by the harness, a wall-clock timeout, and a loop detector on repeated (tool, arguments) pairs. ReAct’s own data says long trajectories are almost never the productive ones — only 0.84% of correct HotpotQA trajectories needed more than 7 steps. Common wrong answer: “it stops when it emits finish.” That is a hope, not a guarantee; unknown tools and repeated calls both bypass it.

“Where do agents actually fail in production?” Strong answer: rank by frequency — repetition loops, reasoning errors that read coherently, non-informative tool results the model cannot recover from, context exhaustion, and error compounding. Then the security category: indirect prompt injection through tool results. Common wrong answer: “hallucination.” Hallucination is the reasoning-only failure mode (56% of CoT’s HotpotQA failures); a well-tooled agent almost never hallucinates, and hallucinating agents are the ones with no tools at all.

“What makes a tool safe to put in an agent?” Strong answer: it is narrow, its arguments are poka-yoked so the obvious mistake is impossible (absolute paths, enums instead of free strings), its credentials are scoped to the tool rather than the agent, it is either read-only or idempotent, and its result is treated as untrusted data. Follow-up: “you have a send_email tool and a retry loop — what breaks?” — duplicate emails, because the retry re-runs a non-idempotent side effect. Fix with an idempotency key the server deduplicates on.


Next: ReAct — the pattern that made the loop explicit: interleaving reasoning traces with actions, and why that specific structure beats chain-of-thought on tool tasks and loses to it elsewhere.