Explainer
What counts as an AI agent? A working definition for engineers
- Author
- HARBAN AI LABS
- Published
- Reading time
- 6 min read
Contents
Why a definition matters
"Agent" is used for everything from a chatbot with a long system prompt to a system that works unattended for hours. That vagueness is expensive: teams argue past each other in design reviews, buy frameworks they do not need, and evaluate the wrong thing.
For engineering purposes, a useful definition is one you can check against code. Which components exist? Who decides what happens next — your program, or the model?
A working definition
Definition. An AI agent is a system in which a model repeatedly chooses the next action from a set of tools, observes the result, and continues — carrying state between steps — until a stop condition is met.
That sentence has five parts, and each is something you can point to in a codebase:
- Model. The component that decides the next step. It reads the current state and returns either an action or a final answer.
- Tools. Typed actions the model can request: search, run code, query a database, call an API, edit a file. Each has a schema for its arguments and returns an observation.
- Loop. After each observation, control returns to the model. Crucially, the model chooses the path, not a fixed sequence in your code.
- State. Whatever carries information from one step to the next: the transcript, a scratchpad, external memory, and — often forgotten — the state of the environment the tools changed.
- Stop condition. The model signalling it is done, backed by hard limits your code enforces: a step cap, a budget, a timeout, a human approval gate.
Remove a part and you have something else. No tools: a chat model. Tools but no loop: a single function call. A loop whose path your code fixes in advance: a workflow.
This lines up with a distinction Anthropic draws in its engineering guidance: workflows orchestrate models and tools through code paths defined in advance, while in agents the model directs its own process and tool use (Anthropic, December 2024 (external site)).
An early formulation: ReAct
A clear early statement of this loop is ReAct (Yao et al., 2022), which prompted a language model to produce reasoning traces and task-specific actions in an interleaved way, with the actions querying external sources — in the paper, a Wikipedia API — and feeding the results back in. The authors evaluated it on question answering (HotpotQA), fact verification (FEVER) and two interactive decision-making environments (ALFWorld and WebShop) (arXiv:2210.03629 (external site)).
The think → act → observe cycle it describes is a good mental model for the loop in the definition above, whatever library you use. Our paper breakdown of ReAct covers the method and its results in more detail.
The smallest loop that qualifies
Here is the whole idea in plain Python. call_model is whatever client you use; it returns text plus zero or more requested tool calls. The message format is illustrative — real provider APIs need their own shapes and tool-call IDs.
from dataclasses import dataclass, field
from typing import Any, Callable
@dataclass
class ToolCall:
name: str
arguments: dict[str, Any]
@dataclass
class ModelTurn:
text: str
tool_calls: list[ToolCall] = field(default_factory=list)
@dataclass
class AgentResult:
status: str # "done", or "step_limit" when the hard cap stopped the run
text: str # the final answer, or the model's last message before the cap
transcript: list[dict[str, Any]] # everything that happened, for the caller
def run_agent(
task: str,
call_model: Callable[[list[dict[str, Any]]], ModelTurn],
tools: dict[str, Callable[..., str]],
max_steps: int = 10,
) -> AgentResult:
messages: list[dict[str, Any]] = [{"role": "user", "content": task}] # state
last_text = ""
for _ in range(max_steps): # loop, with a hard limit
turn = call_model(messages) # the model chooses the next step
last_text = turn.text
messages.append({"role": "assistant", "content": turn.text})
if not turn.tool_calls: # no action requested: the model is done
return AgentResult("done", turn.text, messages)
for call in turn.tool_calls: # act, then record the observation
tool = tools.get(call.name)
try:
if tool is None:
raise KeyError(f"unknown tool {call.name!r}")
result = tool(**call.arguments)
except Exception as exc: # errors are observations too
result = f"error: {exc}"
messages.append({"role": "tool", "name": call.name, "content": result})
# Hard stop: say so, and hand back what was done so far.
return AgentResult("step_limit", last_text, messages)Every part of the definition is visible: the model call, the tool table, the for loop, the messages list as state, and two stop conditions — a natural one (no tool calls) and a hard one (max_steps). Two details are worth copying into real systems. Tool errors are returned to the model as observations instead of crashing the loop, so it can recover. And hitting the step cap is an explicit, distinguishable outcome — status="step_limit", with the transcript so far — rather than a silent empty answer or an exception that throws the work away.
State is where agents get hard
The loop is easy. State is where most production problems live.
- The transcript grows. Every observation is appended, so long runs fill the context window and cost more per step. Worse, models do not use long contexts evenly: one study found performance often highest when the relevant information is at the beginning or end of the input, and degraded when it sits in the middle (Liu et al., 2023 (external site)). Summarise or prune old observations deliberately, and keep the task and constraints where the model will attend to them.
- The environment has state too. Files written, rows updated and messages sent are not rolled back when the loop fails. Make tools idempotent where you can, offer dry-run modes, and run anything destructive in a sandbox.
- Long tasks need durable state. If a run can outlive a process, persist the transcript and a step counter so it can resume instead of starting over.
Stop conditions are a design decision
"The model decides when it is done" is necessary but not sufficient. Decide, in code:
- Natural stop. No tool call, or a dedicated
finishtool whose arguments are the structured result. - Hard limits. Maximum steps, a token or cost budget, and a wall-clock timeout. Anthropic's guidance notes that stopping conditions such as a maximum number of iterations are a common way to keep control (Anthropic (external site)).
- Guard stops. The same tool called with the same arguments several times in a row, or a run of consecutive tool errors, usually means the agent is stuck.
- Human gates. Before irreversible actions — sending, deleting, paying, deploying — pause for approval.
Also decide what a stopped run returns. "Stopped at the step limit; here is what was done so far" is far more useful to a caller than an exception with no context.
When a workflow is the better choice
In a workflow, your code fixes the sequence and the model fills in individual steps: classify, extract, draft, check. It is often the better engineering choice. Prefer a workflow when:
- the steps are known in advance and rarely change;
- you need every run to follow an auditable path;
- latency and cost must be predictable;
- you want validation at specific checkpoints (after extraction, before sending);
- you cannot yet evaluate open-ended behaviour.
Reach for an agent when the path genuinely depends on what is discovered along the way — debugging, open-ended research, navigating an unfamiliar system — and the environment gives useful feedback, such as test results or error messages, that the model can act on.
Many good systems are hybrids: a fixed workflow with one bounded agentic step inside it, with its own tools, budget and stop conditions. Anthropic's guidance recommends finding the simplest solution possible and adding complexity only when it is needed (Anthropic (external site)); that is sound advice regardless of vendor.
Evaluating an agent
Agents need evaluation designed for loops, not single responses:
- Check outcomes, not transcripts. Define tasks with a verifiable end state: tests pass, the right record changed, the right answer returned.
- Run each task several times. τ-bench introduced pass^k, the chance that all k independent trials of a task succeed, averaged across tasks (Yao et al., 2024 (external site)). For anything user-facing, reliability matters more than the best of several tries.
- Record the cost of success. Steps, tokens, wall-clock time and tool errors per run belong in the results table alongside the success rate.
- Read trajectories. Aggregate numbers tell you that runs fail; reading a sample of transcripts tells you why.
When you compare published agent results, remember that the score belongs to the model and its harness — see How to read an AI benchmark table.
A quick test for "is this an agent?"
- Does a model choose between tools at run time?
- Does control return to the model after each observation?
- Is state carried between steps?
- Is there a stop condition beyond "the model said so"?
Four yeses: an agent. Fewer: something else — and possibly something simpler, cheaper and easier to test, which is not a bad thing.
Related insights
Analysis, 7 min read
How to read an AI benchmark table without fooling yourself
Eight questions to ask before you trust a row on a leaderboard: who ran it, which version, under what settings, whether the benchmark still separates models, whether the model has seen it, what the grader checks, how big the noise is, and which harness produced it.
Career, 5 min read
Building a portfolio that proves AI engineering skill
How to choose, evaluate, write up and package projects so that a reviewer can see you build systems that work and measure whether they do — plus how to scope freelance AI work and estimate it honestly.
Guide, 6 min read
A practical checklist for shipping a RAG system
What to have in place before a retrieval-augmented generation system meets real users: a retrieval evaluation set, structure-aware chunking, hybrid search, verifiable citations, permission-aware retrieval, a freshness plan and production monitoring.