In short
An agent is an LLM in a loop with tools, a step limit and approval gates on anything risky. The model only proposes actions; your code runs them, decides when to stop and records every step so you can test it.
"AI agent" now covers everything from a chatbot to a system that edits your database. Underneath, the working ones share one small mechanism. Once you see it, the production questions get easier: what can it touch, when does it stop, and how do you know it did the right thing?
The loop
An agent is a language model running in a loop. It gets a goal, thinks about the next step, picks a tool, your code runs that tool, and the result goes back to the model. It repeats until it decides the goal is met or something stops it.
The ReAct paper (Yao et al., 2022) named the core idea: interleave reasoning with actions, so each thought is checked against real information instead of the model's guesses. Anthropic's guide puts it as getting "ground truth" from the environment at every step.
Each lap is one model call plus its tool runs, so the lap count is a first measure of cost and latency.
Tools are a contract
The model never runs anything itself. You describe each tool with a name, a description and a JSON schema for its input. When the model wants one, it returns a structured call instead of prose. Your code runs it and sends back the result, tagged with the call's id.
The two big APIs use the same shape. With Claude, the response has stop_reason: "tool_use" and a tool_use block, and you reply with a tool_result. With OpenAI's Responses API, the output contains a function_call item with a call_id, and you reply with a function_call_output. Both offer strict: true so calls match your schema.
The model proposes; your code decides whether to run it.
Tool design deserves as much care as the prompt. Write descriptions a new colleague could follow, and shape the arguments so mistakes are hard to make. Anthropic calls this "poka-yoke" for tools.
Memory and state
The conversation is the state. On each lap you send the whole message list again: the goal, every call the model made and every result. Anything that must last longer than one run, such as notes or past decisions, lives in a store you own and reach through a tool.
When it stops
With Claude, the loop continues while the stop reason is tool_use and ends on anything else, normally a final answer. That is the model's view of "done". You need your own as well:
- a maximum number of steps,
- a time or token budget,
- a clean failure when either runs out, so a person picks it up.
type ToolCall = { id: string; name: string; input: unknown };
type ModelTurn = { done: boolean; text: string; calls: ToolCall[] };
type Message = { role: "user" | "assistant" | "tool"; content: unknown };
declare function callModel(messages: Message[]): Promise<ModelTurn>;
declare function runTool(call: ToolCall): Promise<string>;
declare function needsApproval(call: ToolCall): boolean;
declare function requestApproval(call: ToolCall): Promise<boolean>;
const MAX_STEPS = 8;
export async function runAgent(goal: string): Promise<string> {
const messages: Message[] = [{ role: "user", content: goal }];
for (let step = 0; step < MAX_STEPS; step++) {
const turn = await callModel(messages);
messages.push({ role: "assistant", content: turn });
if (turn.done) return turn.text;
for (const call of turn.calls) {
const approved = !needsApproval(call) || (await requestApproval(call));
const result = approved ? await runTool(call) : "Rejected by a reviewer.";
messages.push({ role: "tool", content: { id: call.id, result } });
}
}
throw new Error(`Stopped after ${MAX_STEPS} steps without finishing.`);
}
This is provider-neutral: callModel wraps whichever API you use. In production the approval is often a saved, paused run that resumes later, not an await.
Guardrails and human approval
Split tools by what they can break. Reading data is usually safe to run freely. Sending, paying, deleting or changing records goes through a gate where a person approves the exact action.
Safe steps flow; risky ones wait for a person, and the agent can't route around the gate.
Our invoice reconciliation agent works this way. It can read the bank feeds and write only to reconciliation fields in the ERP. Unsure matches go to a review queue, it drafts reminder emails, and nothing is sent until someone approves it. Every match and approval is in an audit log.
Mostly a workflow, with small agents inside
That case is typical. The order of operations, permissions and retries belong to a plain workflow, and the agent gets one narrow job with a short tool list. We covered how to decide in Agent or workflow?
Testing and observing it
Log every step as a trace: each model call, tool call, input, result and approval. That trace is your debugger, audit trail and test data.
Then build evals. Anthropic's guide on agent evals suggests starting with 20 to 50 tasks drawn from real failures, and grading both the outcome and the transcript with a mix of code, model and human graders. Check that the agent reached the right end state without taking a shortcut on the way, and read the transcripts yourself.
In practice
- Give the agent one goal and the fewest tools that can reach it.
- Make read tools free and put every side effect behind approval.
- Set a step and budget limit, and fail loudly when it's hit.
- Trace every call before you ship, and turn each production failure into an eval case.
If you are planning an agent and want a second opinion on where the loop should end and the workflow begin, see our services.
Sources
- Building effective agents – Anthropic Engineering, December 19, 2024
- Tool use with Claude – Claude API docs
- How tool use works – Claude API docs
- Function calling – OpenAI API docs
- ReAct: Synergizing Reasoning and Acting in Language Models – Yao et al., arXiv 2210.03629
- Demystifying evals for AI agents – Anthropic Engineering, January 9, 2026